diff --git a/.cache/.gitkeep b/.cache/.gitkeep new file mode 100644 index 0000000000000..e69de29bb2d1d diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000000000..a82f998c83d0d --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,25 @@ +// For format details, see https://aka.ms/devcontainer.json. +{ + "name": "WordPress Core Development", + "dockerComposeFile": "docker-compose.yml", + "service": "app", + "workspaceFolder": "/workspace", + + // Features to add to the dev container. More info: https://containers.dev/features. + "features": { + "ghcr.io/devcontainers/features/common-utils:2": { + "username": "wordpress" + }, + "ghcr.io/devcontainers/features/node:1": { + "version": "20" + }, + "ghcr.io/devcontainers/features/docker-in-docker:2": {}, + "ghcr.io/devcontainers/features/git:1": {} + }, + "onCreateCommand": "sudo chmod +x .devcontainer/install-tools.sh && .devcontainer/install-tools.sh", + "postCreateCommand": "sudo chmod +x .devcontainer/setup.sh && .devcontainer/setup.sh", + "forwardPorts": [ + 8080 + ], + "remoteUser": "wordpress" +} diff --git a/.devcontainer/docker-compose.yml b/.devcontainer/docker-compose.yml new file mode 100644 index 0000000000000..ecb45d5462419 --- /dev/null +++ b/.devcontainer/docker-compose.yml @@ -0,0 +1,29 @@ +version: '3.1' + +services: + app: + image: wordpress + restart: always + ports: + - 8080:80 + environment: + WORDPRESS_DB_HOST: db + WORDPRESS_DB_USER: exampleuser + WORDPRESS_DB_PASSWORD: examplepass + WORDPRESS_DB_NAME: exampledb + volumes: + - ..:/workspace:cached + + db: + image: mariadb + restart: unless-stopped + environment: + MYSQL_DATABASE: exampledb + MYSQL_USER: exampleuser + MYSQL_PASSWORD: examplepass + MYSQL_RANDOM_ROOT_PASSWORD: '1' + volumes: + - db:/var/lib/mysql + +volumes: + db: diff --git a/.devcontainer/install-tools.sh b/.devcontainer/install-tools.sh new file mode 100755 index 0000000000000..7a52fabe841c3 --- /dev/null +++ b/.devcontainer/install-tools.sh @@ -0,0 +1,15 @@ +#!/bin/sh + +set -eux + +echo "Installing wp-cli..." +curl -O https://raw.githubusercontent.com/wp-cli/builds/gh-pages/phar/wp-cli.phar +sudo chmod +x wp-cli.phar +sudo mv wp-cli.phar /usr/local/bin/wp + +echo "Installing chromium..." +sudo apt-get update +sudo apt-get -y install --no-install-recommends chromium + +# Copy the welcome message +sudo cp .devcontainer/welcome-message.txt /usr/local/etc/vscode-dev-containers/first-run-notice.txt diff --git a/.devcontainer/setup.sh b/.devcontainer/setup.sh new file mode 100755 index 0000000000000..9480e3f76d27a --- /dev/null +++ b/.devcontainer/setup.sh @@ -0,0 +1,18 @@ +#!/bin/sh + +set -eux + +if [ -z ${CODESPACE_NAME+x} ]; then + SITE_HOST="http://localhost:8080" +else + SITE_HOST="https://${CODESPACE_NAME}-8080.${GITHUB_CODESPACES_PORT_FORWARDING_DOMAIN}" +fi + +# Install dependencies +cd /workspace +npm install && npm run build:dev + +# Install WordPress and activate the plugin/theme. +cd /var/www/html +echo "Setting up WordPress at $SITE_HOST" +wp core install --url="$SITE_HOST" --title="WordPress Trunk" --admin_user="admin" --admin_email="admin@example.com" --admin_password="password" --skip-email diff --git a/.devcontainer/welcome-message.txt b/.devcontainer/welcome-message.txt new file mode 100644 index 0000000000000..cedaa756fa991 --- /dev/null +++ b/.devcontainer/welcome-message.txt @@ -0,0 +1,6 @@ +👋 Welcome to "WordPress Core Development" in Codespaces! + +🛠️ Your environment is fully setup with all the required software. + +🚀 To get started, wait for the "postCreateCommand" to finish setting things up, then open the portforwarded URL and append '/wp-admin'. + diff --git a/.env b/.env deleted file mode 100644 index 9affb93f5321e..0000000000000 --- a/.env +++ /dev/null @@ -1,51 +0,0 @@ -## -# Default configuration options for the local dev environment. -# -# All of these options can be overridden by setting them as environment variables before starting -# the environment. You will need to restart your environment when changing any of these. -# -# Below, the following substitutions can be made: -# - '{version}': any major.minor PHP version from 5.2 onwards. -## - -# The site will be available at http://localhost:LOCAL_PORT -LOCAL_PORT=8889 - -# Where to run WordPress from. Valid options are 'src' and 'build'. -LOCAL_DIR=src - -# The PHP version to use. Valid options are 'latest', and '{version}-fpm'. -LOCAL_PHP=latest - -# Whether or not to enable XDebug. -LOCAL_PHP_XDEBUG=false - -# Whether or not to enable Memcached. -LOCAL_PHP_MEMCACHED=false - -## -# The database software to use. -# -# Supported values are `mysql` and `mariadb`. -## -LOCAL_DB_TYPE=mysql - -## -# The database version to use. -# -# Defaults to 5.7 with the assumption that LOCAL_DB_TYPE is set to `mysql` above. -# -# When using `mysql`, see https://hub.docker.com/_/mysql/ for valid versions. -# When using `mariadb`, see https://hub.docker.com/_/mariadb for valid versions. -## -LOCAL_DB_VERSION=5.7 - -# The debug settings to add to `wp-config.php`. -LOCAL_WP_DEBUG=true -LOCAL_WP_DEBUG_LOG=true -LOCAL_WP_DEBUG_DISPLAY=true -LOCAL_SCRIPT_DEBUG=true -LOCAL_WP_ENVIRONMENT_TYPE=local - -# The URL to use when running e2e tests. -WP_BASE_URL=http://localhost:${LOCAL_PORT} diff --git a/.env.example b/.env.example new file mode 100644 index 0000000000000..76a4744165505 --- /dev/null +++ b/.env.example @@ -0,0 +1,74 @@ +## +# Default configuration options for the local dev environment. +# +# All of these options can be overridden by setting them as environment variables before starting +# the environment. You will need to restart your environment when changing any of these. +# +# Below, the following substitutions can be made: +# - '{version}': any major.minor PHP version from 5.2 onwards. +## + +# The site will be available at http://localhost:LOCAL_PORT +LOCAL_PORT=8889 + +# Where to run WordPress from. Valid options are 'src' and 'build'. +LOCAL_DIR=src + +# The PHP version to use. Valid options are 'latest', and '{version}-fpm'. +LOCAL_PHP=latest + +# Whether or not to enable Xdebug. +LOCAL_PHP_XDEBUG=false + +## +# The Xdebug features to enable. +# +# By default, the following features are enabled in the local environment: +# - Development helpers (`develop`). +# - Step debugging (`debug`). +# +# To generate a code coverage report, `coverage` mode must be active. +# +# For a full list of accepted values, see https://xdebug.org/docs/all_settings#mode. +## +LOCAL_PHP_XDEBUG_MODE=develop,debug + +# Whether or not to enable Memcached. +LOCAL_PHP_MEMCACHED=false + +## +# The database software to use. +# +# Supported values are `mysql` and `mariadb`. +## +LOCAL_DB_TYPE=mysql + +## +# The database version to use. +# +# Defaults to 8.0 with the assumption that LOCAL_DB_TYPE is set to `mysql` above. +# +# When using `mysql`, see https://hub.docker.com/_/mysql for valid versions. +# When using `mariadb`, see https://hub.docker.com/_/mariadb for valid versions. +## +LOCAL_DB_VERSION=8.4 + +# Whether or not to enable multisite. +LOCAL_MULTISITE=false + +# The debug settings to add to `wp-config.php`. +LOCAL_WP_DEBUG=true +LOCAL_WP_DEBUG_LOG=true +LOCAL_WP_DEBUG_DISPLAY=true +LOCAL_SCRIPT_DEBUG=true +LOCAL_WP_ENVIRONMENT_TYPE=local +LOCAL_WP_DEVELOPMENT_MODE=core +LOCAL_WP_TESTS_DOMAIN=example.org + +# The URL to use when running e2e tests. +WP_BASE_URL=http://localhost:${LOCAL_PORT} + +## +# This silences the tips output by the dotenv package. +## +DOTENV_CONFIG_QUIET=true diff --git a/.eslintignore b/.eslintignore index 07f72581a9f4c..9b3144df5c64d 100644 --- a/.eslintignore +++ b/.eslintignore @@ -1,15 +1,26 @@ -# Files and folders related to build/test tools +# Files and folders related to build/test tools including generated files /build /node_modules /tests /vendor /tools +/jsdoc +/artifacts +/coverage +.cache/* +/src/wp-includes/blocks/**/*.js +/src/wp-includes/blocks/**/*.js.map +/src/wp-admin/js +/src/wp-includes/js # Excluded files and folders based on `jsdoc.conf.json` exclusions /src/js/_enqueues/vendor -# Webpack built files -/src/wp-includes/js/media-* - # Themes -src/wp-content/themes/ +src/wp-content/themes + +# Files and folders that get created in wp-content +/src/wp-content/plugins +/src/wp-content/mu-plugins +/src/wp-content/upgrade +/src/wp-content/uploads diff --git a/.git-blame-ignore-revs b/.git-blame-ignore-revs new file mode 100644 index 0000000000000..6070b4d617783 --- /dev/null +++ b/.git-blame-ignore-revs @@ -0,0 +1,74 @@ +# Pinking shears +a4789b3cc1cb59e5cd062670f4439f264c0d34f3 # [12042] +fb4e38b0e750c0cab5eb4c50532da93e2d9882b3 # [15843] +4b33a0e9c45d67c5f3123577262f73727444254a # [16438] +fa33255b567df6b19ccc3a61824ad243db116993 # [16439] +46d96c7704b978b9d070bfec1cb1a33e4caf46b0 # [18254] +feaf2597bf16b1bd6b489a0e5680501f12990bee # [18276] +9e7890c3f61781200b9337186d85cd4034edac61 # [18386] +fd10e500e47ca1ae3280ac6817e725965f7fc0dc # [19054] +6610e321e79d9b56c6fc496b6630dd2ac664a0d3 # [19528] +d26f0a4c407a5d13a095c7676b397e51a959d82c # [19577] +89e9bcc1d69ebbac01c8c4501f9066e421125e66 # [20000] +8c50f982ea9784fe06844ce84b1a3297b1241442 # [20715] +b6e23d7269e50296270f93dda96dacb118bee8b3 # [20944] +48a3ec24c5ea7615431bde431098546674a0096c # [21070] +2439e4d722f5093631c4bac9a45106ab33624aaa # [21381] +56c4577feae624f7a3bf55d001295e0707c972b6 # [21486] +680b671330327f2568549dec9460a2e942219d55 # [21492] +dd39a3b3d2d1f792b6ed8e88b1fcc13877fbd8dd # [22491] +6d8bce688f5215ad20133b2cefedc72ac5136be3 # [22634] +05c0f14024a2bb8ef9bdfcb84589d04205f8ce24 # [23679] +ece7a7714477142d4cdb3269d2f9ad3d5dab2e5f # [23780] +687d1a2ce992a0e7e24675b506b85a57e42fe78e # [24303] +45d2a20783460667f85d5fdced422efd54c33b72 # [24603] +c8889d984fd98381d6f0bd3228972e93348f1266 # [25085] +# 21da24227f2c0087c95f44c60b1bca65cedf0611 # [25824] includes a punctuation change +30f822b8ee767c0b07fe1780f2829de0cba8c314 # [25880] +1cdb0ac2fa5452e58d7a002b260d594a581af02e # [26475] +37a37b5fc578d945376cf3b66b4a23265d6491f3 # [26597] +bda43fd1071ed45ded3def7872866f62e40af9f5 # [26627] +98fe4a5aedfdaa8c769bf2aaedc58eb63d342320 # [26631] +d6e06a2ee5c15b19ca80b9162f3e19c06b5af34d # [26714] +cfd5c395bb3ba810edaec6cf4483212181823d2e # [26851] +2ec5e68249bdacee9ac0ce59fad91b1149244af8 # [27123] +305e72859a4154fc96bac7774ab7808ead6a06d4 # [29169] +ca32a2d410f954b6b40d23f3392ba53e62a3d9f1 # [29707] +48a504cc50a16f60a395ace8d133130e3d6962f5 # [30047] +b539bd985bdf2dde162f46fcbf14e51e46ff8be7 # [30372] +9a3942ffd82b28a77982e0a0d1c2bb0b8a70ce44 # [30996] +469164785ff8defefbfcdf972c57d9004c273256 # [31077] +c4e9c64233166957cd1cbcc54ff54b1a02edd3b5 # [31623] +f4f1b4821342fd1d58708356b3ec39d6fefe31ac # [33411] +0ec540b946eaebebc7e8ee39a8e4738914e7f2a3 # [33627] +0c5bd752629b0960c6f43ca212a64724e4f40346 # [34534] +991feb70438e981290696fa2b4fbe2ea54696a02 # [34774] +6911ff11308089eace23719fc50160d403081a8e # [35627] +8df8cf2df14fe26174f97af5bb17d63f2e867231 # [42843] + +# Coding Standards +8f95800d52c1736d651ae6e259f90ad4a0db2c3f # [42343] + +# 6.8 Coding Standards +a4d6fb7c96cb46859e6d48a5d4c06fdeea7d039b # [59292] +9dd87b8f91300917447c271968d3c36289b440e8 # [59558] +# 903b1fe840d4232bbc249d32d8981824e5fa71de # [59953] includes a punctuation change +a96fa164b00ed51c7c0481574834cff92ab9b1f0 # [60043] +7607cbc5d1e770451f1a2b61d851820dfa23bb43 # [60044] +7047a91c0ecdbf43d4a7a0a591464cd1ed2f2c4b # [60046] +1aa6da693ad739b78752a55d154cd48cb757b90b # [60047] +d44e1c2ce2dc638e89ed6a1d02b1cfadb8a15fe7 # [60048] +a18719e7ea49ab7ac0091e076840cb7efdf51cc5 # [60049] + +# 6.9 Coding Standards +d0d89b62485e724e3d06f01981dd1940b2f36fce # [60074] +c53010159b60735ffa6ba5fa8a416ff0e86a159c # [60109] +cbb6519119276ceba4279eaee73ab66294ebd820 # [60402] +3d736c763e0b1384c65abfa3bcf6d3bc45869516 # [60664] +b96f25f5c31bfd1580c21084c368098792b4c741 # [60780] +ff6c5fadfa6272685d910b474917ecb6adc17f10 # [60808] +08b2f9cfe9064873a501c3543e2c995405431dcc # [60816] +e683403cc1856113e3cb010b1579dcd01cdef5fc # [61036] +8d24041c08a58b2f79504699fb3f63d01737b876 # [61075] +87cbbb1dfcf19fcfc128fc66603462a649d01502 # [61087] +db1b4811e5ab8df343b03032d7607abe01a9e8e2 # [61138] diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index b53da615a7aae..b8bcf4f4eff2c 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -12,11 +12,25 @@ If this is your first time contributing, you may also find reviewing these guide - Inline Documentation Standards: https://make.wordpress.org/core/handbook/best-practices/inline-documentation-standards/ - Browser Support Policies: https://make.wordpress.org/core/handbook/best-practices/browser-support/ - Proper spelling and grammar related best practices: https://make.wordpress.org/core/handbook/best-practices/spelling/ +- ✨ If you are using AI tools, you must adhere to the AI Guidelines: https://make.wordpress.org/ai/handbook/ai-guidelines/ --> Trac ticket: +## Use of AI Tools + + + --- **This Pull Request is for code review only. Please keep all other discussion in the Trac ticket. Do not merge this Pull Request. See [GitHub Pull Requests for Code Review](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/) in the Core Handbook for more details.** diff --git a/.github/workflows/check-built-files.yml b/.github/workflows/check-built-files.yml new file mode 100644 index 0000000000000..01a239c4eb3b0 --- /dev/null +++ b/.github/workflows/check-built-files.yml @@ -0,0 +1,54 @@ +# Checks for uncommitted changes to built files in pull requests. +name: Check Built Files (PRs) + +on: + # Because all commits happen through SVN and should always be manually reviewed by a committer, this workflow only + # runs for pull requests. + # + # Other workflows that run for the push event will detect changes to versioned files and fail. + pull_request: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Any change to a CSS, JavaScript, JSON, or SASS file should run checks. + - '**.css' + - '**.js' + - '**.json' + - '**.sass' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # Confirm any changes to relevant workflow files. + - '.github/workflows/check-built-files.yml' + - '.github/workflows/reusable-check-built-files.yml' + # Changes to the default themes should be handled by the themes workflows. + - '!src/wp-content/themes/twenty**' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + check-for-built-file-changes: + name: Check built files + if: ${{ github.repository == 'wordpress/wordpress-develop' }} + uses: ./.github/workflows/reusable-check-built-files.yml + permissions: + contents: read diff --git a/.github/workflows/cleanup-pull-requests.yml b/.github/workflows/cleanup-pull-requests.yml new file mode 100644 index 0000000000000..578710dcf56ac --- /dev/null +++ b/.github/workflows/cleanup-pull-requests.yml @@ -0,0 +1,28 @@ +name: Cleanup Pull Requests + +on: + push: + branches: + - trunk + - '4.[1-9]' + - '[5-9].[0-9]' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs pull request cleanup. + close-prs: + name: Clean up pull requests + permissions: + pull-requests: write + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + uses: ./.github/workflows/reusable-cleanup-pull-requests.yml diff --git a/.github/workflows/coding-standards.yml b/.github/workflows/coding-standards.yml index 41faf379ee1c8..6f9fc831df92f 100644 --- a/.github/workflows/coding-standards.yml +++ b/.github/workflows/coding-standards.yml @@ -5,16 +5,16 @@ on: # PHPCS checking was introduced in WordPress 5.1. push: branches: - - master - trunk - '3.[89]' - '[4-9].[0-9]' tags: - - '3.[89]*' - - '[4-9].[0-9]*' + - '3.[89]' + - '3.[89].[0-9]+' + - '[4-9].[0-9]' + - '[4-9].[0-9].[0-9]+' pull_request: branches: - - master - trunk - '3.[89]' - '[4-9].[0-9]' @@ -22,16 +22,19 @@ on: # Any change to a PHP or JavaScript file should run checks. - '**.js' - '**.php' - # These files configure NPM. Changes could affect the outcome. + # These files configure npm. Changes could affect the outcome. - 'package*.json' + - '.npmrc' + - '.nvmrc' # These files configure Composer. Changes could affect the outcome. - 'composer.*' # This file configures JSHint. Changes could affect the outcome. - '.jshintrc' # This file configures PHPCS. Changes could affect the outcome. - 'phpcs.xml.dist' - # Changes to workflow files should always verify all workflows are successful. - - '.github/workflows/*.yml' + # Confirm any changes to relevant workflow files. + - '.github/workflows/coding-standards.yml' + - '.github/workflows/reusable-coding-standards-*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -41,121 +44,72 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + jobs: - # Runs PHP coding standards checks. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up PHP. - # - Logs debug information. - # - Installs Composer dependencies (use cache if possible). - # - Make Composer packages available globally. - # - Logs PHP_CodeSniffer debug information. - # - Runs PHPCS on the full codebase with warnings suppressed. - # - Runs PHPCS on the `tests` directory without warnings suppressed. - # - Ensures version-controlled files are not modified or deleted. + # Runs the PHP coding standards checks. phpcs: - name: PHP coding standards - runs-on: ubuntu-latest + name: Coding standards + uses: ./.github/workflows/reusable-coding-standards-php.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Set up PHP - uses: shivammathur/setup-php@afefcaf556d98dc7896cca380e181decb609ca44 # v2.10.0 - with: - php-version: '7.4' - coverage: none - tools: composer, cs2pr - - - name: Log debug information - run: | - php --version - composer --version - - - name: Install Composer dependencies - uses: ramsey/composer-install@92a7904348d4ad30236f3611e33b7f0c6f9edd70 # v1.1.0 - with: - composer-options: "--no-progress --no-ansi --no-interaction" - - - name: Make Composer packages available globally - run: echo "${PWD}/vendor/bin" >> $GITHUB_PATH - - - name: Log PHPCS debug information - run: phpcs -i - - - name: Run PHPCS on all Core files - run: phpcs -q -n --report=checkstyle | cs2pr - - - name: Check test suite files for warnings - run: phpcs tests -q --report=checkstyle | cs2pr - - - name: Ensure version-controlled files are not modified during the tests - run: git diff --exit-code - # Runs the JavaScript coding standards checks. - # - # JSHint violations are not currently reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # - Logs updated debug information. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Run the WordPress JSHint checks. - # - Ensures version-controlled files are not modified or deleted. jshint: - name: JavaScript coding standards - runs-on: ubuntu-latest + name: Coding standards + uses: ./.github/workflows/reusable-coding-standards-javascript.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: ${{ true }} - - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - npm --version - node --version - git --version - svn --version - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - - - name: Install Dependencies - run: npm ci - - - name: Run JSHint - run: npm run grunt jshint - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ phpcs, jshint ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.phpcs.result == 'success' && needs.jshint.result == 'success' && 'success' || ( needs.phpcs.result == 'cancelled' || needs.jshint.result == 'cancelled' ) && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ phpcs, jshint, slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/commit-built-file-changes.yml b/.github/workflows/commit-built-file-changes.yml new file mode 100644 index 0000000000000..b6ba9935ba675 --- /dev/null +++ b/.github/workflows/commit-built-file-changes.yml @@ -0,0 +1,169 @@ +# Commits all missed changes to built files back to pull request branches. +name: Commit Built File Changes (PRs) + +on: + workflow_run: + workflows: + - 'Check Built Files (PRs)' + - 'Test Default Themes & Create ZIPs' + types: + - completed + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'workflow_run' && format( '{0}-{1}', github.event.workflow_run.head_branch, github.event.workflow_run.head_repository.name ) || github.sha }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Checks a PR for uncommitted changes to built files. + # + # Performs the following steps: + # - Attempts to download the artifact containing the PR diff. + # - Checks for the existence of an artifact. + # - Unzips the artifact. + # - Generates a token for authenticating with the GitHub App. + # - Checks out the repository. + # - Applies the patch file. + # - Displays the result of git diff. + # - Configures the Git author. + # - Stages changes. + # - Commits changes. + # - Pushes changes. + update-built-files: + name: Check and update built files + runs-on: ubuntu-24.04 + if: ${{ github.repository == 'wordpress/wordpress-develop' }} + timeout-minutes: 10 + permissions: + contents: write + steps: + - name: Download artifact + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts( { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.RUN_ID, + } ); + + const matchArtifact = artifacts.data.artifacts.filter( ( artifact ) => { + return artifact.name === 'pr-built-file-changes' + } )[0]; + + if ( ! matchArtifact ) { + core.info( 'No artifact found!' ); + return; + } + + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); + + const fs = require( 'fs' ); + fs.writeFileSync( '${{ github.workspace }}/pr-built-file-changes.zip', Buffer.from( download.data ) ) + env: + RUN_ID: ${{ github.event.workflow_run.id }} + + - name: Check for artifact + id: artifact-check + run: | + if [ -f "pr-built-file-changes.zip" ]; then + echo "exists=true" >> "$GITHUB_OUTPUT" + else + echo "exists=false" >> "$GITHUB_OUTPUT" + fi + + - name: Unzip the artifact containing the PR data + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + run: unzip pr-built-file-changes.zip + + - name: Generate Installation Token + id: generate_token + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + env: + GH_APP_ID: ${{ secrets.GH_PR_BUILT_FILES_APP_ID }} + GH_APP_PRIVATE_KEY: ${{ secrets.GH_PR_BUILT_FILES_PRIVATE_KEY }} + run: | + echo "$GH_APP_PRIVATE_KEY" > private-key.pem + + # Generate JWT + JWT=$(python3 - <> "$GITHUB_ENV" + + rm -f private-key.pem + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + with: + repository: ${{ github.event.workflow_run.head_repository.full_name }} + ref: ${{ github.event.workflow_run.head_branch }} + path: 'pr-repo' + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + token: ${{ env.ACCESS_TOKEN }} + persist-credentials: true + + - name: Apply patch + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + run: git apply "$GITHUB_WORKSPACE/changes.diff" + + - name: Display changes to versioned files + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + run: git diff + + - name: Configure git user name and email + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + env: + GH_APP_ID: ${{ secrets.GH_PR_BUILT_FILES_APP_ID }} + run: | + git config user.name "wordpress-develop-pr-bot[bot]" + git config user.email "${GH_APP_ID}+wordpress-develop-pr-bot[bot]@users.noreply.github.com" + + - name: Stage changes + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + run: git add . + + - name: Commit changes + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + run: | + git commit -m "Automation: Updating built files with changes." + + - name: Push changes + if: ${{ steps.artifact-check.outputs.exists == 'true' }} + working-directory: 'pr-repo' + run: git push diff --git a/.github/workflows/end-to-end-tests.yml b/.github/workflows/end-to-end-tests.yml index 6bcaa9a3ff712..4375091546dd7 100644 --- a/.github/workflows/end-to-end-tests.yml +++ b/.github/workflows/end-to-end-tests.yml @@ -1,22 +1,43 @@ name: End-to-end Tests on: - # The end to end test suite was introduced in WordPress 5.3. + # The end-to-end test suite was introduced in WordPress 5.3. push: branches: - - master - trunk - '5.[3-9]' - '[6-9].[0-9]' tags: - - '5.[3-9]*' - - '[6-9].[0-9]*' + - '5.[3-9]' + - '5.[3-9].[0-9]+' + - '[6-9]+.[0-9]' + - '[6-9]+.[0-9].[0-9]+' pull_request: branches: - - master - trunk - '5.[3-9]' - '[6-9].[0-9]' + paths: + # Any change to a PHP, CSS, or JavaScript file should run checks. + - '**.css' + - '**.js' + - '**.php' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # This files affect the e2e tests. Changes could affect the outcome. + - 'tests/e2e/**' + # Confirm any changes to relevant workflow files. + - '.github/workflows/end-to-end-tests.yml' + - '.github/workflows/reusable-end-to-end-tests*.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -26,106 +47,75 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + env: LOCAL_DIR: build + PUPPETEER_SKIP_DOWNLOAD: true jobs: # Runs the end-to-end test suite. - # - # Performs the following steps: - # - Set environment variables. - # - Checks out the repository. - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Builds WordPress to run from the `build` directory. - # - Starts the WordPress Docker container. - # - Logs general debug information. - # - Logs the running Docker containers. - # - Logs Docker debug information (about both the Docker installation within the runner and the WordPress container). - # - Install WordPress within the Docker container. - # - Run the E2E tests. - # - Ensures version-controlled files are not modified or deleted. e2e-tests: - name: E2E Tests - runs-on: ubuntu-latest + name: ${{ matrix.label }} + uses: ./.github/workflows/reusable-end-to-end-tests.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - - steps: - - name: Configure environment variables - run: | - echo "PHP_FPM_UID=$(id -u)" >> $GITHUB_ENV - echo "PHP_FPM_GID=$(id -g)" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - php --version - php -i - locale -a - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Install Dependencies - run: npm ci - - - name: Build WordPress - run: npm run build - - - name: Start Docker environment - run: | - npm run env:start - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - - - name: Log running Docker containers - run: docker ps -a - - - name: Docker debug information - run: | - docker -v - docker-compose -v - docker-compose run --rm mysql mysql --version - docker-compose run --rm php php --version - docker-compose run --rm php php -m - docker-compose run --rm php php -i - docker-compose run --rm php locale -a - - - name: Install WordPress - run: npm run env:install - - - name: Run E2E tests - run: npm run test:e2e - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code + strategy: + fail-fast: false + matrix: + LOCAL_SCRIPT_DEBUG: [ true, false ] + # A matrix value is needed in the 'name' directive for proper grouping in the GitHub UI. + label: [ 'E2E Tests' ] + with: + LOCAL_SCRIPT_DEBUG: ${{ matrix.LOCAL_SCRIPT_DEBUG }} slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ e2e-tests ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.e2e-tests.result == 'success' && 'success' || needs.e2e-tests.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ e2e-tests, slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/failed-workflow.yml b/.github/workflows/failed-workflow.yml new file mode 100644 index 0000000000000..6df8999464a68 --- /dev/null +++ b/.github/workflows/failed-workflow.yml @@ -0,0 +1,56 @@ +## +# Performs follow-up tasks when a workflow fails or is cancelled. +## +name: Failed Workflow + +on: + workflow_dispatch: + inputs: + run_id: + description: 'ID of the GitHub Action workflow run to rerun' + required: true + type: 'string' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Attempts to rerun a workflow. + # + # Performs the following steps: + # - Retrieves the workflow run that dispatched this workflow. + # - Restarts all failed jobs when the workflow fails or is cancelled for the first time. + failed-workflow: + name: Rerun a workflow + runs-on: ubuntu-24.04 + permissions: + actions: write + timeout-minutes: 30 + + steps: + - name: Rerun a workflow + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 15 + retry-exempt-status-codes: 418 + script: | + const workflow_run = await github.rest.actions.getWorkflowRun({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.RUN_ID, + }); + + // Only rerun after the first run attempt. + if ( workflow_run.data.run_attempt > 1 ) { + return; + } + + const rerun = await github.rest.actions.reRunWorkflowFailedJobs({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.RUN_ID, + enable_debug_logging: true + }); + env: + RUN_ID: ${{ inputs.run_id }} diff --git a/.github/workflows/install-testing.yml b/.github/workflows/install-testing.yml new file mode 100644 index 0000000000000..8da6a84f1caeb --- /dev/null +++ b/.github/workflows/install-testing.yml @@ -0,0 +1,186 @@ +# Confirms that installing WordPress using WP-CLI works successfully. +# +# This workflow is not meant to test wordpress-develop checkouts, but rather tagged versions officially available on WordPress.org. +# +# This workflow is triggered for all WordPress versions that are currently receiving security updates. It therefore needs to +# retain support for older PHP and database versions. +name: Installation Tests + +on: + push: + branches: + - trunk + # Always test the workflow after it's updated. + paths: + - '.github/workflows/install-testing.yml' + - '.version-support-*.json' + - '.github/workflows/reusable-support-json-reader-v1.yml' + pull_request: + # Always test the workflow when changes are suggested. + paths: + - '.version-support-*.json' + - '.github/workflows/install-testing.yml' + - '.github/workflows/reusable-support-json-reader-v1.yml' + + schedule: + - cron: '0 0 * * 1' + workflow_dispatch: + inputs: + wp-version: + description: 'The version to test installing. Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' + type: string + default: 'nightly' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ inputs.wp-version || github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Determines the supported values for PHP and database versions based on the WordPress version being tested. + build-test-matrix: + name: Build Test Matrix + uses: ./.github/workflows/reusable-support-json-reader-v1.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + with: + wp-version: ${{ inputs.wp-version }} + + # Test the WordPress installation process through WP-CLI. + # + # Performs the following steps: + # - Sets up PHP. + # - Downloads the specified version of WordPress. + # - Creates a `wp-config.php` file. + # - Installs WordPress. + install-tests-mysql: + name: WP ${{ inputs.wp-version || 'nightly' }} / PHP ${{ matrix.php }} / ${{ 'mariadb' == matrix.db-type && 'MariaDB' || 'MySQL' }} ${{ matrix.db-version }}${{ matrix.multisite && ' multisite' || '' }} + permissions: + contents: read + runs-on: ${{ matrix.os }} + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + timeout-minutes: 10 + needs: [ build-test-matrix ] + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04 ] + php: ${{ fromJSON( needs.build-test-matrix.outputs.php-versions ) }} + db-type: [ 'mysql' ] + db-version: ${{ fromJSON( needs.build-test-matrix.outputs.mysql-versions ) }} + multisite: [ false, true ] + memcached: [ false ] + + # Exclude some PHP and MySQL versions that cannot currently be tested with Docker containers. + exclude: + # There are no local WordPress Docker environment containers for PHP <= 5.3. + - php: '5.2' + - php: '5.3' + # MySQL containers <= 5.5 do not exist or fail to start properly. + - db-version: '5.0' + - db-version: '5.1' + - db-version: '5.5' + # Only test the latest innovation release. + - db-version: '9.0' + - db-version: '9.1' + - db-version: '9.2' + - db-version: '9.3' + - db-version: '9.4' + - db-version: '9.5' + # MySQL 9.0+ will not work on PHP 7.2 & 7.3. See https://core.trac.wordpress.org/ticket/61218. + - php: '7.2' + db-version: '9.6' + - php: '7.3' + db-version: '9.6' + + services: + database: + image: ${{ matrix.db-type }}:${{ matrix.db-version }} + ports: + - 3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval="30s" + --health-timeout="10s" + --health-retries="5" + -e MYSQL_ROOT_PASSWORD="root" + -e MYSQL_DATABASE="test_db" + --entrypoint sh ${{ matrix.db-type }}:${{ matrix.db-version }} + -c "exec docker-entrypoint.sh mysqld${{ matrix.db-type == 'mysql' && contains( fromJSON('["5.4", "5.5", "5.6", "7.0", "7.1", "7.2", "7.3"]'), matrix.php ) && ( matrix.db-version == '8.4' && ' --mysql-native-password=ON --authentication-policy=mysql_native_password' || ' --default-authentication-plugin=mysql_native_password' ) || '' }}" + + steps: + - name: Set up PHP ${{ matrix.php }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: '${{ matrix.php }}' + coverage: none + tools: ${{ contains( fromJSON('["5.4", "5.5"]'), matrix.php ) && 'wp-cli:2.4.0' || 'wp-cli' }} + + - name: Download WordPress + run: wp core download --version="${WP_VERSION}" + env: + WP_VERSION: ${{ inputs.wp-version || 'nightly' }} + + - name: Create wp-config.php file + run: wp config create --dbname=test_db --dbuser=root --dbpass=root --dbhost="127.0.0.1:${DB_PORT}" + env: + DB_PORT: ${{ job.services.database.ports['3306'] }} + + - name: Install WordPress + run: wp core ${{ matrix.multisite && 'multisite-install' || 'install' }} --url=http://localhost/ --title="Upgrade Test" --admin_user=admin --admin_password=password --admin_email=me@example.org --skip-email + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ install-tests-mysql ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/javascript-tests.yml b/.github/workflows/javascript-tests.yml index 4c0c6e17e14ef..4ebb1fd17b499 100644 --- a/.github/workflows/javascript-tests.yml +++ b/.github/workflows/javascript-tests.yml @@ -4,32 +4,40 @@ on: # JavaScript testing was introduced in WordPress 3.8. push: branches: - - master - trunk - '3.[89]' - '[4-9].[0-9]' tags: - - '3.[89]*' - - '[4-9].[0-9]*' + - '3.[89]' + - '3.[89].[0-9]+' + - '[4-9].[0-9]' + - '[4-9].[0-9].[0-9]+' pull_request: branches: - - master - trunk - '3.[89]' - '[4-9].[0-9]' paths: # Any change to a JavaScript file should run tests. - '**.js' - # These files configure NPM. Changes could affect the outcome. + # These files configure npm and the task runner. Changes could affect the outcome. - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' # This file configures ESLint. Changes could affect the outcome. - '.eslintignore' # This file configures JSHint. Changes could affect the outcome. - '.jshintrc' # Any change to the QUnit directory should run tests. - 'tests/qunit/**' - # Changes to workflow files should always verify all workflows are successful. - - '.github/workflows/*.yml' + # Confirm any changes to relevant workflow files. + - '.github/workflows/javascript-tests.yml' + - '.github/workflows/reusable-javascript-tests.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -39,62 +47,64 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + jobs: - # Runs the QUnit tests for WordPress. - # - # Performs the following steps: - # - Checks out the repository. - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # - Logs updated debug information. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Run the WordPress QUnit tests. - # - Ensures version-controlled files are not modified or deleted. + # Runs the WordPress Core JavaScript tests. test-js: name: QUnit Tests - runs-on: ubuntu-latest + uses: ./.github/workflows/reusable-javascript-tests.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - npm --version - node --version - git --version - svn --version - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Log debug information - run: | - npm --version - node --version - - - name: Install Dependencies - run: npm ci - - - name: Run QUnit tests - run: npm run grunt qunit:compiled - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code - slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ test-js ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.test-js.result == 'success' && 'success' || needs.test-js.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/javascript-type-checking.yml b/.github/workflows/javascript-type-checking.yml new file mode 100644 index 0000000000000..b8a10da5465bd --- /dev/null +++ b/.github/workflows/javascript-type-checking.yml @@ -0,0 +1,101 @@ +name: JavaScript Type Checking + +on: + # JavaScript type checking was introduced in 7.0.0. + push: + branches: + - trunk + - '[7-9].[0-9]' + tags: + - '[7-9].[0-9]' + - '[7-9]+.[0-9].[0-9]+' + pull_request: + branches: + - trunk + - '[7-9].[0-9]' + paths: + # This workflow only scans JavaScript files. + - '**.js' + - '**.ts' + - '**.tsx' + # These files configure npm. Changes could affect the outcome. + - 'package*.json' + - '.nvmrc' + - '.npmrc' + # This file configures TypeScript. Changes could affect the outcome. + - 'tsconfig.json' + # This directory contains TypeScript definitions. Changes could affect the outcome. + - 'typings/**' + # Confirm any changes to relevant workflow files. + - '.github/workflows/javascript-type-checking.yml' + - '.github/workflows/reusable-javascript-type-checking-v1.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs JavaScript type checking. + typecheck: + name: JavaScript type checking + uses: ./.github/workflows/reusable-javascript-type-checking-v1.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ typecheck ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/local-docker-environment.yml b/.github/workflows/local-docker-environment.yml new file mode 100644 index 0000000000000..d42bba623ec64 --- /dev/null +++ b/.github/workflows/local-docker-environment.yml @@ -0,0 +1,169 @@ +name: Local Docker Environment + +on: + # Local Docker environment testing was introduced in WordPress 6.8. + push: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Any changes to Docker related files. + - '.env.example' + - 'docker-compose.yml' + # Any changes to local environment related files + - 'tools/local-env/**' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + - '.npmrc' + - '.nvmrc' + # These files configure Composer. Changes could affect the local environment. + - 'composer.*' + # These files define the versions to test. + - '.version-support-*.json' + # Changes to this and related workflow files should always be verified. + - '.github/workflows/local-docker-environment.yml' + - '.github/workflows/reusable-support-json-reader-v1.yml' + - '.github/workflows/reusable-test-docker-environment-v1.yml' + pull_request: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Any changes to Docker related files. + - '.env.example' + - 'docker-compose.yml' + # Any changes to local environment related files + - 'tools/local-env/**' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/webpack/**' + - '.npmrc' + - '.nvmrc' + # These files configure Composer. Changes could affect the local environment. + - 'composer.*' + # These files define the versions to test. + - '.version-support-*.json' + # Changes to this and related workflow files should always be verified. + - '.github/workflows/local-docker-environment.yml' + - '.github/workflows/reusable-support-json-reader-v1.yml' + - '.github/workflows/reusable-test-docker-environment-v1.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # + # Determines the appropriate supported values for PHP and database versions based on the WordPress + # version being tested. + # + build-test-matrix: + name: Build Test Matrix + uses: ./.github/workflows/reusable-support-json-reader-v1.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + with: + wp-version: ${{ github.event_name == 'pull_request' && github.base_ref || github.ref_name }} + + # Tests the local Docker environment. + environment-tests-mysql: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-test-local-docker-environment-v1.yml + permissions: + contents: read + needs: [ build-test-matrix ] + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04 ] + memcached: [ false, true ] + php: ${{ fromJSON( needs.build-test-matrix.outputs.php-versions ) }} + db-version: ${{ fromJSON( needs.build-test-matrix.outputs.mysql-versions ) }} + + exclude: + # MySQL containers <= 5.5 do not exist or fail to start properly. + - db-version: '5.5' + # Only test the latest innovation release. + - db-version: '9.0' + - db-version: '9.1' + - db-version: '9.2' + - db-version: '9.3' + - db-version: '9.4' + - db-version: '9.5' + # No PHP 8.5 + Memcached support yet. + - php: '8.5' + memcached: true + + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: 'mysql' + db-version: ${{ matrix.db-version }} + memcached: ${{ matrix.memcached }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ build-test-matrix, environment-tests-mysql ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ build-test-matrix, environment-tests-mysql, slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/performance.yml b/.github/workflows/performance.yml new file mode 100644 index 0000000000000..d9be2c8842ec4 --- /dev/null +++ b/.github/workflows/performance.yml @@ -0,0 +1,184 @@ +name: Performance Tests + +on: + # Performance testing was introduced in WordPress 6.2. + push: + branches: + - trunk + - '6.[2-9]' + - '[7-9].[0-9]' + tags: + - '6.[2-9]' + - '6.[2-9].[0-9]+' + - '[7-9].[0-9]' + - '[7-9].[0-9].[0-9]+' + pull_request: + branches: + - trunk + - '6.[2-9]' + - '[7-9].[0-9]' + paths: + # Any change to a PHP, CSS, or JavaScript file should run checks. + - '**.css' + - '**.js' + - '**.php' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # This files affect the performance tests. Changes could affect the outcome. + - 'tests/performance/**' + # Confirm any changes to relevant workflow files. + - '.github/workflows/performance.yml' + - '.github/workflows/reusable-performance.yml' + - '.github/workflows/reusable-performance-*.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + determine-matrix: + name: Determine Matrix + runs-on: ubuntu-24.04 + if: ${{ ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) && ! contains( github.event.before, '00000000' ) }} + permissions: + actions: read + env: + TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + outputs: + subjects: ${{ steps.set-subjects.outputs.result }} + target_sha: ${{ env.TARGET_SHA }} + steps: + # The `workflow_dispatch` event is the only one missing the needed SHA to target. + - name: Retrieve previous commit SHA (if necessary) + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "TARGET_SHA=$(git rev-parse HEAD^1)" >> "$GITHUB_ENV" + + - name: Set subjects + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: set-subjects + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + const has_previous_build = !! artifacts.data.artifacts[0]; + + const subjects = [ + 'current', + ]; + + if ( context.eventName === 'push' && context.ref === 'refs/heads/trunk' ) { + subjects.push( 'base' ); + } else if ( has_previous_build ) { + subjects.push( 'before' ); + } + + return subjects; + + # Runs the performance test suite. + performance: + name: ${{ matrix.multisite && 'Multisite' || 'Single Site' }} ${{ matrix.memcached && 'Memcached' || 'Default' }} + uses: ./.github/workflows/reusable-performance-test-v2.yml + needs: [ determine-matrix ] + permissions: + contents: read + strategy: + fail-fast: false + matrix: + memcached: [ false, true ] + multisite: [ true, false ] + subject: ${{ fromJson( needs.determine-matrix.outputs.subjects ) }} + with: + memcached: ${{ matrix.memcached }} + multisite: ${{ matrix.multisite }} + subject: ${{ matrix.subject }} + TARGET_SHA: ${{ needs.determine-matrix.outputs.target_sha }} + + compare: + name: ${{ matrix.label }} + uses: ./.github/workflows/reusable-performance-report-v2.yml + needs: [ determine-matrix, performance ] + permissions: + contents: read + strategy: + fail-fast: false + matrix: + memcached: [ false, true ] + multisite: [ true, false ] + # A matrix value is needed in the 'name' directive for proper grouping in the GitHub UI. + label: [ Compare ] + with: + memcached: ${{ matrix.memcached }} + multisite: ${{ matrix.multisite }} + BASE_TAG: ${{ needs.performance.outputs.BASE_TAG }} + publish: ${{ contains( fromJson( needs.determine-matrix.outputs.subjects ), 'base' ) && ! matrix.memcached && ! matrix.multisite }} + secrets: + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ performance ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/php-compatibility.yml b/.github/workflows/php-compatibility.yml index de89b79a3de6c..bd81c8958daa6 100644 --- a/.github/workflows/php-compatibility.yml +++ b/.github/workflows/php-compatibility.yml @@ -4,16 +4,16 @@ on: # PHP compatibility testing was introduced in WordPress 5.5. push: branches: - - master - trunk - '5.[5-9]' - '[6-9].[0-9]' tags: - - '5.[5-9]*' - - '[6-9].[0-9]*' + - '5.[5-9]' + - '5.[5-9].[0-9]+' + - '[6-9].[0-9]' + - '[6-9].[0-9].[0-9]+' pull_request: branches: - - master - trunk - '5.[5-9]' - '[6-9].[0-9]' @@ -22,10 +22,11 @@ on: - '**.php' # These files configure Composer. Changes could affect the outcome. - 'composer.*' - # This file configures PHP Compatibility scanning. Changes could affect the outcome. + # This file configures PHP compatibility scanning. Changes could affect the outcome. - 'phpcompat.xml.dist' - # Changes to workflow files should always verify all workflows are successful. - - '.github/workflows/*.yml' + # Confirm any changes to relevant workflow files. + - '.github/workflows/php-compatibility.yml' + - '.github/workflows/reusable-php-compatibility.yml' workflow_dispatch: # Cancels all previous workflow runs for pull requests that have not completed. @@ -35,68 +36,64 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true -jobs: +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} +jobs: # Runs PHP compatibility testing. - # - # Violations are reported inline with annotations. - # - # Performs the following steps: - # - Checks out the repository. - # - Sets up PHP. - # - Logs debug information about the runner container. - # - Installs Composer dependencies (use cache if possible). - # - Make Composer packages available globally. - # - Logs PHP_CodeSniffer debug information. - # - Runs the PHP compatibility tests. - # - Ensures version-controlled files are not modified or deleted. php-compatibility: name: Check PHP compatibility - runs-on: ubuntu-latest + uses: ./.github/workflows/reusable-php-compatibility.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Set up PHP - uses: shivammathur/setup-php@afefcaf556d98dc7896cca380e181decb609ca44 # v2.10.0 - with: - php-version: '7.4' - coverage: none - tools: composer, cs2pr - - - name: Log debug information - run: | - php --version - composer --version - - - name: Install Composer dependencies - uses: ramsey/composer-install@92a7904348d4ad30236f3611e33b7f0c6f9edd70 # v1.1.0 - with: - composer-options: "--no-progress --no-ansi --no-interaction" - - - name: Make Composer packages available globally - run: echo "${PWD}/vendor/bin" >> $GITHUB_PATH - - - name: Log PHPCS debug information - run: phpcs -i - - - name: Run PHP compatibility tests - run: phpcs --standard=phpcompat.xml.dist -q --report=checkstyle | cs2pr - - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code - slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ php-compatibility ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.php-compatibility.result == 'success' && 'success' || needs.php-compatibility.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/phpstan-static-analysis.yml b/.github/workflows/phpstan-static-analysis.yml new file mode 100644 index 0000000000000..a479e8e371214 --- /dev/null +++ b/.github/workflows/phpstan-static-analysis.yml @@ -0,0 +1,97 @@ +name: PHPStan Static Analysis + +on: + # PHPStan testing was introduced in 7.0.0. + push: + branches: + - trunk + - '[7-9].[0-9]' + tags: + - '[7-9].[0-9]' + - '[7-9]+.[0-9].[0-9]+' + pull_request: + branches: + - trunk + - '[7-9].[0-9]' + paths: + # This workflow only scans PHP files. + - '**.php' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # These files configure PHPStan. Changes could affect the outcome. + - 'phpstan.neon.dist' + - 'tests/phpstan/base.neon' + - 'tests/phpstan/baseline.php' + # Confirm any changes to relevant workflow files. + - '.github/workflows/phpstan-static-analysis.yml' + - '.github/workflows/reusable-phpstan-static-analysis-v1.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs PHPStan Static Analysis. + phpstan: + name: PHP static analysis + uses: ./.github/workflows/reusable-phpstan-static-analysis-v1.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ phpstan ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/phpunit-tests.yml b/.github/workflows/phpunit-tests.yml index b6ce31b99bba6..74dfc220c04a6 100644 --- a/.github/workflows/phpunit-tests.yml +++ b/.github/workflows/phpunit-tests.yml @@ -3,19 +3,40 @@ name: PHPUnit Tests on: push: branches: - - master - trunk - '3.[7-9]' - '[4-9].[0-9]' tags: - - '3.[7-9]*' - - '[4-9].[0-9]*' + - '[0-9]+.[0-9]' + - '[0-9]+.[0-9].[0-9]+' pull_request: branches: - - master - trunk - '3.[7-9]' - '[4-9].[0-9]' + paths: + # Any change to a PHP, CSS, JavaScript, JSON, HTML, or otherwise tested file should run checks. + - '**.css' + - '**.html' + - '**.js' + - '**.json' + - '**.php' + - 'src/license.txt' + - 'src/SECURITY.md' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # These files affect the phpunit tests. Changes could affect the outcome. + - 'tests/phpunit/**' + - 'tests/phpunit/multisite.xml' + - 'phpunit.xml.dist' + # Confirm any changes to relevant workflow files. + - '.github/workflows/phpunit-tests.yml' + - '.github/workflows/reusable-phpunit-tests-*.yml' workflow_dispatch: # Once weekly On Sundays at 00:00 UTC. schedule: @@ -28,230 +49,325 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} cancel-in-progress: true -env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: ${{ true }} - LOCAL_PHP_MEMCACHED: ${{ false }} - SLOW_TESTS: 'external-http,media,restapi' +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} jobs: - # Runs the PHPUnit tests for WordPress. # - # Note: Steps running tests for PHP 8.1 jobs are allowed to "continue-on-error". - # This prevents workflow runs from being marked as "failed" when only PHP 8.1 fails. + # Creates a PHPUnit test job for each PHP/MySQL combination. # - # Performs the following steps: - # - Set environment variables. - # - Sets up the environment variables needed for testing with memcached (if desired). - # - Installs NodeJS 14. - # - Installs NPM dependencies - # - Configures caching for Composer. - # - Installs Composer dependencies (if desired). - # - Logs Docker debug information (about both the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Starts the memcached server after the Docker network has been created (if desired). - # - Logs WordPress Docker container debug information. - # - Logs debug general information. - # - Logs the running Docker containers. - # - Logs debug information about what's installed within the WordPress Docker containers. - # - Install WordPress within the Docker container. - # - Run the PHPUnit tests. - # - Ensures version-controlled files are not modified or deleted. - # - Checks out the WordPress Test reporter repository. - # - Reconnect the directory to the Git repository. - # - Submit the test results to the WordPress.org host test results. - test-php: - name: ${{ matrix.php }}${{ matrix.multisite && ' multisite' || '' }}${{ matrix.split_slow && ' slow tests' || '' }}${{ matrix.memcached && ' with memcached' || '' }} on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + # Though MySQL 5.5 and 5.6 are still supported by WordPress, they are not currently tested here because the Docker + # images do not work. Testing against MariaDB 5.5 provides a reasonable level of MySQL 5.5 testing (see MariaDB matrix + # below for more details). + # + test-with-mysql: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} + if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} strategy: fail-fast: false matrix: - php: [ '5.6', '7.0', '7.1', '7.2', '7.3', '7.4', '8.0', '8.1' ] - os: [ ubuntu-latest ] - memcached: [ false ] - split_slow: [ false ] + os: [ ubuntu-24.04 ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.0', '8.4' ] + tests-domain: [ 'example.org' ] multisite: [ false, true ] + memcached: [ false ] + include: - # Additional "slow" jobs for PHP 5.6. - - php: '5.6' - os: ubuntu-latest - memcached: false + # Include jobs that test with memcached. + - os: ubuntu-24.04 + php: '8.3' + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org' multisite: false - split_slow: true - - php: '5.6' - os: ubuntu-latest - memcached: false + memcached: true + - os: ubuntu-24.04 + php: '8.3' + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org' multisite: true - split_slow: true - # Include jobs for PHP 7.4 with memcached. - - php: '7.4' - os: ubuntu-latest memcached: true + # Include jobs with a port on the test domain for both single and multisite. + - os: ubuntu-24.04 + php: '8.4' + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org:8889' multisite: false - - php: '7.4' - os: ubuntu-latest - memcached: true + memcached: false + - os: ubuntu-24.04 + php: '8.4' + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org:8889' multisite: true - # Report the results of the PHP 7.4 without memcached job. - - php: '7.4' - os: ubuntu-latest memcached: false + # Report test results to the Host Test Results. + - os: ubuntu-24.04 + db-type: 'mysql' + db-version: '8.4' + tests-domain: 'example.org' multisite: false + memcached: false report: true + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + multisite: ${{ matrix.multisite }} + memcached: ${{ matrix.memcached }} + phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} + tests-domain: ${{ matrix.tests-domain }} + report: ${{ matrix.report || false }} - env: - LOCAL_PHP: ${{ matrix.php }}-fpm - LOCAL_PHP_MEMCACHED: ${{ matrix.memcached }} - PHPUNIT_CONFIG: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} - - steps: - - name: Configure environment variables - run: | - echo "PHP_FPM_UID=$(id -u)" >> $GITHUB_ENV - echo "PHP_FPM_GID=$(id -g)" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Install Dependencies - run: npm ci - - # This date is used to ensure that the Composer cache is refreshed at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "::set-output name=date::$(/bin/date -u --date='last Mon' "+%F")" - - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache Composer dependencies - uses: actions/cache@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v2.1.4 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php-${{ matrix.php }}-date-${{ steps.get-date.outputs.date }}-composer-${{ hashFiles('**/composer.json') }} - - - name: Install Composer dependencies - run: | - docker-compose run --rm php composer --version - - # Install using `composer update` as there is no `composer.lock` file. - if [ ${{ env.LOCAL_PHP }} == '8.1-fpm' ]; then - docker-compose run --rm php composer update --ignore-platform-reqs - else - docker-compose run --rm php composer update - fi - - - name: Docker debug information - run: | - docker -v - docker-compose -v - - - name: Start Docker environment - run: | - npm run env:start - - # The memcached server needs to start after the Docker network has been set up with `npm run env:start`. - - name: Start the Memcached server. - if: ${{ matrix.memcached }} - run: | - cp tests/phpunit/includes/object-cache.php src/wp-content/object-cache.php - docker run --name memcached --net $(basename "$PWD")_wpdevnet -d memcached - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - docker-compose run --rm mysql mysql --version - docker-compose run --rm php php --version - docker-compose run --rm php php -m - docker-compose run --rm php php -i - docker-compose run --rm php locale -a - - - name: Install WordPress - run: npm run env:install - - - name: Run slow PHPUnit tests - if: ${{ matrix.split_slow }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --group ${{ env.SLOW_TESTS }} - - - name: Run PHPUnit tests for single site excluding slow tests - if: ${{ matrix.php < '7.0' && ! matrix.split_slow && ! matrix.multisite }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --exclude-group ${{ env.SLOW_TESTS }},ajax,ms-files,ms-required - - - name: Run PHPUnit tests for Multisite excluding slow tests - if: ${{ matrix.php < '7.0' && ! matrix.split_slow && matrix.multisite }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --exclude-group ${{ env.SLOW_TESTS }},ajax,ms-files,ms-excluded,oembed-headers - - - name: Run PHPUnit tests - if: ${{ matrix.php >= '7.0' }} - continue-on-error: ${{ matrix.php == '8.1' }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} - - - name: Run AJAX tests - if: ${{ ! matrix.split_slow }} - continue-on-error: ${{ matrix.php == '8.1' }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c ${{ env.PHPUNIT_CONFIG }} --group ajax + # + # Creates a PHPUnit test job for each PHP/MariaDB combination. + # + # All LTS versions of MariaDB supported by WordPress with greater than 1% usage according to w.org/stats should be + # tested. The exceptions to this rule are the most recent LTS and version 5.5. + # + # The 5.5 release was intended as a drop-in replacement for MySQL. Because the MySQL 5.5 Docker containers do not + # work, this ensures some level of MySQL 5.5 testing. + # + test-with-mariadb: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} + if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04 ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + db-type: [ 'mariadb' ] + db-version: [ '5.5', '10.3', '10.5', '10.6', '10.11', '11.4', '11.8' ] + multisite: [ false, true ] + memcached: [ false ] - - name: Run ms-files tests as a multisite install - if: ${{ matrix.multisite && ! matrix.split_slow }} - continue-on-error: ${{ matrix.php == '8.1' }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c tests/phpunit/multisite.xml --group ms-files + include: + # Include jobs that test with memcached. + - os: ubuntu-24.04 + php: '8.3' + db-type: 'mariadb' + db-version: '11.8' + multisite: false + memcached: true + - os: ubuntu-24.04 + php: '8.3' + db-type: 'mariadb' + db-version: '11.8' + multisite: true + memcached: true + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + multisite: ${{ matrix.multisite }} + memcached: ${{ matrix.memcached }} + phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} + report: false - - name: Run external HTTP tests - if: ${{ ! matrix.multisite && ! matrix.split_slow }} - continue-on-error: ${{ matrix.php == '8.1' }} - run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c phpunit.xml.dist --group external-http + # + # Creates PHPUnit test jobs to test MariaDB and MySQL innovation releases. + # + # Though innovation releases are deemed "production grade", they never receive LTS status. However, they include new + # features and updates that will be included in the next LTS version. + # + # Because upstream support for innovation releases is dropped when a new one is released (including security updates), + # only the most recent innovation version is tested. + # + # MariaDB does not currently have a supported innovation release. + # + test-innovation-releases: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} + if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} + strategy: + fail-fast: false + matrix: + os: [ ubuntu-24.04 ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + db-type: [ 'mysql', 'mariadb' ] + db-version: [ '9.6', '12.1' ] + multisite: [ false, true ] + memcached: [ false ] + db-innovation: [ true ] + + exclude: + # Exclude version combinations that don't exist. + - db-type: 'mariadb' + db-version: '9.6' + - db-type: 'mysql' + db-version: '12.1' + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + db-innovation: ${{ matrix.db-innovation }} + multisite: ${{ matrix.multisite }} + memcached: ${{ matrix.memcached }} + phpunit-config: ${{ matrix.multisite && 'tests/phpunit/multisite.xml' || 'phpunit.xml.dist' }} + report: false - # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. - - name: Run (xDebug) tests - if: ${{ ! matrix.split_slow }} - continue-on-error: ${{ matrix.php == '8.1' }} - run: LOCAL_PHP_XDEBUG=true node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit -v --group xdebug --exclude-group __fakegroup__ + # + # Runs the HTML API test group. + # + # This test group runs separately due to the large number of tests that are skipped in this group while the + # HTML API is being developed. The skipped tests would otherwise cloud the results of all other test groups. + # + # These tests are run against the most recent LTS version of MySQL. + # + html-api-test-groups: + name: ${{ matrix.label }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} + if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} + strategy: + fail-fast: false + matrix: + php: [ '7.4', '8.0', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '8.4' ] + phpunit-test-groups: [ 'html-api-html5lib-tests' ] + # A matrix value is needed in the 'name' directive for proper grouping in the GitHub UI. + label: [ 'HTML API' ] + with: + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + phpunit-test-groups: ${{ matrix.phpunit-test-groups }} - - name: Ensure version-controlled files are not modified or deleted - run: git diff --exit-code + # + # Runs unit tests for forks. + # + # Because the majority of forks will belong to personal GitHub accounts (which are limited to just 20 concurrent jobs + # at any given time), forks only run a small subset of test combinations. This allows contributors to open pull + # requests back to their own forks for testing purposes without having to wait hours for workflow to complete. + # + limited-matrix-for-forks: + name: PHP ${{ matrix.php }} + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} + WPT_REPORT_API_KEY: ${{ secrets.WPT_REPORT_API_KEY }} + if: ${{ ! startsWith( github.repository, 'WordPress/' ) && github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + php: [ '7.4', '8.4' ] + db-version: [ '8.4', '11.8' ] + db-type: [ 'mysql', 'mariadb' ] + multisite: [ false ] - - name: Checkout the WordPress Test Reporter - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.ref == 'refs/heads/master' && matrix.report }} - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - with: - repository: 'WordPress/phpunit-test-runner' - path: 'test-runner' + include: + # Include one multisite job for each database type. + - php: '8.4' + db-version: '8.4' + db-type: 'mysql' + multisite: true + - php: '8.4' + db-version: '11.8' + db-type: 'mariadb' + multisite: true + # Test with memcached. + - php: '8.4' + db-version: '8.4' + db-type: 'mysql' + multisite: true + memcached: true + # Run specific test groups once. + - php: '8.4' + db-version: '8.4' + db-type: 'mysql' + phpunit-test-groups: 'html-api-html5lib-tests' + + exclude: + # Exclude PHP versions that are not supported by the database versions. + - db-type: 'mysql' + db-version: '11.8' + - db-type: 'mariadb' + db-version: '8.4' - - name: Submit test results to the WordPress.org host test results - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.ref == 'refs/heads/master' && matrix.report }} - env: - WPT_REPORT_API_KEY: "${{ secrets.WPT_REPORT_API_KEY }}" - run: docker-compose run --rm -e WPT_REPORT_API_KEY -e WPT_PREPARE_DIR=/var/www -e WPT_TEST_DIR=/var/www php php test-runner/report.php + with: + php: ${{ matrix.php }} + db-version: ${{ matrix.db-version }} + db-type: ${{ matrix.db-type }} + memcached: ${{ matrix.memcached || false }} + phpunit-test-groups: ${{ matrix.phpunit-test-groups || '' }} slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master - needs: [ test-php ] + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ test-with-mysql, test-with-mariadb, test-innovation-releases, html-api-test-groups, limited-matrix-for-forks ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.test-php.result == 'success' && 'success' || needs.test-php.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/props-bot.yml b/.github/workflows/props-bot.yml new file mode 100644 index 0000000000000..a8656eb5ce1a9 --- /dev/null +++ b/.github/workflows/props-bot.yml @@ -0,0 +1,92 @@ +name: Props Bot + +on: + # This event runs anytime a PR is (re)opened, updated, marked ready for review, or labeled. + # GitHub does not allow filtering the `labeled` event by a specific label. + # However, the logic below will short-circuit the workflow when the `props-bot` label is not the one being added. + # Note: The pull_request_target event is used instead of pull_request because this workflow needs permission to comment + # on the pull request. Because this event grants extra permissions to `GITHUB_TOKEN`, any code changes within the PR + # should be considered untrusted. See https://securitylab.github.com/research/github-actions-preventing-pwn-requests/. + pull_request_target: + types: + - opened + - synchronize + - reopened + - labeled + - ready_for_review + # This event runs anytime a comment is added or deleted. + # You cannot filter this event for PR comments only. + # However, the logic below does short-circuit the workflow for issues. + issue_comment: + types: + - created + # This event will run everytime a new PR review is initially submitted. + pull_request_review: + types: + - submitted + # This event runs anytime a PR review comment is created or deleted. + pull_request_review_comment: + types: + - created + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ contains( fromJSON( '["pull_request_target", "pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Compiles a list of props for a pull request. + # + # Performs the following steps: + # - Collects a list of contributor props and leaves a comment. + # - Removes the props-bot label, if necessary. + props-bot: + name: Generate a list of props + runs-on: ubuntu-24.04 + permissions: + # The action needs permission `write` permission for PRs in order to add a comment. + pull-requests: write + contents: read + timeout-minutes: 20 + # The job will run when pull requests are open, ready for review and: + # + # - A comment is added to the pull request. + # - A review is created or commented on (unless PR originates from a fork). + # - The pull request is opened, synchronized, marked ready for review, or reopened. + # - The `props-bot` label is added to the pull request. + if: | + ( + github.event_name == 'issue_comment' && github.event.issue.pull_request || + ( contains( fromJSON( '["pull_request_review", "pull_request_review_comment"]' ), github.event_name ) && ! github.event.pull_request.head.repo.fork ) || + github.event_name == 'pull_request_target' && github.event.action != 'labeled' || + 'props-bot' == github.event.label.name + ) && + ( ! github.event.pull_request.draft && github.event.pull_request.state == 'open' || ! github.event.issue.draft && github.event.issue.state == 'open' ) + + steps: + - name: Gather a list of contributors + uses: WordPress/props-bot-action@trunk + with: + format: 'svn' + + - name: Remove the props-bot label + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + if: ${{ github.event.action == 'labeled' && 'props-bot' == github.event.label.name }} + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.issues.removeLabel({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: process.env.ISSUE_NUMBER, + name: 'props-bot' + }); + env: + ISSUE_NUMBER: ${{ github.event.number }} diff --git a/.github/workflows/pull-request-comments.yml b/.github/workflows/pull-request-comments.yml new file mode 100644 index 0000000000000..da30e2feb7f11 --- /dev/null +++ b/.github/workflows/pull-request-comments.yml @@ -0,0 +1,230 @@ +# Responsible for making comments on pull requests, such as commenting for first time contributors. +name: Pull Request Comments + +on: + pull_request_target: + types: [ 'opened', 'synchronize', 'reopened', 'edited' ] + workflow_run: + workflows: [ 'Test Build Processes' ] + types: + - completed + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.event_name == 'workflow_dispatch' && github.event.number || github.sha }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Comments on a pull request when the author is a first time contributor. + post-welcome-message: + runs-on: ubuntu-24.04 + permissions: + issues: write + pull-requests: write + timeout-minutes: 5 + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name == 'pull_request_target' }} + steps: + - name: Post a welcome comment + uses: wow-actions/welcome@68019c2c271561f63162fea75bb7707ef8a02c85 # v1.3.1 + with: + FIRST_PR_REACTIONS: 'hooray' + FIRST_PR_COMMENT: > + Hi @{{ author }}! 👋 + + + Thank you for your contribution to WordPress! 💖 + + + It looks like this is your first pull request to `wordpress-develop`. Here are a few things to be aware of that may help you out! + + + **No one monitors this repository for new pull requests.** Pull requests **must** be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. + + + **Pull requests are never merged on GitHub.** The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. + + + More information about how GitHub pull requests can be used to contribute to WordPress can be found in [the Core Handbook](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/). + + + **Please include automated tests.** Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the [Automated Testing](https://make.wordpress.org/core/handbook/testing/automated-testing/) page in the handbook. + + + If you have not had a chance, please review the [Contribute with Code page](https://make.wordpress.org/core/handbook/contribute/) in the [WordPress Core Handbook](https://make.wordpress.org/core/handbook/). + + + The [Developer Hub](https://developer.wordpress.org/) also documents the various [coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/) that are followed: + + - [PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) + + - [CSS Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/css/) + + - [HTML Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/html/) + + - [JavaScript Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/) + + - [Accessibility Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/accessibility/) + + - [Inline Documentation Standards](https://developer.wordpress.org/coding-standards/inline-documentation-standards/) + + + Thank you, + + The WordPress Project + + # Leaves a comment on a pull request with a link to test the changes in a WordPress Playground instance. + playground-details: + name: Comment on a pull request with Playground details + runs-on: ubuntu-24.04 + permissions: + issues: write + pull-requests: write + if: > + github.repository == 'WordPress/wordpress-develop' && + github.event.workflow_run.event == 'pull_request' && + github.event.workflow_run.conclusion == 'success' + steps: + - name: Download artifact + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const artifacts = await github.rest.actions.listWorkflowRunArtifacts( { + owner: context.repo.owner, + repo: context.repo.repo, + run_id: process.env.RUN_ID, + } ); + + const matchArtifact = artifacts.data.artifacts.filter( ( artifact ) => { + return artifact.name === 'pr-number' + } )[0]; + + if ( ! matchArtifact ) { + core.setFailed( 'No artifact found!' ); + return; + } + + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); + + const fs = require( 'fs' ); + fs.writeFileSync( '${{github.workspace}}/pr-number.zip', Buffer.from( download.data ) ) + env: + RUN_ID: ${{ github.event.workflow_run.id }} + + - name: Unzip the artifact containing the PR number + run: unzip pr-number.zip + + - name: Leave a comment about testing with Playground + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const fs = require( 'fs' ); + const issue_number = Number( fs.readFileSync( './NR' ) ); + + core.info( `Checking pull request #${issue_number}.` ); + + // Confirm that the pull request is still open before leaving a comment. + const pr = await github.rest.pulls.get({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: issue_number, + }); + + if ( pr.data.state !== 'open' ) { + core.info( 'The pull request has been closed. No comment will be left.' ); + return; + } + + // Comments are only added after the first successful build. Check for the presence of a comment and bail early. + const commentInfo = { + owner: context.repo.owner, + repo: context.repo.repo, + issue_number, + }; + + const comments = ( await github.rest.issues.listComments( commentInfo ) ).data; + + for ( const currentComment of comments ) { + if ( currentComment.user.type === 'Bot' && currentComment.body.includes( 'Test using WordPress Playground' ) ) { + core.info( 'A comment with instructions to test within a Playground instance already exists.' ); + return; + } + }; + + // No comment was found. Create one. + commentInfo.body = `## Test using WordPress Playground + The changes in this pull request can previewed and tested using a [WordPress Playground](https://developer.wordpress.org/playground/) instance. + + [WordPress Playground](https://developer.wordpress.org/playground/) is an experimental project that creates a full WordPress instance entirely within the browser. + + ### Some things to be aware of + - All changes will be lost when closing a tab with a Playground instance. + - All changes will be lost when refreshing the page. + - A fresh instance is created each time the link below is clicked. + - Every time this pull request is updated, a new ZIP file containing all changes is created. If changes are not reflected in the Playground instance, + it's possible that the most recent build failed, or has not completed. Check the [list of workflow runs to be sure](https://github.com/WordPress/wordpress-develop/actions/workflows/test-build-processes.yml). + + For more details about these limitations and more, check out the [Limitations page](https://wordpress.github.io/wordpress-playground/limitations/) in the WordPress Playground documentation. + + [Test this pull request with WordPress Playground](https://playground.wordpress.net/wordpress.html?pr=${ issue_number }). + `; + + github.rest.issues.createComment( commentInfo ); + + # Manages comments reminding contributors to include a Trac ticket link when opening a pull request. + trac-ticket-check: + name: Manage Trac ticket reminders for pull requests + runs-on: ubuntu-24.04 + permissions: + issues: write + pull-requests: write + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name == 'pull_request_target' && ! github.event.pull_request.draft && github.event.pull_request.state == 'open' }} + steps: + - name: Check for Trac ticket and manage comment + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + script: | + const { owner, repo } = context.repo; + const { number } = context.issue; + + // Check for the presence of a comment and bail early. + const comments = ( await github.rest.issues.listComments( { owner, repo, issue_number: number } ) ).data; + + const hasMissingTicketComment = comments.find( comment => comment.user.type === 'Bot' && comment.body.includes( 'Trac Ticket Missing' ) ); + + if ( hasMissingTicketComment ) { + // Trac ticket link found, delete existing "Trac Ticket Missing" comment. + await github.rest.issues.deleteComment( { owner, repo, comment_id: hasMissingTicketComment.id } ); + return; + } + + // No comment was found. Create one. + const pr = ( await github.rest.pulls.get( { owner, repo, pull_number: number } ) ).data; + + const prBody = pr.body ?? ''; + const prTitle = pr.title ?? ''; + + const tracTicketRegex = new RegExp( '(https?://core.trac.wordpress.org/ticket/|Core-|ticket:)([0-9]+)', 'g' ); + const tracTicketMatches = prBody.match( tracTicketRegex ) || prTitle.match( tracTicketRegex ); + + if ( ! tracTicketMatches ) { + github.rest.issues.createComment( { + owner, + repo, + issue_number: number, + body: `## Trac Ticket Missing + This pull request is missing a link to a [Trac ticket](https://core.trac.wordpress.org/). For a contribution to be considered, there must be a corresponding ticket in Trac. + + To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. More information about contributing to WordPress on GitHub can be found in [the Core Handbook](https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/). + `, + } ); + } diff --git a/.github/workflows/reusable-build-package.yml b/.github/workflows/reusable-build-package.yml new file mode 100644 index 0000000000000..320ec1c621335 --- /dev/null +++ b/.github/workflows/reusable-build-package.yml @@ -0,0 +1,60 @@ +## +# A reusable workflow that builds and packages WordPress. The resulting package can be used to test upgrading and installing. +## +name: Build and package WordPress + +on: + workflow_call: + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Builds and packages WordPress. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Runs the build script. + # - Prepares the directory structure for the ZIP. + # - Creates a ZIP of the built files. + # - Uploads the ZIP as a GitHub Actions artifact. + build: + name: WordPress + permissions: + contents: read + runs-on: ubuntu-24.04 + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Install npm Dependencies + run: npm ci + + - name: Build WordPress + run: npm run build + + - name: Prepare the directory structure for the ZIP + run: mv build wordpress + + - name: Create ZIP of built files + run: zip -q -r develop.zip wordpress/. + + - name: Upload ZIP as a GitHub Actions artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: wordpress-develop + path: develop.zip + if-no-files-found: error diff --git a/.github/workflows/reusable-check-built-files.yml b/.github/workflows/reusable-check-built-files.yml new file mode 100644 index 0000000000000..290161c485324 --- /dev/null +++ b/.github/workflows/reusable-check-built-files.yml @@ -0,0 +1,111 @@ +## +# A reusable workflow that checks for uncommitted changes to built files in pull requests. +## +name: Check Built Files (PRs) + +on: + workflow_call: + +permissions: {} + +jobs: + # Checks a PR for uncommitted changes to built files. + # + # When changes are detected, the patch is stored as an artifact for processing by the Commit Built File Changes + # workflow. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Configures caching for Composer. + # - Installs Composer dependencies. + # - Logs general debug information about the runner. + # - Installs npm dependencies. + # - Builds CSS file using SASS. + # - Builds Emoji files. + # - Builds bundled Root Certificate files. + # - Builds WordPress. + # - Checks for changes to versioned files. + # - Displays the result of git diff for debugging purposes. + # - Saves the diff to a patch file. + # - Uploads the patch file as an artifact. + update-built-files: + name: Check and update built files + runs-on: ubuntu-24.04 + timeout-minutes: 10 + permissions: + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + # This date is used to ensure that the PHPCS cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + + - name: Install npm Dependencies + run: npm ci + + - name: Run SASS precommit tasks + run: npm run grunt precommit:css + + - name: Run Emoji precommit task + run: npm run grunt precommit:emoji + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run certificate tasks + run: npm run grunt copy:certificates + + - name: Build WordPress + run: npm run build:dev + + - name: Check for changes to versioned files + id: built-file-check + run: | + if git diff --quiet; then + echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT" + else + echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT" + fi + + - name: Display changes to versioned files + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + run: git diff + + - name: Save diff to a file + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + run: git diff > ./changes.diff + + # Uploads the diff file as an artifact. + - name: Upload diff file as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + with: + name: pr-built-file-changes + path: changes.diff diff --git a/.github/workflows/reusable-cleanup-pull-requests.yml b/.github/workflows/reusable-cleanup-pull-requests.yml new file mode 100644 index 0000000000000..cdce56001d16b --- /dev/null +++ b/.github/workflows/reusable-cleanup-pull-requests.yml @@ -0,0 +1,117 @@ +## +# A reusable workflow that finds and closes any pull requests that are linked to Trac +# tickets that are referenced as fixed in commit messages. +# +# More info about using GitHub pull requests for contributing to WordPress can be found in the handbook: https://make.wordpress.org/core/handbook/contribute/git/github-pull-requests-for-code-review/. +## +name: Run pull request cleanup + +on: + workflow_call: + +jobs: + # Finds and closes pull requests referencing fixed Trac tickets in commit messages using the + # documented expected format + # + # Commit message format is documented in the Core handbook: https://make.wordpress.org/core/handbook/best-practices/commit-messages/. + # + # Performs the following steps: + # - Parse fixed ticket numbers from the commit message. + # - Parse the SVN revision from the commit message. + # - Searches for pull requests referencing any fixed tickets. +# - Comments on pull requests referencing any fixed tickets before closing. + close-prs: + name: Find and close PRs + runs-on: ubuntu-24.04 + permissions: + pull-requests: write + + steps: + - name: Find fixed ticket numbers + id: trac-tickets + env: + COMMIT_MSG_RAW: ${{ github.event.head_commit.message }} + run: | + COMMIT_MESSAGE="$(echo "$COMMIT_MSG_RAW" | sed -n '/^Fixes #/,/\./p')" + echo "fixed_list=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*Fixes #\([0-9]\+\).*/\1/p' | tr '\n' ' ')" >> "$GITHUB_OUTPUT" + + - name: Get the SVN revision + id: git-svn-id + env: + COMMIT_MSG_RAW: ${{ github.event.head_commit.message }} + run: | + COMMIT_MESSAGE="$(echo "$COMMIT_MSG_RAW" | sed -n '$p')" + echo "svn_revision_number=$(echo "$COMMIT_MESSAGE" | sed -n 's/.*git-svn-id: https:\/\/develop.svn.wordpress.org\/[^@]*@\([0-9]*\) .*/\1/p')" >> "$GITHUB_OUTPUT" + + - name: Find, comment on, and close pull requests + if: ${{ steps.trac-tickets.outputs.fixed_list != '' && steps.git-svn-id.outputs.svn_revision_number != '' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + env: + FIXED_LIST: ${{ steps.trac-tickets.outputs.fixed_list }} + SVN_REVISION_NUMBER: ${{ steps.git-svn-id.outputs.svn_revision_number }} + with: + script: | + const fixedList = process.env.FIXED_LIST.split(' ').filter(Boolean); + const svnRevisionNumber = process.env.SVN_REVISION_NUMBER; + const githubSha = process.env.GITHUB_SHA; + let prNumbers = []; + + for (const ticket of fixedList) { + const tracTicketUrl = `https://core.trac.wordpress.org/ticket/${ticket}`; + const corePrefix = `core-${ticket}`; + + const query = ` + query($searchQuery: String!) { + search(query: $searchQuery, type: ISSUE_ADVANCED, first: 20) { + nodes { + ... on PullRequest { + number + bodyText + } + } + } + } + `; + + const searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:pr is:open ( "${tracTicketUrl}" OR "${corePrefix}" )`; + + const result = await github.graphql(query, { + searchQuery, + }); + + // Since search queries will match anywhere for any activity on a pull request, the body specifically needs to be manually checked. + const matchingPRs = result.search.nodes + .filter(pr => { + const bodyLower = pr.bodyText.toLowerCase(); + + return bodyLower.includes(tracTicketUrl.toLowerCase()) || bodyLower.includes(corePrefix.toLowerCase()); + }).map(pr => pr.number); + + prNumbers.push(...matchingPRs); + } + + const commentBody = `A commit was made that fixes the Trac ticket referenced in the description of this pull request. + + SVN changeset: [${svnRevisionNumber}](https://core.trac.wordpress.org/changeset/${svnRevisionNumber}) + GitHub commit: https://github.com/WordPress/wordpress-develop/commit/${githubSha} + + This PR will be closed, but please confirm the accuracy of this and reopen if there is more work to be done.`; + + // Update all matched pull requests. + for (const prNumber of prNumbers) { + // Comment on the pull request with details. + await github.rest.issues.createComment({ + owner: context.repo.owner, + repo: context.repo.repo, + issue_number: prNumber, + body: commentBody + }); + + // Close the pull request. + await github.rest.pulls.update({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: prNumber, + state: 'closed' + }); + } diff --git a/.github/workflows/reusable-coding-standards-javascript.yml b/.github/workflows/reusable-coding-standards-javascript.yml new file mode 100644 index 0000000000000..6d776aabf1e27 --- /dev/null +++ b/.github/workflows/reusable-coding-standards-javascript.yml @@ -0,0 +1,61 @@ +## +# A reusable workflow that checks the JavaScript coding standards. +## +name: JavaScript coding standards + +on: + workflow_call: + +env: + PUPPETEER_SKIP_DOWNLOAD: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the JavaScript coding standards checks. + # + # JSHint violations are not currently reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Logs debug information about the GitHub Action runner. + # - Installs npm dependencies. + # - Run the WordPress JSHint checks. + # - Ensures version-controlled files are not modified or deleted. + jshint: + name: JavaScript checks + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + git --version + + - name: Install npm Dependencies + run: npm ci + + - name: Run JSHint + run: npm run grunt jshint + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-coding-standards-php.yml b/.github/workflows/reusable-coding-standards-php.yml new file mode 100644 index 0000000000000..99683e0850d64 --- /dev/null +++ b/.github/workflows/reusable-coding-standards-php.yml @@ -0,0 +1,108 @@ +## +# A reusable workflow that checks the PHP coding standards. +## +name: PHP coding standards + +on: + workflow_call: + inputs: + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + old-branch: + description: 'Whether this is an old branch that runs phpcbf instead of phpcs' + required: false + type: 'boolean' + default: false + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the PHP coding standards checks. + # + # Violations are reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up PHP. + # - Configures caching for PHPCS scans. + # - Installs Composer dependencies. + # - Make Composer packages available globally. + # - Runs PHPCS on the full codebase (warnings excluded). + # - Generate a report for displaying issues as pull request annotations. + # - Runs PHPCS on the `tests` directory without (warnings included). + # - Generate a report for displaying `test` directory issues as pull request annotations. + # - Ensures version-controlled files are not modified or deleted. + phpcs: + name: PHP checks + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: ${{ inputs.php-version }} + coverage: none + tools: cs2pr + + # This date is used to ensure that the PHPCS cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + - name: Cache PHPCS scan cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + .cache/phpcs-src.json + .cache/phpcs-tests.json + key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-php-${{ inputs.php-version }}-phpcs-cache-${{ hashFiles('**/composer.json', 'phpcs.xml.dist') }} + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Make Composer packages available globally + run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" + + - name: Run PHPCS on all Core files + id: phpcs-core + if: ${{ ! inputs.old-branch }} + run: phpcs -n --report-full --cache=./.cache/phpcs-src.json --report-checkstyle=./.cache/phpcs-report.xml + + - name: Show PHPCS results in PR + if: ${{ always() && steps.phpcs-core.outcome == 'failure' }} + run: cs2pr ./.cache/phpcs-report.xml + + - name: Check test suite files for warnings + id: phpcs-tests + if: ${{ ! inputs.old-branch }} + run: phpcs tests --report-full --cache=./.cache/phpcs-tests.json --report-checkstyle=./.cache/phpcs-tests-report.xml + + - name: Show test suite scan results in PR + if: ${{ always() && steps.phpcs-tests.outcome == 'failure' }} + run: cs2pr ./.cache/phpcs-tests-report.xml + + - name: Run PHPCBF on all Core files (old branches) + if: ${{ inputs.old-branch }} + run: phpcbf + + - name: Ensure version-controlled files are not modified during the tests + run: git diff --exit-code diff --git a/.github/workflows/reusable-end-to-end-tests.yml b/.github/workflows/reusable-end-to-end-tests.yml new file mode 100644 index 0000000000000..0b2ceec077602 --- /dev/null +++ b/.github/workflows/reusable-end-to-end-tests.yml @@ -0,0 +1,157 @@ +## +# A reusable workflow that runs end-to-end tests. +# +# Branches 6.3 and earlier used Puppeteer instead of Playwright. +# Use https://github.com/WordPress/wordpress-develop/tree/6.3/.github/workflows/reusable-end-to-end-tests.yml instead. +## +name: End-to-end Tests + +on: + workflow_call: + inputs: + LOCAL_SCRIPT_DEBUG: + description: 'Whether to enable script debugging.' + required: false + type: 'boolean' + default: false + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + install-gutenberg: + description: 'Whether to install the Gutenberg plugin.' + required: false + type: 'boolean' + default: true + gutenberg-version: + description: 'A specific version of Gutenberg to install.' + required: false + type: 'string' + install-playwright: + description: 'Whether to install Playwright browsers.' + required: false + type: 'boolean' + default: true + +env: + LOCAL_DIR: build + LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the end-to-end test suite. + # + # Performs the following steps: + # - Sets environment variables. + # - Checks out the repository. + # - Sets up Node.js. + # - Logs debug information about the GitHub Action runner. + # - Installs npm dependencies. + # - Install Playwright browsers. + # - Builds WordPress to run from the `build` directory. + # - Starts the WordPress Docker container. + # - Logs the running Docker containers. + # - Logs Docker debug information (about both the Docker installation within the runner and the WordPress container). + # - Install WordPress within the Docker container. + # - Install Gutenberg. + # - Install additional languages. + # - Run the E2E tests. + # - Uploads screenshots and HTML snapshots as an artifact. + # - Ensures version-controlled files are not modified or deleted. + e2e-tests: + name: SCRIPT_DEBUG ${{ inputs.LOCAL_SCRIPT_DEBUG && 'enabled' || 'disabled' }} + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + locale -a + + - name: Install npm Dependencies + run: npm ci + + - name: Install Playwright browsers + if: ${{ inputs.install-playwright }} + run: npx playwright install --with-deps chromium + + - name: Build WordPress + run: npm run build + + - name: Start Docker environment + run: | + npm run env:start + + - name: Log running Docker containers + run: docker ps -a + + - name: Docker debug information + run: | + docker -v + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + env: + LOCAL_SCRIPT_DEBUG: ${{ inputs.LOCAL_SCRIPT_DEBUG }} + run: npm run env:install + + - name: Install Gutenberg + if: ${{ inputs.install-gutenberg }} + run: | + npm run env:cli -- plugin install gutenberg \ + ${{ inputs.gutenberg-version && '--version="${GUTENBERG_VERSION}"' || '' }} \ + --path="/var/www/${LOCAL_DIR}" + env: + GUTENBERG_VERSION: ${{ inputs.gutenberg-version }} + + - name: Install additional languages + run: | + npm run env:cli -- language core install de_DE --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language plugin install de_DE --all --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language theme install de_DE --all --path="/var/www/${LOCAL_DIR}" + + - name: Run E2E tests + run: npm run test:e2e + + - name: Archive debug artifacts (screenshots, HTML snapshots) + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: always() + with: + name: failures-artifacts${{ inputs.LOCAL_SCRIPT_DEBUG && '-SCRIPT_DEBUG' || '' }}-${{ github.run_id }} + path: artifacts + if-no-files-found: ignore + include-hidden-files: true + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-javascript-tests.yml b/.github/workflows/reusable-javascript-tests.yml new file mode 100644 index 0000000000000..6bab6a5287665 --- /dev/null +++ b/.github/workflows/reusable-javascript-tests.yml @@ -0,0 +1,71 @@ +## +# A reusable workflow that runs JavaScript tests. +## +name: JavaScript tests + +on: + workflow_call: + inputs: + disable-apparmor: + description: 'Whether to disable AppArmor.' + required: false + type: 'boolean' + default: false + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the QUnit test suite. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Logs debug information about the GitHub Action runner. + # - Installs npm dependencies. + # - Run the WordPress QUnit tests. + # - Ensures version-controlled files are not modified or deleted. + test-js: + name: Run QUnit tests + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + git --version + + - name: Install npm Dependencies + run: npm ci + + # Older branches using outdated versions of Puppeteer fail on newer versions of the `ubuntu-24` image. + # This disables AppArmor in order to work around those failures. + # + # See https://issues.chromium.org/issues/373753919 + # and https://chromium.googlesource.com/chromium/src/+/main/docs/security/apparmor-userns-restrictions.md + - name: Disable AppArmor + if: ${{ inputs.disable-apparmor }} + run: echo 0 | sudo tee /proc/sys/kernel/apparmor_restrict_unprivileged_userns + + - name: Run QUnit tests + run: npm run grunt qunit:compiled + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-javascript-type-checking-v1.yml b/.github/workflows/reusable-javascript-type-checking-v1.yml new file mode 100644 index 0000000000000..7eab9346f2147 --- /dev/null +++ b/.github/workflows/reusable-javascript-type-checking-v1.yml @@ -0,0 +1,76 @@ +## +# A reusable workflow that runs JavaScript Type Checking. +## +name: JavaScript Type Checking + +on: + workflow_call: + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs JavaScript type checking. + # + # Violations are reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Logs debug information. + # - Installs npm dependencies. + # - Configures caching for TypeScript build info. + # - Runs JavaScript type checking. + # - Saves the TypeScript build info. + # - Ensures version-controlled files are not modified or deleted. + typecheck: + name: Run JavaScript type checking + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 10 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + + - name: Install npm dependencies + run: npm ci --ignore-scripts + + - name: Cache TypeScript build info + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: | + *.tsbuildinfo + key: "ts-build-info-${{ github.run_id }}" + restore-keys: | + ts-build-info- + + - name: Run JavaScript type checking + run: npm run typecheck:js + + - name: "Save result cache" + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + if: ${{ !cancelled() }} + with: + path: | + *.tsbuildinfo + key: "ts-build-info-${{ github.run_id }}" + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-performance-report-v2.yml b/.github/workflows/reusable-performance-report-v2.yml new file mode 100644 index 0000000000000..1b158bb6813ae --- /dev/null +++ b/.github/workflows/reusable-performance-report-v2.yml @@ -0,0 +1,114 @@ +## +# A reusable workflow that compares and publishes the performance tests. +## +name: Compare and publish performance Tests + +on: + workflow_call: + inputs: + BASE_TAG: + description: 'The version being used for baseline measurements.' + required: true + type: string + memcached: + description: 'Whether to enable memcached.' + required: false + type: boolean + default: false + multisite: + description: 'Whether to use Multisite.' + required: false + type: boolean + default: false + publish: + description: 'Whether to publish the results to Code Vitals.' + required: false + type: boolean + default: false + secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for https://www.codevitals.run/project/wordpress.' + required: false + +env: + BASE_TAG: ${{ inputs.BASE_TAG }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Performs the following steps: + # - Checkout repository. + # - Set up Node.js. + # - Download the relevant performance test artifacts. + # - List the downloaded files for debugging. + # - Compare results. + # - Add workflow summary. + # - Determine the sha of the baseline tag if necessary. + # - Publish performance results if necessary. + compare: + name: ${{ inputs.multisite && 'Multisite' || 'Single Site' }} ${{ inputs.memcached && 'Memcached' || 'Default' }} ${{ inputs.publish && '(Publishes)' || '' }} + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Download artifacts + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + pattern: performance-${{ inputs.multisite && 'multisite' || 'single' }}-${{ inputs.memcached && 'memcached' || 'default' }}-* + path: artifacts + merge-multiple: true + + - name: List files + run: tree artifacts + + - name: Compare results + run: node ./tests/performance/compare-results.js "${RUNNER_TEMP}/summary.md" + + - name: Add workflow summary + run: cat "${RUNNER_TEMP}/summary.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Set the base sha + # Only needed when publishing results. + if: ${{ inputs.publish }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: base-sha + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + result-encoding: string + script: | + const baseRef = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/' + process.env.BASE_TAG, + }); + return baseRef.data.object.sha; + + - name: Publish performance results + if: ${{ inputs.publish }} + env: + BASE_SHA: ${{ steps.base-sha.outputs.result }} + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + HOST_NAME: codevitals.run + run: | + if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then + echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" + exit 1 + fi + COMMITTED_AT="$(git show -s "$GITHUB_SHA" --format='%cI')" + node ./tests/performance/log-results.js "$CODEVITALS_PROJECT_TOKEN" trunk "$GITHUB_SHA" "$BASE_SHA" "$COMMITTED_AT" "$HOST_NAME" diff --git a/.github/workflows/reusable-performance-test-v2.yml b/.github/workflows/reusable-performance-test-v2.yml new file mode 100644 index 0000000000000..691e79c39508a --- /dev/null +++ b/.github/workflows/reusable-performance-test-v2.yml @@ -0,0 +1,267 @@ +## +# A reusable workflow that runs the performance test suite. +## +name: Run performance Tests + +on: + workflow_call: + inputs: + subject: + description: Subject of the test. One of `current`, `before`, or `base`. + required: true + type: string + LOCAL_DIR: + description: 'Where to run WordPress from.' + required: false + type: 'string' + default: 'build' + BASE_TAG: + description: 'The version being used for baseline measurements.' + required: false + type: 'string' + default: '6.7.0' + TARGET_SHA: + description: 'SHA hash of the target commit used for "before" measurements.' + required: true + type: 'string' + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + memcached: + description: 'Whether to enable memcached.' + required: false + type: 'boolean' + default: false + multisite: + description: 'Whether to use Multisite.' + required: false + type: 'boolean' + default: false + outputs: + BASE_TAG: + description: 'The version being used for baseline measurements.' + value: ${{ inputs.BASE_TAG }} + secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for https://www.codevitals.run/project/wordpress.' + required: false + +env: + PUPPETEER_SKIP_DOWNLOAD: true + + # Prevent wp-scripts from downloading extra Playwright browsers, + # since Chromium will be installed in its dedicated step already. + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + + # Performance testing should be performed in an environment reflecting a standard production environment. + LOCAL_WP_DEBUG: false + LOCAL_SCRIPT_DEBUG: false + LOCAL_SAVEQUERIES: false + LOCAL_WP_DEVELOPMENT_MODE: "''" + + BASE_TAG: ${{ inputs.BASE_TAG }} + TARGET_SHA: ${{ inputs.TARGET_SHA }} + + LOCAL_DIR: ${{ inputs.LOCAL_DIR }} + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} + LOCAL_MULTISITE: ${{ inputs.multisite }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Performs the following steps: + # - Configure environment variables. + # - Checkout repository. + # - Set up Node.js. + # - Log debug information. + # - Install npm dependencies. + # - Install Playwright browsers. + # - Build WordPress. + # - Start Docker environment. + # - Put the baseline or target version of WordPress in place if necessary. + # - Install object cache drop-in. + # - Log running Docker containers. + # - Docker debug information. + # - Install WordPress. + # - WordPress debug information. + # - Enable themes on Multisite. + # - Install WordPress Importer plugin. + # - Import mock data. + # - Deactivate WordPress Importer plugin. + # - Update permalink structure. + # - Install additional languages. + # - Disable external HTTP requests. + # - Disable cron. + # - List defined constants. + # - Install MU plugin. + # - Run performance tests. + # - Archive artifacts. + # - Ensure version-controlled files are not modified or deleted. + performance: + name: Test ${{ inputs.subject == 'base' && inputs.BASE_TAG || inputs.subject }} + runs-on: ubuntu-24.04 + permissions: + contents: read + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + locale -a + + - name: Install npm dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Start Docker environment + run: npm run env:start + + - name: Build WordPress + run: npm run build + + - name: Download previous build artifact (target branch or previous commit) + if: ${{ inputs.subject == 'before' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: get-previous-build + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + const matchArtifact = artifacts.data.artifacts[0]; + if ( ! matchArtifact ) { + core.setFailed( 'No artifact found!' ); + return false; + } + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); + const fs = require( 'fs' ); + fs.writeFileSync( process.env.GITHUB_WORKSPACE + '/before.zip', Buffer.from( download.data ) ) + return true; + + - name: Unzip the previous build + if: ${{ inputs.subject == 'before' }} + run: | + unzip "${GITHUB_WORKSPACE}/before.zip" + unzip -o "${GITHUB_WORKSPACE}/wordpress.zip" + + - name: Set the environment to the baseline version + if: ${{ inputs.subject == 'base' }} + run: | + VERSION="${BASE_TAG%.0}" + npm run env:cli -- core download --version="$VERSION" --force --path="/var/www/${LOCAL_DIR}" + + - name: Install object cache drop-in + if: ${{ inputs.memcached }} + run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php + + - name: Log running Docker containers + run: docker ps -a + + - name: Docker debug information + run: | + docker -v + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Check version number + run: npm run env:cli -- core version --path="/var/www/${LOCAL_DIR}" + + - name: Enable themes on Multisite + if: ${{ inputs.multisite }} + run: | + npm run env:cli -- theme enable twentytwentyone --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentythree --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfour --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfive --network --path="/var/www/${LOCAL_DIR}" + + - name: Install WordPress Importer plugin + run: npm run env:cli -- plugin install wordpress-importer --activate --path="/var/www/${LOCAL_DIR}" + + - name: Import mock data + run: | + curl -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml + npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" + rm themeunittestdata.wordpress.xml + + - name: Deactivate WordPress Importer plugin + run: npm run env:cli -- plugin deactivate wordpress-importer --path="/var/www/${LOCAL_DIR}" + + - name: Install additional languages + run: | + npm run env:cli -- language core install de_DE --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language plugin install de_DE --all --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language theme install de_DE --all --path="/var/www/${LOCAL_DIR}" + + # Prevent background update checks from impacting test stability. + - name: Disable external HTTP requests + run: npm run env:cli -- config set WP_HTTP_BLOCK_EXTERNAL true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + # Prevent background tasks from impacting test stability. + - name: Disable cron + run: npm run env:cli -- config set DISABLE_WP_CRON true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + - name: List defined constants + run: npm run env:cli -- config list --path="/var/www/${LOCAL_DIR}" + + - name: Install MU plugin + run: | + mkdir "./${LOCAL_DIR}/wp-content/mu-plugins" + cp ./tests/performance/wp-content/mu-plugins/server-timing.php "./${LOCAL_DIR}/wp-content/mu-plugins/server-timing.php" + + - name: Run performance tests + run: npm run test:performance + env: + TEST_RESULTS_PREFIX: ${{ inputs.subject != 'current' && inputs.subject || '' }} + + - name: Archive artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: always() + with: + name: performance-${{ inputs.multisite && 'multisite' || 'single' }}-${{ inputs.memcached && 'memcached' || 'default' }}-${{ inputs.subject }} + path: artifacts + if-no-files-found: error + include-hidden-files: true + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-performance.yml b/.github/workflows/reusable-performance.yml new file mode 100644 index 0000000000000..3ebe31ff8e38f --- /dev/null +++ b/.github/workflows/reusable-performance.yml @@ -0,0 +1,357 @@ +## +# A reusable workflow that runs the performance test suite. +## +name: Run performance Tests + +on: + workflow_call: + inputs: + LOCAL_DIR: + description: 'Where to run WordPress from.' + required: false + type: 'string' + default: 'build' + BASE_TAG: + description: 'The version being used for baseline measurements.' + required: false + type: 'string' + default: '6.7.0' + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + memcached: + description: 'Whether to enable memcached.' + required: false + type: 'boolean' + default: false + multisite: + description: 'Whether to use Multisite.' + required: false + type: 'boolean' + default: false + secrets: + CODEVITALS_PROJECT_TOKEN: + description: 'The authorization token for https://www.codevitals.run/project/wordpress.' + required: false + +env: + PUPPETEER_SKIP_DOWNLOAD: true + + # Prevent wp-scripts from downloading extra Playwright browsers, + # since Chromium will be installed in its dedicated step already. + PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: true + + # Performance testing should be performed in an environment reflecting a standard production environment. + LOCAL_WP_DEBUG: false + LOCAL_SCRIPT_DEBUG: false + LOCAL_SAVEQUERIES: false + LOCAL_WP_DEVELOPMENT_MODE: "''" + + # This workflow takes two sets of measurements — one for the current commit, + # and another against a consistent version that is used as a baseline measurement. + # This is done to isolate variance in measurements caused by the GitHub runners + # from differences caused by code changes between commits. The BASE_TAG value here + # represents the version being used for baseline measurements. It should only be + # changed if we want to normalize results against a different baseline. + BASE_TAG: ${{ inputs.BASE_TAG }} + LOCAL_DIR: ${{ inputs.LOCAL_DIR }} + TARGET_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || '' }} + TARGET_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }} + + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + LOCAL_PHP: ${{ inputs.php-version }}${{ 'latest' != inputs.php-version && '-fpm' || '' }} + LOCAL_MULTISITE: ${{ inputs.multisite }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Performs the following steps: + # - Configure environment variables. + # - Checkout repository. + # - Determine the target SHA value (on `workflow_dispatch` only). + # - Set up Node.js. + # - Log debug information. + # - Install npm dependencies. + # - Install Playwright browsers. + # - Build WordPress. + # - Start Docker environment. + # - Install object cache drop-in. + # - Log running Docker containers. + # - Docker debug information. + # - Install WordPress. + # - Enable themes on Multisite. + # - Install WordPress Importer plugin. + # - Import mock data. + # - Deactivate WordPress Importer plugin. + # - Update permalink structure. + # - Install additional languages. + # - Disable external HTTP requests. + # - Disable cron. + # - List defined constants. + # - Install MU plugin. + # - Run performance tests (current commit). + # - Download previous build artifact (target branch or previous commit). + # - Download artifact. + # - Unzip the build. + # - Run any database upgrades. + # - Flush cache. + # - Delete expired transients. + # - Run performance tests (previous/target commit). + # - Set the environment to the baseline version. + # - Run any database upgrades. + # - Flush cache. + # - Delete expired transients. + # - Run baseline performance tests. + # - Archive artifacts. + # - Compare results. + # - Add workflow summary. + # - Set the base sha. + # - Set commit details. + # - Publish performance results. + # - Ensure version-controlled files are not modified or deleted. + performance: + name: ${{ inputs.multisite && 'Multisite' || 'Single site' }} / ${{ inputs.memcached && 'Memcached' || 'Default' }} + runs-on: ubuntu-24.04 + permissions: + contents: read + if: ${{ ! contains( github.event.before, '00000000' ) }} + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + fetch-depth: ${{ github.event_name == 'workflow_dispatch' && '2' || '1' }} + persist-credentials: false + + # The `workflow_dispatch` event is the only one missing the needed SHA to target. + - name: Retrieve previous commit SHA (if necessary) + if: ${{ github.event_name == 'workflow_dispatch' }} + run: echo "TARGET_SHA=$(git rev-parse HEAD^1)" >> "$GITHUB_ENV" + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + locale -a + + - name: Install npm dependencies + run: npm ci + + - name: Install Playwright browsers + run: npx playwright install --with-deps chromium + + - name: Build WordPress + run: npm run build + + - name: Start Docker environment + run: npm run env:start + + - name: Install object cache drop-in + if: ${{ inputs.memcached }} + run: cp src/wp-content/object-cache.php build/wp-content/object-cache.php + + - name: Log running Docker containers + run: docker ps -a + + - name: Docker debug information + run: | + docker -v + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Enable themes on Multisite + if: ${{ inputs.multisite }} + run: | + npm run env:cli -- theme enable twentytwentyone --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentythree --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfour --network --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- theme enable twentytwentyfive --network --path="/var/www/${LOCAL_DIR}" + + - name: Install WordPress Importer plugin + run: npm run env:cli -- plugin install wordpress-importer --activate --path="/var/www/${LOCAL_DIR}" + + - name: Import mock data + run: | + curl -O https://raw.githubusercontent.com/WordPress/theme-test-data/b9752e0533a5acbb876951a8cbb5bcc69a56474c/themeunittestdata.wordpress.xml + npm run env:cli -- import themeunittestdata.wordpress.xml --authors=create --path="/var/www/${LOCAL_DIR}" + rm themeunittestdata.wordpress.xml + + - name: Deactivate WordPress Importer plugin + run: npm run env:cli -- plugin deactivate wordpress-importer --path="/var/www/${LOCAL_DIR}" + + - name: Install additional languages + run: | + npm run env:cli -- language core install de_DE --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language plugin install de_DE --all --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- language theme install de_DE --all --path="/var/www/${LOCAL_DIR}" + + # Prevent background update checks from impacting test stability. + - name: Disable external HTTP requests + run: npm run env:cli -- config set WP_HTTP_BLOCK_EXTERNAL true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + # Prevent background tasks from impacting test stability. + - name: Disable cron + run: npm run env:cli -- config set DISABLE_WP_CRON true --raw --type=constant --path="/var/www/${LOCAL_DIR}" + + - name: List defined constants + run: npm run env:cli -- config list --path="/var/www/${LOCAL_DIR}" + + - name: Install MU plugin + run: | + mkdir "./${LOCAL_DIR}/wp-content/mu-plugins" + cp ./tests/performance/wp-content/mu-plugins/server-timing.php "./${LOCAL_DIR}/wp-content/mu-plugins/server-timing.php" + + - name: Run performance tests (current commit) + run: npm run test:performance + + - name: Download previous build artifact (target branch or previous commit) + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: get-previous-build + with: + script: | + const artifacts = await github.rest.actions.listArtifactsForRepo({ + owner: context.repo.owner, + repo: context.repo.repo, + name: 'wordpress-build-' + process.env.TARGET_SHA, + }); + + const matchArtifact = artifacts.data.artifacts[0]; + + if ( ! matchArtifact ) { + core.setFailed( 'No artifact found!' ); + return false; + } + + const download = await github.rest.actions.downloadArtifact( { + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + } ); + + const fs = require( 'fs' ); + fs.writeFileSync( process.env.GITHUB_WORKSPACE + '/before.zip', Buffer.from( download.data ) ) + + return true; + + - name: Unzip the build + if: ${{ steps.get-previous-build.outputs.result }} + run: | + unzip "${GITHUB_WORKSPACE}/before.zip" + unzip -o "${GITHUB_WORKSPACE}/wordpress.zip" + + - name: Run any database upgrades + if: ${{ steps.get-previous-build.outputs.result }} + run: npm run env:cli -- core update-db --path="/var/www/${LOCAL_DIR}" + + - name: Flush cache + if: ${{ steps.get-previous-build.outputs.result }} + run: npm run env:cli -- cache flush --path="/var/www/${LOCAL_DIR}" + + - name: Delete expired transients + if: ${{ steps.get-previous-build.outputs.result }} + run: npm run env:cli -- transient delete --expired --path="/var/www/${LOCAL_DIR}" + + - name: Run target performance tests (previous/target commit) + if: ${{ steps.get-previous-build.outputs.result }} + env: + TEST_RESULTS_PREFIX: before + run: npm run test:performance + + - name: Set the environment to the baseline version + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: | + VERSION="${BASE_TAG}" + VERSION="${VERSION%.0}" + npm run env:cli -- core update --version="$VERSION" --force --path="/var/www/${LOCAL_DIR}" + npm run env:cli -- core version --path="/var/www/${LOCAL_DIR}" + + - name: Run any database upgrades + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: npm run env:cli -- core update-db --path="/var/www/${LOCAL_DIR}" + + - name: Flush cache + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: npm run env:cli -- cache flush --path="/var/www/${LOCAL_DIR}" + + - name: Delete expired transients + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + run: npm run env:cli -- transient delete --expired --path="/var/www/${LOCAL_DIR}" + + - name: Run baseline performance tests + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' }} + env: + TEST_RESULTS_PREFIX: base + run: npm run test:performance + + - name: Archive artifacts + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: always() + with: + name: performance-artifacts${{ inputs.multisite && '-multisite' || '' }}${{ inputs.memcached && '-memcached' || '' }}-${{ github.run_id }} + path: artifacts + if-no-files-found: ignore + include-hidden-files: true + + - name: Compare results + run: node ./tests/performance/compare-results.js "${RUNNER_TEMP}/summary.md" + + - name: Add workflow summary + run: cat "${RUNNER_TEMP}/summary.md" >> "$GITHUB_STEP_SUMMARY" + + - name: Set the base sha + # Only needed when publishing results. + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! inputs.memcached && ! inputs.multisite }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + id: base-sha + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const baseRef = await github.rest.git.getRef({ + owner: context.repo.owner, + repo: context.repo.repo, + ref: 'tags/' + process.env.BASE_TAG, + }); + return baseRef.data.object.sha; + + - name: Publish performance results + # Only publish results on pushes to trunk. + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/trunk' && ! inputs.memcached && ! inputs.multisite }} + env: + BASE_SHA: ${{ steps.base-sha.outputs.result }} + CODEVITALS_PROJECT_TOKEN: ${{ secrets.CODEVITALS_PROJECT_TOKEN }} + HOST_NAME: "codevitals.run" + run: | + if [ -z "$CODEVITALS_PROJECT_TOKEN" ]; then + echo "Performance results could not be published. 'CODEVITALS_PROJECT_TOKEN' is not set" + exit 1 + fi + COMMITTED_AT="$(git show -s "$GITHUB_SHA" --format='%cI')" + node ./tests/performance/log-results.js "$CODEVITALS_PROJECT_TOKEN" trunk "$GITHUB_SHA" "$BASE_SHA" "$COMMITTED_AT" "$HOST_NAME" + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-php-compatibility.yml b/.github/workflows/reusable-php-compatibility.yml new file mode 100644 index 0000000000000..a00c952bae6d1 --- /dev/null +++ b/.github/workflows/reusable-php-compatibility.yml @@ -0,0 +1,90 @@ +## +# A reusable workflow that runs PHP compatibility tests. +## +name: PHP Compatibility + +on: + workflow_call: + inputs: + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs PHP compatibility tests. + # + # Violations are reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up PHP. + # - Logs debug information. + # - Configures caching for PHP compatibility scans. + # - Installs Composer dependencies. + # - Make Composer packages available globally. + # - Runs the PHP compatibility tests. + # - Generate a report for displaying issues as pull request annotations. + # - Ensures version-controlled files are not modified or deleted. + php-compatibility: + name: Run compatibility checks + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: ${{ inputs.php-version }} + coverage: none + tools: cs2pr + + - name: Log debug information + run: | + composer --version + + # This date is used to ensure that the PHP compatibility cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + - name: Cache PHP compatibility scan cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: .cache/phpcompat.json + key: ${{ runner.os }}-date-${{ steps.get-date.outputs.date }}-php-${{ inputs.php-version }}-phpcompat-cache-${{ hashFiles('**/composer.json', 'phpcompat.xml.dist') }} + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Make Composer packages available globally + run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" + + - name: Run PHP compatibility tests + id: phpcs + run: phpcs --standard=phpcompat.xml.dist --report-full --report-checkstyle=./.cache/phpcs-compat-report.xml + + - name: Show PHPCompatibility results in PR + if: ${{ always() && steps.phpcs.outcome == 'failure' }} + run: cs2pr ./.cache/phpcs-compat-report.xml + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-phpstan-static-analysis-v1.yml b/.github/workflows/reusable-phpstan-static-analysis-v1.yml new file mode 100644 index 0000000000000..d1ac9f9799792 --- /dev/null +++ b/.github/workflows/reusable-phpstan-static-analysis-v1.yml @@ -0,0 +1,109 @@ +## +# A reusable workflow that runs PHP Static Analysis tests. +## +name: PHP Static Analysis + +on: + workflow_call: + inputs: + php-version: + description: 'The PHP version to use.' + required: false + type: 'string' + default: 'latest' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs PHP static analysis tests. + # + # Violations are reported inline with annotations. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up PHP. + # - Logs debug information. + # - Installs Composer dependencies. + # - Configures caching for PHP static analysis scans. + # - Make Composer packages available globally. + # - Runs PHPStan static analysis (with Pull Request annotations). + # - Saves the PHPStan result cache. + # - Ensures version-controlled files are not modified or deleted. + phpstan: + name: Run PHP static analysis + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: ${{ inputs.php-version }} + coverage: none + tools: cs2pr + + # This date is used to ensure that the Composer cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + - name: General debug information + run: | + npm --version + node --version + composer --version + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Make Composer packages available globally + run: echo "${PWD}/vendor/bin" >> "$GITHUB_PATH" + + - name: Install npm dependencies + run: npm ci --ignore-scripts + + - name: Build WordPress + run: npm run build:dev + + - name: Cache PHP Static Analysis scan cache + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + with: + path: .cache # This is defined in the base.neon file. + key: "phpstan-result-cache-${{ github.run_id }}" + restore-keys: | + phpstan-result-cache- + + - name: Run PHP static analysis tests + id: phpstan + run: composer run phpstan -- -vvv --error-format=checkstyle | cs2pr --errors-as-warnings --graceful-warnings + + - name: "Save result cache" + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + if: ${{ !cancelled() }} + with: + path: .cache + key: "phpstan-result-cache-${{ github.run_id }}" + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-phpunit-tests-v1.yml b/.github/workflows/reusable-phpunit-tests-v1.yml new file mode 100644 index 0000000000000..b55e9f58fcf17 --- /dev/null +++ b/.github/workflows/reusable-phpunit-tests-v1.yml @@ -0,0 +1,197 @@ +## +# DEPRECATED +# +# A reusable workflow that runs the PHPUnit test suite with the specified configuration. +# +# This workflow is used by branches 4.7 through 5.1. +## +name: Run PHPUnit tests + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on' + required: false + type: 'string' + default: 'ubuntu-24.04' + php: + description: 'The version of PHP to use, in the format of X.Y' + required: true + type: 'string' + phpunit: + description: 'The PHPUnit version to use when running tests. See .env for details about valid values.' + required: false + type: 'string' + default: ${{ inputs.php }}-fpm + multisite: + description: 'Whether to run tests as multisite' + required: false + type: 'boolean' + default: false + split_slow: + description: 'Whether to run slow tests group.' + required: false + type: 'boolean' + default: false + memcached: + description: 'Whether to test with memcached enabled' + required: false + type: 'boolean' + default: false + phpunit-config: + description: 'The PHPUnit configuration file to use' + required: false + type: 'string' + default: 'phpunit.xml.dist' + allow-errors: + description: 'Whether to continue when test errors occur.' + required: false + type: boolean + default: false +env: + COMPOSER_INSTALL: false + LOCAL_PHP: ${{ inputs.php }}-fpm + LOCAL_PHPUNIT: ${{ inputs.phpunit && inputs.phpunit || inputs.php }}-fpm + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} + PHPUNIT_SCRIPT: php + PUPPETEER_SKIP_DOWNLOAD: true + SLOW_TESTS: 'external-http,media' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the PHPUnit tests for WordPress. + # + # Performs the following steps: + # - Sets environment variables. + # - Sets up the environment variables needed for testing with memcached (if desired). + # - Installs NodeJS. + # - Build WordPress + # _ Installs npm dependencies. + # - Configures caching for Composer. + # _ Installs Composer dependencies (if desired). + # - Logs Docker debug information (about the Docker installation within the runner). + # - Starts the WordPress Docker container. + # - Starts the Memcached server after the Docker network has been created (if desired). + # - Logs general debug information about the runner. + # - Logs the running Docker containers. + # - Logs debug information from inside the WordPress Docker container. + # - Logs debug information about what's installed within the WordPress Docker containers. + # - Install WordPress within the Docker container. + # - Run the PHPUnit tests. + test-php: + name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }} + runs-on: ${{ inputs.os }} + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Install Dependencies + run: npm ci + + - name: Build WordPress + run: npm run build + + - name: Get composer cache directory + if: ${{ env.COMPOSER_INSTALL == true }} + id: composer-cache + run: echo "composer_dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer dependencies + if: ${{ env.COMPOSER_INSTALL == true }} + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + env: + cache-name: cache-composer-dependencies + with: + path: ${{ steps.composer-cache.outputs.dir }} + key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} + + - name: Install Composer dependencies + if: ${{ env.COMPOSER_INSTALL == true }} + run: | + docker compose run --rm php composer --version + docker compose run --rm php composer install + + - name: Docker debug information + run: | + docker -v + docker compose -v + + - name: Start Docker environment + run: | + npm run env:start + + # The memcached server needs to start after the Docker network has been set up with `npm run env:start`. + - name: Start the Memcached server. + if: ${{ inputs.memcached }} + run: | + cp tests/phpunit/includes/object-cache.php build/wp-content/object-cache.php + BASE=$(basename "$PWD") + docker run --name memcached --net "${BASE}_wpdevnet" -d memcached + + - name: General debug information + run: | + npm --version + node --version + curl --version + git --version + + - name: Log running Docker containers + run: docker ps -a + + - name: WordPress Docker container debug information + run: | + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Run slow PHPUnit tests + if: ${{ inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" + + - name: Run PHPUnit tests for single site excluding slow tests + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" + + - name: Run PHPUnit tests for Multisite excluding slow tests + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" + + - name: Run PHPUnit tests + if: ${{ inputs.php >= '7.0' }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" + + - name: Run AJAX tests + if: ${{ ! inputs.multisite && ! inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax + + - name: Run external HTTP tests + if: ${{ ! inputs.multisite && ! inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http diff --git a/.github/workflows/reusable-phpunit-tests-v2.yml b/.github/workflows/reusable-phpunit-tests-v2.yml new file mode 100644 index 0000000000000..36d5927976505 --- /dev/null +++ b/.github/workflows/reusable-phpunit-tests-v2.yml @@ -0,0 +1,212 @@ +## +# DEPRECATED +# +# A reusable workflow that runs the PHPUnit test suite with the specified configuration. +# +# This workflow is used by branches 5.2 through 5.8. +## +name: Run PHPUnit tests + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on' + required: false + type: 'string' + default: 'ubuntu-24.04' + php: + description: 'The version of PHP to use, in the format of X.Y' + required: true + type: 'string' + multisite: + description: 'Whether to run tests as multisite' + required: false + type: 'boolean' + default: false + split_slow: + description: 'Whether to run slow tests group.' + required: false + type: 'boolean' + default: false + test_ajax: + description: 'Whether to run AJAX tests.' + required: false + type: 'boolean' + default: true + memcached: + description: 'Whether to test with memcached enabled' + required: false + type: 'boolean' + default: false + phpunit-config: + description: 'The PHPUnit configuration file to use' + required: false + type: 'string' + default: 'phpunit.xml.dist' + report: + description: 'Whether to report results to WordPress.org Hosting Tests' + required: false + type: 'boolean' + default: false + allow-errors: + description: 'Whether to continue when test errors occur.' + required: false + type: boolean + default: false +env: + LOCAL_PHP: ${{ inputs.php }}-fpm + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} + PUPPETEER_SKIP_DOWNLOAD: true + # Controls which npm script to use for running PHPUnit tests. Options ar `php` and `php-composer`. + PHPUNIT_SCRIPT: php + SLOW_TESTS: 'external-http,media' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the PHPUnit tests for WordPress. + # + # Performs the following steps: + # - Sets environment variables. + # - Checks out the repository. + # - Installs Node.js. + # - Installs npm dependencies + # - Configures caching for Composer. + # - Installs Composer dependencies. + # - Logs Docker debug information (about the Docker installation within the runner). + # - Starts the WordPress Docker container. + # - Logs general debug information about the runner. + # - Logs the running Docker containers. + # - Logs debug information from inside the WordPress Docker container. + # - Install WordPress within the Docker container. + # - Run the PHPUnit tests. + # - Ensures version-controlled files are not modified or deleted. + test-php: + name: PHP ${{ inputs.php }} / ${{ inputs.multisite && ' Multisite' || 'Single Site' }}${{ inputs.split_slow && ' slow tests' || '' }}${{ inputs.memcached && ' with memcached' || '' }} + runs-on: ${{ inputs.os }} + timeout-minutes: 20 + permissions: + contents: read + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Install Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Install npm dependencies + run: npm ci + + - name: Get composer cache directory + id: composer-cache + run: echo "composer_dir=$(composer config cache-files-dir)" >> "$GITHUB_OUTPUT" + + - name: Cache Composer dependencies + uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5.0.4 + env: + cache-name: cache-composer-dependencies + with: + path: ${{ steps.composer-cache.outputs.composer_dir }} + key: ${{ runner.os }}-php-${{ inputs.php }}-composer-${{ hashFiles('**/composer.lock') }} + + - name: Install Composer dependencies + run: | + docker compose run --rm php composer --version + + # The PHPUnit 7.x phar is not compatible with PHP 8 and won't be updated, + # as PHPUnit 7 is no longer supported. The Composer-installed PHPUnit should be + # used for PHP 8 testing instead. + if [ "${LOCAL_PHP}" == '8.0-fpm' ]; then + docker compose run --rm php composer install --ignore-platform-reqs + echo "PHPUNIT_SCRIPT=php-composer" >> "$GITHUB_ENV" + elif [ "${LOCAL_PHP}" == '7.1-fpm' ]; then + docker compose run --rm php composer update + git checkout -- composer.lock + elif [[ "${LOCAL_PHP}" == '5.6-fpm' || "${LOCAL_PHP}" == '7.0-fpm' ]]; then + docker compose run --rm php composer require --dev phpunit/phpunit:"^5.7" --update-with-dependencies + git checkout -- composer.lock composer.json + else + docker compose run --rm php composer install + fi + + - name: Docker debug information + run: | + docker -v + docker compose -v + + - name: Start Docker environment + run: | + npm run env:start + + - name: General debug information + run: | + npm --version + node --version + curl --version + git --version + + - name: Log running Docker containers + run: docker ps -a + + - name: WordPress Docker container debug information + run: | + docker compose run --rm mysql mysql --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Run slow PHPUnit tests + if: ${{ inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group "${SLOW_TESTS}" + + - name: Run PHPUnit tests for single site excluding slow tests + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && ! inputs.multisite }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-required" + + - name: Run PHPUnit tests for Multisite excluding slow tests + if: ${{ inputs.php < '7.0' && ! inputs.split_slow && inputs.multisite }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --exclude-group "${SLOW_TESTS},ajax,ms-files,ms-excluded,oembed-headers" + + - name: Run PHPUnit tests + if: ${{ inputs.php >= '7.0' }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" + + - name: Run AJAX tests + if: ${{ ! inputs.split_slow&& inputs.test_ajax }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ajax + + - name: Run ms-files tests as a multisite install + if: ${{ inputs.multisite && ! inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c "${PHPUNIT_CONFIG}" --group ms-files + + - name: Run external HTTP tests + if: ${{ ! inputs.multisite && ! inputs.split_slow }} + run: npm run "test:${PHPUNIT_SCRIPT}" -- --verbose -c phpunit.xml.dist --group external-http + + # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. + - name: Run (xDebug) tests + if: ${{ ! inputs.split_slow }} + run: LOCAL_PHP_XDEBUG=true npm run "test:${PHPUNIT_SCRIPT}" -- -v --group xdebug --exclude-group __fakegroup__ + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-phpunit-tests-v3.yml b/.github/workflows/reusable-phpunit-tests-v3.yml new file mode 100644 index 0000000000000..793fac8adfc4f --- /dev/null +++ b/.github/workflows/reusable-phpunit-tests-v3.yml @@ -0,0 +1,271 @@ +## +# A reusable workflow that runs the PHPUnit test suite with the specified configuration. +# +# This workflow is used by `trunk` and branches >= 5.9. +## +name: Run PHPUnit tests + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on' + required: false + type: 'string' + default: 'ubuntu-24.04' + php: + description: 'The version of PHP to use, in the format of X.Y' + required: true + type: 'string' + db-type: + description: 'Database type. Valid types are mysql and mariadb' + required: false + type: 'string' + default: 'mysql' + db-version: + description: 'Database version' + required: false + type: 'string' + default: '8.4' + db-innovation: + description: 'Whether a database software innovation release is being tested.' + required: false + type: 'boolean' + default: false + multisite: + description: 'Whether to run tests as multisite' + required: false + type: 'boolean' + default: false + memcached: + description: 'Whether to test with memcached enabled' + required: false + type: 'boolean' + default: false + phpunit-config: + description: 'The PHPUnit configuration file to use' + required: false + type: 'string' + default: 'phpunit.xml.dist' + phpunit-test-groups: + description: 'A list of test groups to run.' + required: false + type: 'string' + default: '' + tests-domain: + description: 'The domain to use for the tests' + required: false + type: 'string' + default: 'example.org' + coverage-report: + description: 'Whether to generate a code coverage report.' + required: false + type: boolean + default: false + report: + description: 'Whether to report results to WordPress.org Hosting Tests' + required: false + type: 'boolean' + default: false + allow-errors: + description: 'Whether to continue when test errors occur.' + required: false + type: boolean + default: false + secrets: + CODECOV_TOKEN: + description: 'The Codecov token required for uploading reports.' + required: false + WPT_REPORT_API_KEY: + description: 'The WordPress.org Hosting Tests API key.' + required: false + +env: + LOCAL_PHP: ${{ inputs.php }}-fpm + LOCAL_PHP_XDEBUG: ${{ inputs.coverage-report || false }} + LOCAL_PHP_XDEBUG_MODE: ${{ inputs.coverage-report && 'coverage' || 'develop,debug' }} + LOCAL_DB_TYPE: ${{ inputs.db-type }} + LOCAL_DB_VERSION: ${{ inputs.db-version }} + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + LOCAL_WP_TESTS_DOMAIN: ${{ inputs.tests-domain }} + PHPUNIT_CONFIG: ${{ inputs.phpunit-config }} + PUPPETEER_SKIP_DOWNLOAD: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs the PHPUnit tests for WordPress. + # + # Performs the following steps: + # - Sets environment variables. + # - Checks out the repository. + # - Sets up Node.js. + # - Sets up PHP. + # - Installs Composer dependencies. + # - Installs npm dependencies + # - Logs general debug information about the runner. + # - Logs Docker debug information (about the Docker installation within the runner). + # - Starts the WordPress Docker container. + # - Logs the running Docker containers. + # - Logs debug information about what's installed within the WordPress Docker containers. + # - Install WordPress within the Docker container. + # - Run the PHPUnit tests. + # - Upload the code coverage report to Codecov.io. + # - Upload the HTML code coverage report as an artifact. + # - Ensures version-controlled files are not modified or deleted. + # - Checks out the WordPress Test reporter repository. + # - Submit the test results to the WordPress.org host test results. + phpunit-tests: + name: ${{ ( inputs.phpunit-test-groups || inputs.coverage-report ) && format( 'PHP {0} with ', inputs.php ) || '' }} ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }}${{ inputs.db-innovation && ' (innovation release)' || '' }}${{ inputs.memcached && ' with memcached' || '' }}${{ inputs.report && ' (test reporting enabled)' || '' }} ${{ 'example.org' != inputs.tests-domain && inputs.tests-domain || '' }} + runs-on: ${{ inputs.os }} + timeout-minutes: ${{ inputs.coverage-report && 120 || inputs.php == '8.4' && 30 || 20 }} + permissions: + contents: read + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + ## + # This allows Composer dependencies to be installed using a single step. + # + # Since the tests are currently run within the Docker containers where the PHP version varies, + # the same PHP version needs to be configured for the action runner machine so that the correct + # dependency versions are installed and cached. + ## + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: '${{ inputs.php }}' + coverage: none + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") + + - name: Install npm dependencies + run: npm ci + + - name: Build WordPress + run: npm run build:dev + + - name: General debug information + run: | + npm --version + node --version + curl --version + git --version + composer --version + locale -a + + - name: Docker debug information + run: | + docker -v + + - name: Start Docker environment + run: | + npm run env:start + + - name: Log running Docker containers + run: docker ps -a + + - name: WordPress Docker container debug information + run: | + docker compose run --rm mysql "${LOCAL_DB_CMD}" --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + env: + LOCAL_DB_CMD: ${{ env.LOCAL_DB_TYPE == 'mariadb' && contains( fromJSON('["5.5", "10.0", "10.1", "10.2", "10.3"]'), env.LOCAL_DB_VERSION ) && 'mysql' || env.LOCAL_DB_TYPE }} + + - name: Install WordPress + run: npm run env:install + + - name: Run PHPUnit tests${{ inputs.phpunit-test-groups && format( ' ({0} groups)', inputs.phpunit-test-groups ) || '' }}${{ inputs.coverage-report && ' with coverage report' || '' }} + continue-on-error: ${{ inputs.allow-errors }} + run: | + node ./tools/local-env/scripts/docker.js run \ + php ./vendor/bin/phpunit \ + --verbose \ + -c "${PHPUNIT_CONFIG}" \ + ${{ inputs.phpunit-test-groups && '--group "${TEST_GROUPS}"' || '' }} \ + ${{ inputs.coverage-report && '--coverage-clover "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}.xml" --coverage-html "wp-code-coverage-${MULTISITE_FLAG}-${GITHUB_SHA}"' || '' }} + env: + TEST_GROUPS: ${{ inputs.phpunit-test-groups }} + MULTISITE_FLAG: ${{ inputs.multisite && 'multisite' || 'single' }} + + - name: Run AJAX tests + if: ${{ ! inputs.phpunit-test-groups && ! inputs.coverage-report }} + continue-on-error: ${{ inputs.allow-errors }} + run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group ajax + + - name: Run ms-files tests as a multisite install + if: ${{ inputs.multisite && ! inputs.phpunit-test-groups && ! inputs.coverage-report }} + continue-on-error: ${{ inputs.allow-errors }} + run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group ms-files + + - name: Run external HTTP tests + if: ${{ ! inputs.multisite && ! inputs.phpunit-test-groups && ! inputs.coverage-report }} + continue-on-error: ${{ inputs.allow-errors }} + run: node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit --verbose -c "${PHPUNIT_CONFIG}" --group external-http + + # __fakegroup__ is excluded to force PHPUnit to ignore the settings in phpunit.xml.dist. + - name: Run (Xdebug) tests + if: ${{ ! inputs.phpunit-test-groups && ! inputs.coverage-report }} + continue-on-error: ${{ inputs.allow-errors }} + run: LOCAL_PHP_XDEBUG=true node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit -v --group xdebug --exclude-group __fakegroup__ + + - name: Upload test coverage report to Codecov + if: ${{ inputs.coverage-report }} + uses: codecov/codecov-action@1af58845a975a7985b0beb0cbe6fbbb71a41dbad # v5.5.3 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }}.xml + flags: ${{ inputs.multisite && 'multisite' || 'single' }},php + fail_ci_if_error: true + + - name: Upload HTML coverage report as artifact + if: ${{ inputs.coverage-report }} + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} + path: wp-code-coverage${{ inputs.multisite && '-multisite' || '-single' }}-${{ github.sha }} + overwrite: true + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code + + - name: Checkout the WordPress Test Reporter + if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }} + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: 'WordPress/phpunit-test-runner' + path: 'test-runner' + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Submit test results to the WordPress.org host test results + if: ${{ github.ref == 'refs/heads/trunk' && inputs.report }} + env: + WPT_REPORT_API_KEY: "${{ secrets.WPT_REPORT_API_KEY }}" + run: docker compose run --rm -e WPT_REPORT_API_KEY -e WPT_PREPARE_DIR=/var/www -e WPT_TEST_DIR=/var/www php php test-runner/report.php diff --git a/.github/workflows/reusable-support-json-reader-v1.yml b/.github/workflows/reusable-support-json-reader-v1.yml new file mode 100644 index 0000000000000..be5693aac7297 --- /dev/null +++ b/.github/workflows/reusable-support-json-reader-v1.yml @@ -0,0 +1,155 @@ +## +# A reusable workflow that reads the .version-support-*.json files and returns values for use in a +# test matrix based on a given WordPress version. +## +name: Determine test matrix values + +on: + workflow_call: + inputs: + wp-version: + description: 'The WordPress version to test . Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' + type: string + default: 'nightly' + repository: + description: 'The repository to read support JSON files from.' + type: string + default: 'WordPress/wordpress-develop' + outputs: + major-wp-version: + description: "The major WordPress version based on the version provided in wp-version" + value: ${{ jobs.major-wp-version.outputs.version }} + php-versions: + description: "The PHP versions to test for the given wp-version" + value: ${{ jobs.php-versions.outputs.versions }} + mysql-versions: + description: "The MySQL versions to test for the given wp-version" + value: ${{ jobs.mysql-versions.outputs.versions }} + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Determines the major version of WordPress being tested. + # + # The data in the JSON files are indexed by major version, so this is used to look up the appropriate support policy. + # + # Performs the following steps: + # - Checks out the repository + # - Returns the major WordPress version as an output based on the value passed to the wp-version input. + major-wp-version: + name: Determine major WordPress version + permissions: + contents: read + runs-on: ubuntu-24.04 + timeout-minutes: 5 + outputs: + version: ${{ steps.major-wp-version.outputs.version }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ inputs.repository }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Determine the major WordPress version + id: major-wp-version + run: | + if [ "${WP_VERSION}" ] && [ "${WP_VERSION}" != "nightly" ] && [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "trunk" ]; then + echo "version=$(echo "${WP_VERSION}" | tr '.' '-' | cut -d '-' -f1-2)" >> "$GITHUB_OUTPUT" + elif [ "${WP_VERSION}" ] && [ "${WP_VERSION}" != "trunk" ]; then + echo "version=${WP_VERSION}" >> "$GITHUB_OUTPUT" + else + echo "version=nightly" >> "$GITHUB_OUTPUT" + fi + env: + WP_VERSION: ${{ inputs.wp-version }} + + # Determines the versions of PHP supported for a version of WordPress. + # + # Performs the following steps: + # - Checks out the repository + # - Returns the versions of PHP supported for the major version of WordPress by parsing the + # .version-support-php.json file and returning the values in that version's index. + php-versions: + name: Determine PHP versions + permissions: + contents: read + runs-on: ubuntu-24.04 + needs: [ major-wp-version ] + timeout-minutes: 5 + outputs: + versions: ${{ steps.php-versions.outputs.versions }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ inputs.repository }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + # Look up the major version's specific PHP support policy when a version is provided. + # Otherwise, use the current PHP support policy. + - name: Get supported PHP versions + id: php-versions + run: | + if [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "nightly" ]; then + VERSIONS="$( jq \ + -r \ + --arg wp_version "${WP_VERSION}" \ + '.[$wp_version] | @json' \ + .version-support-php.json + )" + echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" + else + echo "versions=$(jq -r '.[ (keys[-1]) ] | @json' .version-support-php.json)" >> "$GITHUB_OUTPUT" + fi + env: + WP_VERSION: ${{ needs.major-wp-version.outputs.version }} + + # Determines the versions of MySQL supported for a version of WordPress. + # + # Performs the following steps: + # - Checks out the repository + # - Returns the versions of MySQL supported for the major version of WordPress by parsing the + # .version-support-mysql.json file and returning the values in that version's index. + mysql-versions: + name: Determine MySQL versions + permissions: + contents: read + runs-on: ubuntu-24.04 + needs: [ major-wp-version ] + timeout-minutes: 5 + outputs: + versions: ${{ steps.mysql-versions.outputs.versions }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: ${{ inputs.repository }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + # Look up the major version's specific MySQL support policy when a version is provided. + # Otherwise, use the current MySQL support policy. + - name: Get supported MySQL versions + id: mysql-versions + run: | + if [ "${WP_VERSION}" != "latest" ] && [ "${WP_VERSION}" != "nightly" ]; then + VERSIONS="$( jq \ + -r \ + --arg wp_version "${WP_VERSION}" \ + '.[$wp_version] | @json' \ + .version-support-mysql.json + )" + echo "versions=$VERSIONS" >> "$GITHUB_OUTPUT" + else + echo "versions=$(jq -r '.[ (keys[-1]) ] | @json' .version-support-mysql.json)" >> "$GITHUB_OUTPUT" + fi + env: + WP_VERSION: ${{ needs.major-wp-version.outputs.version }} diff --git a/.github/workflows/reusable-test-core-build-process.yml b/.github/workflows/reusable-test-core-build-process.yml new file mode 100644 index 0000000000000..b5c40eb040e64 --- /dev/null +++ b/.github/workflows/reusable-test-core-build-process.yml @@ -0,0 +1,158 @@ +## +# A reusable workflow that tests the WordPress Core build process. +## +name: Test the WordPress Build Process + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on' + required: false + type: 'string' + default: 'ubuntu-24.04' + directory: + description: 'Directory to run WordPress from. Valid values are `src` or `build`' + required: false + type: 'string' + default: 'src' + test-emoji: + description: 'Whether to run the precommit:emoji Grunt script.' + required: false + type: 'boolean' + default: true + test-certificates: + description: 'Whether to run the certificate related Grunt scripts.' + required: false + type: 'boolean' + default: false + save-build: + description: 'Whether to save a ZIP of built WordPress as an artifact.' + required: false + type: 'boolean' + default: false + prepare-playground: + description: 'Whether to prepare the artifacts needed for Playground testing.' + required: false + type: 'boolean' + default: false + +env: + PUPPETEER_SKIP_DOWNLOAD: true + NODE_OPTIONS: --max-old-space-size=4096 + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Verifies that installing npm dependencies and building WordPress works as expected. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Logs debug information about the GitHub Action runner. + # - Installs npm dependencies. + # - Builds WordPress to run from the desired location (src or build). + # - Ensures version-controlled files are not modified or deleted. + # - Creates a ZIP of the built WordPress files (when building to the build directory). + # - Cleans up after building WordPress. + # - Ensures version-controlled files are not modified or deleted. + # - Uploads the ZIP as a GitHub Actions artifact (when building to the build directory). + # - Saves the pull request number to a text file. + # - Uploads the pull request number as an artifact. + build-process-tests: + name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }} + permissions: + contents: read + runs-on: ${{ inputs.os }} + timeout-minutes: 20 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + # This date is used to ensure that the PHPCS cache is cleared at least once every week. + # http://man7.org/linux/man-pages/man1/date.1.html + - name: "Get last Monday's date" + id: get-date + if: ${{ inputs.test-certificates }} + run: echo "date=$(/bin/date -u --date='last Mon' "+%F")" >> "$GITHUB_OUTPUT" + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + if: ${{ inputs.test-certificates }} + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: ${{ steps.get-date.outputs.date }} + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + + - name: Install npm Dependencies + run: npm ci + + - name: Run Emoji precommit task + if: ${{ inputs.test-emoji }} + run: npm run grunt precommit:emoji + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Ensure certificates files are updated + if: ${{ inputs.test-certificates }} + run: npm run grunt copy:certificates && npm run grunt build:certificates + + - name: Build WordPress to run from ${{ inputs.directory }} + run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }} + + - name: Ensure version-controlled files are not modified or deleted during building + run: git diff --exit-code + + - name: Create ZIP of built files + if: ${{ inputs.directory == 'build' && contains( inputs.os, 'ubuntu-' ) }} + run: zip -r wordpress.zip build/. + + - name: Clean after building to run from ${{ inputs.directory }} + run: npm run grunt ${{ inputs.directory == 'src' && 'clean -- --dev' || 'clean' }} + + - name: Ensure version-controlled files are not modified or deleted during cleaning + run: git diff --exit-code + + - name: Upload ZIP as a GitHub Actions artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ inputs.save-build || inputs.prepare-playground }} + with: + name: wordpress-build-${{ github.event_name == 'pull_request' && github.event.number || github.sha }} + path: wordpress.zip + if-no-files-found: error + + - name: Save PR number + if: ${{ inputs.prepare-playground }} + run: | + mkdir -p ./pr-number + echo "${EVENT_NUMBER}" > ./pr-number/NR + env: + EVENT_NUMBER: ${{ github.event.number }} + + # Uploads the PR number as an artifact for the Pull Request Commenting workflow to download and then + # leave a comment detailing how to test the PR within WordPress Playground. + - name: Upload PR number as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ inputs.prepare-playground && github.repository == 'WordPress/wordpress-develop' && github.event_name == 'pull_request' }} + with: + name: pr-number + path: pr-number/ diff --git a/.github/workflows/reusable-test-gutenberg-build-process.yml b/.github/workflows/reusable-test-gutenberg-build-process.yml new file mode 100644 index 0000000000000..772b8ee577d7f --- /dev/null +++ b/.github/workflows/reusable-test-gutenberg-build-process.yml @@ -0,0 +1,100 @@ +## +# A reusable workflow that tests the Gutenberg plugin build process when run within a wordpress-develop checkout. +## +name: Test the Gutenberg plugin Build Process + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on' + required: false + type: 'string' + default: 'ubuntu-24.04' + directory: + description: 'Directory to run WordPress from. Valid values are `src` or `build`' + required: false + type: 'string' + default: 'src' + +env: + GUTENBERG_DIRECTORY: ${{ inputs.directory == 'build' && 'build' || 'src' }}/wp-content/plugins/gutenberg + PUPPETEER_SKIP_DOWNLOAD: true + NODE_OPTIONS: '--max-old-space-size=8192' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Verifies that installing npm dependencies and building the Gutenberg plugin works as expected. + # + # Performs the following steps: + # - Checks out the repository. + # - Checks out the Gutenberg plugin into the plugins directory. + # - Sets up Node.js. + # - Logs debug information about the GitHub Action runner. + # - Installs Gutenberg npm dependencies. + # - Runs the Gutenberg build process. + # - Installs Core npm dependencies. + # - Builds WordPress to run from the relevant location (src or build). + # - Builds Gutenberg. + # - Ensures version-controlled files are not modified or deleted. + build-process-tests: + name: ${{ contains( inputs.os, 'macos-' ) && 'MacOS' || contains( inputs.os, 'windows-' ) && 'Windows' || 'Linux' }} + permissions: + contents: read + runs-on: ${{ inputs.os }} + timeout-minutes: 30 + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Checkout Gutenberg plugin + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + repository: 'WordPress/gutenberg' + path: ${{ env.GUTENBERG_DIRECTORY }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + cache-dependency-path: | + package-lock.json + ${{ env.GUTENBERG_DIRECTORY }}/package-lock.json + + - name: Log debug information + run: | + npm --version + node --version + curl --version + git --version + + - name: Install Gutenberg Dependencies + run: npm ci + working-directory: ${{ env.GUTENBERG_DIRECTORY }} + + - name: Build Gutenberg + run: npm run build + working-directory: ${{ env.GUTENBERG_DIRECTORY }} + + - name: Install Core Dependencies + run: npm ci + + - name: Build WordPress to run from ${{ inputs.directory }} + run: npm run ${{ inputs.directory == 'src' && 'build:dev' || 'build' }} + + - name: Run Gutenberg build script after building Core to run from ${{ inputs.directory }} + run: npm run build + working-directory: ${{ env.GUTENBERG_DIRECTORY }} + + - name: Ensure version-controlled files are not modified or deleted during building + run: git diff --exit-code diff --git a/.github/workflows/reusable-test-local-docker-environment-v1.yml b/.github/workflows/reusable-test-local-docker-environment-v1.yml new file mode 100644 index 0000000000000..370b88c6c0231 --- /dev/null +++ b/.github/workflows/reusable-test-local-docker-environment-v1.yml @@ -0,0 +1,170 @@ +## +# A reusable workflow that ensures the local Docker environment is working properly. +# +# This workflow is used by `trunk` and branches >= 6.8. +## +name: Test local Docker environment + +on: + workflow_call: + inputs: + os: + description: 'Operating system to test' + required: false + type: 'string' + default: 'ubuntu-24.04' + php: + description: 'The version of PHP to use, in the format of X.Y' + required: false + type: 'string' + default: 'latest' + db-type: + description: 'Database type. Valid types are mysql and mariadb' + required: false + type: 'string' + default: 'mysql' + db-version: + description: 'Database version' + required: false + type: 'string' + default: '8.4' + memcached: + description: 'Whether to enable memcached' + required: false + type: 'boolean' + default: false + tests-domain: + description: 'The domain to use for the tests' + required: false + type: 'string' + default: 'example.org' + +env: + LOCAL_PHP: ${{ inputs.php == 'latest' && 'latest' || format( '{0}-fpm', inputs.php ) }} + LOCAL_DB_TYPE: ${{ inputs.db-type }} + LOCAL_DB_VERSION: ${{ inputs.db-version }} + LOCAL_PHP_MEMCACHED: ${{ inputs.memcached }} + LOCAL_WP_TESTS_DOMAIN: ${{ inputs.tests-domain }} + PUPPETEER_SKIP_DOWNLOAD: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Tests the local Docker environment. + # + # Performs the following steps: + # - Sets environment variables. + # - Checks out the repository. + # - Sets up Node.js. + # - Sets up PHP. + # - Installs Composer dependencies. + # - Installs npm dependencies + # - Logs general debug information about the runner. + # - Logs Docker debug information (about the Docker installation within the runner). + # - Starts the WordPress Docker container. + # - Logs the running Docker containers. + # - Logs debug information about what's installed within the WordPress Docker containers. + # - Install WordPress within the Docker container. + # - Restarts the Docker environment. + # - Runs a WP CLI command. + # - Tests the logs command. + # - Tests the reset command. + # - Ensures version-controlled files are not modified or deleted. + local-docker-environment-tests: + name: ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.memcached && ' with memcached' || '' }}${{ 'example.org' != inputs.tests-domain && format( ' {0}', inputs.tests-domain ) || '' }} + permissions: + contents: read + runs-on: ${{ inputs.os }} + timeout-minutes: 20 + + steps: + - name: Configure environment variables + run: | + echo "PHP_FPM_UID=$(id -u)" >> "$GITHUB_ENV" + echo "PHP_FPM_GID=$(id -g)" >> "$GITHUB_ENV" + + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + + ## + # This allows Composer dependencies to be installed using a single step. + # + # Since tests are currently run within the Docker containers where the PHP version varies, + # the same PHP version needs to be configured for the action runner machine so that the correct + # dependency versions are installed and cached. + ## + - name: Set up PHP + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: '${{ inputs.php }}' + coverage: none + + # Since Composer dependencies are installed using `composer update` and no lock file is in version control, + # passing a custom cache suffix ensures that the cache is flushed at least once per week. + - name: Install Composer dependencies + uses: ramsey/composer-install@65e4f84970763564f46a70b8a54b90d033b3bdda # 4.0.0 + with: + custom-cache-suffix: $(/bin/date -u --date='last Mon' "+%F") + + - name: Install npm dependencies + run: npm ci + + - name: Build WordPress + run: npm run build:dev + + - name: General debug information + run: | + npm --version + node --version + curl --version + git --version + composer --version + locale -a + + - name: Docker debug information + run: | + docker -v + + - name: Start Docker environment + run: | + npm run env:start + + - name: Log running Docker containers + run: docker ps -a + + - name: WordPress Docker container debug information + run: | + docker compose run --rm mysql "${LOCAL_DB_TYPE}" --version + docker compose run --rm php php --version + docker compose run --rm php php -m + docker compose run --rm php php -i + docker compose run --rm php locale -a + + - name: Install WordPress + run: npm run env:install + + - name: Restart Docker environment + run: npm run env:restart + + - name: Test a CLI command + run: npm run env:cli option get siteurl + + - name: Test logs command + run: npm run env:logs + + - name: Reset the Docker environment + run: npm run env:reset + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code diff --git a/.github/workflows/reusable-upgrade-testing.yml b/.github/workflows/reusable-upgrade-testing.yml new file mode 100644 index 0000000000000..372b6ae0c3e60 --- /dev/null +++ b/.github/workflows/reusable-upgrade-testing.yml @@ -0,0 +1,137 @@ +# A reusable workflow that runs WordPress upgrade testing under the conditions provided. +name: Upgrade Tests + +on: + workflow_call: + inputs: + os: + description: 'Operating system to run tests on.' + required: false + type: 'string' + default: 'ubuntu-24.04' + wp: + description: 'The version of WordPress to start with.' + required: true + type: 'string' + new-version: + description: 'The version of WordPress to update to. Use "latest" to update to the latest version, "develop" to update to the current branch, or provide a specific version number to update to.' + type: 'string' + default: 'latest' + php: + description: 'The version of PHP to use. Expected format: X.Y.' + required: true + type: 'string' + multisite: + description: 'Whether to run tests as multisite.' + required: false + type: 'boolean' + default: false + db-type: + description: 'Database type. Valid types are mysql and mariadb.' + required: false + type: 'string' + default: 'mysql' + db-version: + description: 'Database version.' + required: false + type: 'string' + default: '5.7' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Runs upgrade tests on a build of WordPress. + # + # Performs the following steps: + # - Sets up PHP. + # - Downloads the specified version of WordPress. + # - Creates a `wp-config.php` file. + # - Installs WordPress. + # - Checks the version of WordPress before the upgrade. + # - Updates to the latest minor version. + # - Updates the database after the minor update. + # - Checks the version of WordPress after the minor update. + # - Updates to the version of WordPress being tested. + # - Updates the database. + # - Checks the version of WordPress after the upgrade. + upgrade-tests: + name: PHP ${{ inputs.php }} with ${{ 'mariadb' == inputs.db-type && 'MariaDB' || 'MySQL' }} ${{ inputs.db-version }}${{ inputs.multisite && ' multisite' || '' }} + permissions: + contents: read + runs-on: ${{ inputs.os }} + timeout-minutes: 20 + + services: + database: + image: ${{ inputs.db-type }}:${{ inputs.db-version }} + ports: + - 3306 + options: >- + --health-cmd="mysqladmin ping" + --health-interval="30s" + --health-timeout="10s" + --health-retries="5" + -e MYSQL_ROOT_PASSWORD="root" + -e MYSQL_DATABASE="test_db" + + steps: + - name: Set up PHP ${{ inputs.php }} + uses: shivammathur/setup-php@accd6127cb78bee3e8082180cb391013d204ef9f # 2.37.0 + with: + php-version: '${{ inputs.php }}' + coverage: none + tools: wp-cli + + - name: Download WordPress ${{ inputs.wp }} + run: wp core download --version="${WP_VERSION}" + env: + WP_VERSION: ${{ inputs.wp }} + + - name: Create wp-config.php file + run: wp config create --dbname=test_db --dbuser=root --dbpass=root --dbhost="127.0.0.1:${DB_PORT}" + env: + DB_PORT: ${{ job.services.database.ports['3306'] }} + + - name: Install WordPress + run: | + wp core ${{ inputs.multisite && 'multisite-install' || 'install' }} \ + --url=http://localhost/ --title="Upgrade Test" --admin_user=admin \ + --admin_password=password --admin_email=me@example.org --skip-email + + - name: Pre-upgrade version check + run: wp core version + + - name: Update to the latest minor version + run: wp core update --minor + + - name: Update the database after the minor update + run: wp core update-db ${{ inputs.multisite && '--network' || '' }} + + - name: Post-upgrade version check after the minor update + run: wp core version + + - name: Download build artifact for the current branch + if: ${{ inputs.new-version == 'develop' }} + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: wordpress-develop + + - name: Upgrade to WordPress at current branch + if: ${{ inputs.new-version == 'develop' }} + run: | + wp core update develop.zip + + - name: Upgrade to WordPress ${{ inputs.new-version }} + if: ${{ inputs.new-version != 'develop' }} + run: | + wp core update ${{ 'latest' != inputs.new-version && '--version="${WP_VERSION}"' || '' }} + env: + WP_VERSION: ${{ inputs.new-version }} + + - name: Update the database + run: wp core update-db ${{ inputs.multisite && '--network' || '' }} + + - name: Post-upgrade version check + run: wp core version diff --git a/.github/workflows/reusable-workflow-lint.yml b/.github/workflows/reusable-workflow-lint.yml new file mode 100644 index 0000000000000..13fcde47f5731 --- /dev/null +++ b/.github/workflows/reusable-workflow-lint.yml @@ -0,0 +1,69 @@ +name: Lint GitHub Actions workflows +on: + workflow_call: + +permissions: {} + +jobs: + # Runs the actionlint GitHub Action workflow file linter. + # + # See https://github.com/rhysd/actionlint. + # + # This helps guard against common mistakes including strong type checking for expressions (${{ }}), security checks, + # `run:` script checking, glob syntax validation, and more. + actionlint: + name: Run actionlint + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 5 + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + + - name: Run actionlint + uses: docker://rhysd/actionlint@sha256:5457037ba91acd225478edac3d4b32e45cf6c10291e0dabbfd2491c63129afe1 # v1.7.11 + with: + args: "-color -verbose" + + # Runs the Zizmor GitHub Action workflow file linter. + # + # See https://github.com/zizmorcore/zizmor + # + # This helps guard against supply chain attacks, unpinned dependencies, excessive permissions, + # dangerous triggers, credential leaks, and sophisticated security vulnerabilities. + # + # Performs the following steps: + # - Checks out the repository. + # - Installs and configures uv. + # - Runs a zizmor scan. + # - Uploads the SARIF file to GitHub. + zizmor: + name: Zizmor + runs-on: ubuntu-24.04 + permissions: + security-events: write + actions: read + contents: read + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + persist-credentials: false + + - name: Install the latest version of uv + uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0 + + - name: Run zizmor + run: uvx zizmor@1.24.1 --persona=regular --format=sarif --strict-collection . > results.sarif + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload SARIF file + uses: github/codeql-action/upload-sarif@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0 + with: + sarif_file: results.sarif + category: zizmor diff --git a/.github/workflows/slack-notifications.yml b/.github/workflows/slack-notifications.yml index ceb06c5fd4165..3d0dd7c680558 100644 --- a/.github/workflows/slack-notifications.yml +++ b/.github/workflows/slack-notifications.yml @@ -6,22 +6,6 @@ name: Slack Notifications on: - workflow_run: - workflows: - - Code Coverage Report - - Coding Standards - - End-to-end Tests - - JavaScript Tests - - PHP Compatibility - - PHPUnit Tests - - Test NPM - - Test old branches - types: - - completed - branches: - - '[3-4].[0-9]' - - '5.[0-8]' - workflow_call: inputs: calling_status: @@ -42,8 +26,12 @@ on: description: 'The Slack webhook URL for a failed build.' required: true +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + env: - CURRENT_BRANCH: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_branch || github.ref_name }} + CURRENT_BRANCH: ${{ github.ref_name }} jobs: # Gathers the details needed for Slack notifications. @@ -52,135 +40,190 @@ jobs: # submit data to Slack webhook URLs configured to post messages. # # Performs the following steps: - # - Retrieves the workflow ID (if necessary). - # - Retrieves the workflow URL (if necessary). - # - Retrieves the previous workflow run and stores its conclusion. + # - Retrieves the current workflow run. + # - Determines the conclusion of the previous workflow run or run attempt. # - Sets the previous conclusion as an output. # - Prepares the commit message. # - Constructs and stores a message payload as an output. prepare: name: Prepare notifications - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + permissions: + actions: read + contents: read + timeout-minutes: 5 if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event.workflow_run.event != 'pull_request' }} outputs: - previous_conclusion: ${{ steps.previous-conclusion.outputs.previous_conclusion }} + previous_conclusion: ${{ steps.previous-attempt-result.outputs.result }} payload: ${{ steps.create-payload.outputs.payload }} steps: - - name: Get the workflow ID - id: current-workflow-id - if: ${{ github.event_name == 'push' || github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' }} - uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 + - name: Determine the status of the previous attempt + id: previous-attempt-result + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 with: + retries: 2 + retry-exempt-status-codes: 418 + result-encoding: string script: | const workflow_run = await github.rest.actions.getWorkflowRun({ owner: context.repo.owner, repo: context.repo.repo, - run_id: ${{ github.run_id }}, + run_id: `${context.runId}`, }); - return workflow_run.data.workflow_id; - - name: Get details about the previous workflow run - id: previous-result - uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 - with: - script: | + if ( process.env.CALLING_STATUS == 'failure' && workflow_run.data.run_attempt == 1 ) { + return 'first-failure'; + } + + // When a workflow has been restarted, check the previous run attempt. Because workflows are automatically + // restarted once and a failure on the first run is not reported, failures on the second run should not be + // considered. + if ( workflow_run.data.run_attempt > 2 ) { + const previous_run = await github.rest.actions.getWorkflowRunAttempt({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: `${context.runId}`, + attempt_number: workflow_run.data.run_attempt - 1 + }); + + return previous_run.data.conclusion; + } + + // Otherwise, check the previous workflow run. const previous_runs = await github.rest.actions.listWorkflowRuns({ owner: context.repo.owner, repo: context.repo.repo, - workflow_id: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.workflow_id || steps.current-workflow-id.outputs.result }}, - branch: '${{ env.CURRENT_BRANCH }}', - per_page: 1, - page: 2, + workflow_id: workflow_run.data.workflow_id, + branch: process.env.CURRENT_BRANCH, + exclude_pull_requests: true, }); - return previous_runs.data.workflow_runs[0].conclusion; - - name: Store previous conclusion as an output - id: previous-conclusion - run: echo "::set-output name=previous_conclusion::${{ steps.previous-result.outputs.result }}" + // This is the first workflow run for this branch or tag. + if ( previous_runs.data.workflow_runs.length < 2 ) { + return 'none'; + } + + const expected_events = new Array( 'push', 'schedule', 'workflow_dispatch' ); + + // Find the workflow run for the commit that immediately preceded this one. + for ( let i = 0; i < previous_runs.data.workflow_runs.length; i++ ) { + if ( previous_runs.data.workflow_runs[ i ].run_number == workflow_run.data.run_number ) { + let next_index = i; + do { + next_index++; + + // Protects against a false notification when contributors use the trunk branch as the pull request head_ref. + if ( expected_events.indexOf( previous_runs.data.workflow_runs[ next_index ].event ) == -1 ) { + continue; + } + + return previous_runs.data.workflow_runs[ next_index ].conclusion; + } while ( next_index < previous_runs.data.workflow_runs.length ); + } + } + + // Can't determine previous workflow conclusion. + return 'unknown'; + env: + CALLING_STATUS: ${{ inputs.calling_status }} - name: Get the commit message id: current-commit-message - uses: actions/github-script@441359b1a30438de65712c2fbca0abe4816fa667 # v5.0.0 + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 if: ${{ github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' }} with: + retries: 2 + retry-exempt-status-codes: 418 + result-encoding: string script: | const commit_details = await github.rest.repos.getCommit({ owner: context.repo.owner, repo: context.repo.repo, - ref: '${{ github.sha }}' + ref: context.sha, }); return commit_details.data.commit.message; - - name: Prepare commit message. - id: commit-message - run: | - COMMIT_MESSAGE=$(cat <<'EOF' | awk 'NR==1' | sed 's/`/\\`/g' | sed 's/\"/\\\\\\"/g' | sed 's/\$/\\$/g' - ${{ github.event_name == 'workflow_run' && github.event.workflow_run.head_commit.message || ( github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' ) && fromJson( steps.current-commit-message.outputs.result ) || github.event.head_commit.message }} - EOF - ) - echo "::set-output name=commit_message_escaped::${COMMIT_MESSAGE}" - - name: Construct payload and store as an output id: create-payload - run: echo "::set-output name=payload::{\"workflow_name\":\"${{ github.event_name == 'workflow_run' && github.event.workflow_run.name || github.workflow }}\",\"ref_name\":\"${{ env.CURRENT_BRANCH }}\",\"run_url\":\"https://github.com/WordPress/wordpress-develop/actions/runs/${{ github.event_name == 'workflow_run' && github.event.workflow_run.id || github.run_id }}\",\"commit_message\":\"${{ steps.commit-message.outputs.commit_message_escaped }}\"}" + run: | + COMMIT_MSG="$(echo "${COMMIT_MSG_RAW}" | awk 'NR==1')" + PAYLOAD="$( jq \ + -n \ + --arg workflow_name "${GITHUB_WORKFLOW}" \ + --arg ref_name "${CURRENT_BRANCH}" \ + --arg run_url "https://github.com/WordPress/wordpress-develop/actions/runs/${GITHUB_RUN_ID}/attempts/${GITHUB_RUN_ATTEMPT}" \ + --arg commit_message "${COMMIT_MSG}" \ + '{workflow_name: $workflow_name, ref_name: $ref_name, run_url: $run_url, commit_message: $commit_message}' | jq -c . + )" + echo "payload=$PAYLOAD" >> "$GITHUB_OUTPUT" + env: + COMMIT_MSG_RAW: ${{ ( github.event_name == 'workflow_dispatch' || github.event_name == 'schedule' ) && steps.current-commit-message.outputs.result || github.event.head_commit.message }} # Posts notifications when a workflow fails. failure: name: Failure notifications - runs-on: ubuntu-latest + permissions: {} + runs-on: ubuntu-24.04 + timeout-minutes: 20 needs: [ prepare ] - if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'failure' || inputs.calling_status == 'failure' || failure() }} + if: ${{ needs.prepare.outputs.previous_conclusion != 'first-failure' && inputs.calling_status == 'failure' || failure() }} steps: - name: Post failure notifications to Slack - uses: slackapi/slack-github-action@d5d276d7ae0f38f29322b80da9baf985cc80f8b1 # v1.15.0 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: + webhook-type: webhook-trigger + webhook: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} # Posts notifications the first time a workflow run succeeds after previously failing. fixed: name: Fixed notifications - runs-on: ubuntu-latest + permissions: {} + runs-on: ubuntu-24.04 + timeout-minutes: 20 needs: [ prepare ] - if: ${{ needs.prepare.outputs.previous_conclusion == 'failure' && ( github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' || inputs.calling_status == 'success' ) && success() }} + if: ${{ contains( fromJson( '["failure", "cancelled", "none"]' ), needs.prepare.outputs.previous_conclusion ) && inputs.calling_status == 'success' && success() }} steps: - name: Post failure notifications to Slack - uses: slackapi/slack-github-action@d5d276d7ae0f38f29322b80da9baf985cc80f8b1 # v1.15.0 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: + webhook-type: webhook-trigger + webhook: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} # Posts notifications when a workflow is successful. success: name: Success notifications - runs-on: ubuntu-latest + permissions: {} + runs-on: ubuntu-24.04 + timeout-minutes: 20 needs: [ prepare ] - if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'success' || inputs.calling_status == 'success' && success() }} + if: ${{ inputs.calling_status == 'success' && success() }} steps: - name: Post success notifications to Slack - uses: slackapi/slack-github-action@d5d276d7ae0f38f29322b80da9baf985cc80f8b1 # v1.15.0 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: + webhook-type: webhook-trigger + webhook: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} # Posts notifications when a workflow is cancelled. cancelled: name: Cancelled notifications - runs-on: ubuntu-latest + permissions: {} + runs-on: ubuntu-24.04 + timeout-minutes: 20 needs: [ prepare ] - if: ${{ github.event_name == 'workflow_run' && github.event.workflow_run.conclusion == 'cancelled' || inputs.calling_status == 'cancelled' || cancelled() }} + if: ${{ inputs.calling_status == 'cancelled' || cancelled() }} steps: - name: Post cancelled notifications to Slack - uses: slackapi/slack-github-action@d5d276d7ae0f38f29322b80da9baf985cc80f8b1 # v1.15.0 + uses: slackapi/slack-github-action@af78098f536edbc4de71162a307590698245be95 # v3.0.1 with: + webhook-type: webhook-trigger + webhook: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} payload: ${{ needs.prepare.outputs.payload }} - env: - SLACK_WEBHOOK_URL: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/test-and-zip-default-themes.yml b/.github/workflows/test-and-zip-default-themes.yml new file mode 100644 index 0000000000000..6ea0f7f206809 --- /dev/null +++ b/.github/workflows/test-and-zip-default-themes.yml @@ -0,0 +1,305 @@ +name: Test Default Themes & Create ZIPs + +on: + push: + branches: + - trunk + - '3.[89]' + - '[4-9].[0-9]' + paths: + # Changing the preferred version of Node.js could affect themes with build processes. + - '.npmrc' + - '.nvmrc' + # Changes to any themes with a build script should be confirmed. + - 'src/wp-content/themes/twentynineteen/**' + - 'src/wp-content/themes/twentytwenty/**' + - 'src/wp-content/themes/twentytwentyone/**' + - 'src/wp-content/themes/twentytwentytwo/**' + - 'src/wp-content/themes/twentytwentyfive/**' + # Changes to this workflow file should always verify success. + - '.github/workflows/test-and-zip-default-themes.yml' + pull_request: + branches: + - trunk + - '3.[89]' + - '[4-9].[0-9]' + paths: + # Changing the preferred version of Node.js could affect themes with build processes. + - '.npmrc' + - '.nvmrc' + # Changes to any themes with a build script should be confirmed. + - 'src/wp-content/themes/twentynineteen/**' + - 'src/wp-content/themes/twentytwenty/**' + - 'src/wp-content/themes/twentytwentyone/**' + - 'src/wp-content/themes/twentytwentytwo/**' + - 'src/wp-content/themes/twentytwentyfive/**' + # Changes to this workflow file should always verify success. + - '.github/workflows/test-and-zip-default-themes.yml' + workflow_dispatch: + inputs: + branch: + description: 'The branch to create ZIP files from' + required: true + type: string + default: 'trunk' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Checks for zero-byte files. + # + # Occasionally, binary files such as images and fonts are added to themes incorrectly. + # This checks that all files contain contents. + # + # Performs the following steps: + # - Checks out the repository. + # - Checks for zero-byte (empty) files. + check-for-empty-files: + name: ${{ matrix.theme }} empty file check + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 10 + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + theme: [ + 'twentytwentyfive', + 'twentytwentyfour', + 'twentytwentythree', + 'twentytwentytwo', + 'twentytwentyone', + 'twentytwenty', + 'twentynineteen', + 'twentyseventeen', + 'twentysixteen', + 'twentyfifteen', + 'twentyfourteen', + 'twentythirteen', + 'twentytwelve', + 'twentyeleven', + 'twentyten' + ] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Check for zero-byte (empty) files + run: | + [[ ! $(find "src/wp-content/themes/${THEME}" -empty) ]] + env: + THEME: ${{ matrix.theme }} + + # Tests the build script for themes that have one. + # + # Performs the following steps: + # - Checks out the repository. + # - Sets up Node.js. + # - Installs npm dependencies. + # - Runs the theme build script. + # - Ensures version-controlled files are not modified or deleted. + test-build-scripts: + name: Test ${{ matrix.theme }} build script + runs-on: ubuntu-24.04 + permissions: + contents: read + timeout-minutes: 10 + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + theme: [ + 'twentytwentyfive', + 'twentytwentytwo', + 'twentytwentyone', + 'twentytwenty', + 'twentynineteen', + ] + + defaults: + run: + working-directory: src/wp-content/themes/${{ matrix.theme }} + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json + + - name: Install npm dependencies + run: npm ci + + - name: Build theme + run: npm run build + + - name: Check for changes to versioned files + id: built-file-check + if: ${{ github.event_name == 'pull_request' }} + run: | + if git diff --quiet; then + echo "uncommitted_changes=false" >> "$GITHUB_OUTPUT" + else + echo "uncommitted_changes=true" >> "$GITHUB_OUTPUT" + fi + + - name: Display changes to versioned files + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + run: git diff + + - name: Save diff to a file + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + run: git diff > ./changes.diff + + # Uploads the diff file as an artifact. + - name: Upload diff file as artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + if: ${{ steps.built-file-check.outputs.uncommitted_changes == 'true' }} + with: + name: pr-built-file-changes + path: src/wp-content/themes/${{ matrix.theme }}/changes.diff + + - name: Ensure version-controlled files are not modified or deleted + run: git diff --exit-code + + # Prepares bundled themes for release. + # + # Performs the following steps: + # - Checks out the repository. + # - Uploads the theme files as a workflow artifact (files uploaded as an artifact are automatically zipped). + bundle-theme: + name: Create ${{ matrix.theme }} ZIP file + runs-on: ubuntu-24.04 + permissions: + contents: read + needs: [ check-for-empty-files, test-build-scripts ] + timeout-minutes: 10 + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + strategy: + fail-fast: false + matrix: + theme: [ + 'twentytwentyfive', + 'twentytwentyfour', + 'twentytwentythree', + 'twentytwentytwo', + 'twentytwentyone', + 'twentytwenty', + 'twentynineteen', + 'twentyseventeen', + 'twentysixteen', + 'twentyfifteen', + 'twentyfourteen', + 'twentythirteen', + 'twentytwelve', + 'twentyeleven', + 'twentyten' + ] + + steps: + - name: Checkout repository + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + with: + ref: ${{ github.event_name == 'workflow_dispatch' && inputs.branch || github.ref }} + show-progress: ${{ runner.debug == '1' && 'true' || 'false' }} + persist-credentials: false + + - name: Set up Node.js for themes needing minification + if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' + uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 + with: + node-version-file: '.nvmrc' + cache: npm + cache-dependency-path: src/wp-content/themes/${{ matrix.theme }}/package-lock.json + + - name: Install npm dependencies + if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' + run: npm ci + working-directory: src/wp-content/themes/${{ matrix.theme }} + + - name: Build theme assets + if: matrix.theme == 'twentytwentytwo' || matrix.theme == 'twentytwentyfive' + run: npm run build + working-directory: src/wp-content/themes/${{ matrix.theme }} + + - name: Upload theme ZIP as an artifact + uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 + with: + name: ${{ matrix.theme }} + path: | + src/wp-content/themes/${{ matrix.theme }} + !src/wp-content/themes/${{ matrix.theme }}/node_modules + if-no-files-found: error + include-hidden-files: true + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ check-for-empty-files, bundle-theme, test-build-scripts ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/test-build-processes.yml b/.github/workflows/test-build-processes.yml new file mode 100644 index 0000000000000..184f85a323993 --- /dev/null +++ b/.github/workflows/test-build-processes.yml @@ -0,0 +1,149 @@ +name: Test Build Processes + +on: + push: + branches: + - trunk + - '3.[7-9]' + - '[4-9].[0-9]' + tags: + - '[0-9]+.[0-9]' + - '[0-9]+.[0-9].[0-9]+' + pull_request: + branches: + - trunk + - '3.[7-9]' + - '[4-9].[0-9]' + paths: + # Any change to a PHP, CSS, JavaScript, or JSON file should run checks. + - '**.css' + - '**.js' + - '**.json' + - '**.php' + # These files configure npm and the task runner. Changes could affect the outcome. + - 'package*.json' + - '.npmrc' + - '.nvmrc' + - 'Gruntfile.js' + - 'webpack.config.js' + - 'tools/gutenberg/**' + - 'tools/vendors/**' + - 'tools/webpack/**' + # These files configure Composer. Changes could affect the outcome. + - 'composer.*' + # Confirm any changes to relevant workflow files. + - '.github/workflows/test-build-processes.yml' + - '.github/workflows/reusable-test-core-build-process.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Tests the WordPress Core build process. + test-core-build-process: + name: Core running from ${{ matrix.directory }} + uses: ./.github/workflows/reusable-test-core-build-process.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + directory: [ 'src', 'build' ] + test-certificates: [ true ] + include: + # Only prepare artifacts for Playground once. + - os: 'ubuntu-24.04' + directory: 'build' + save-build: true + prepare-playground: ${{ github.event_name == 'pull_request' && true || '' }} + with: + os: ${{ matrix.os }} + directory: ${{ matrix.directory }} + test-certificates: ${{ matrix.test-certificates && true || false }} + save-build: ${{ matrix.save-build && matrix.save-build || false }} + prepare-playground: ${{ matrix.prepare-playground && matrix.prepare-playground || false }} + + # Tests the WordPress Core build process on additional operating systems. + # + # This is separate from the job above in order to use stricter conditions when determining when to test additional + # operating systems. This avoids unintentionally consuming excessive minutes. Windows-based jobs consume minutes at a + # 2x rate, and MacOS-based jobs at a 10x rate. + # See https://docs.github.com/en/billing/concepts/product-billing/github-actions#per-minute-rates. + # + # The `matrix` and `runner` contexts are not available for use within `if` expressions. So there is + # currently no way to determine the OS being used on a given job. + # See https://docs.github.com/en/actions/learn-github-actions/contexts#context-availability. + test-core-build-process-additional-os: + name: Core running from ${{ matrix.directory }} + uses: ./.github/workflows/reusable-test-core-build-process.yml + permissions: + contents: read + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + strategy: + fail-fast: false + matrix: + os: [ 'macos-15', 'windows-2025' ] + directory: [ 'src', 'build' ] + with: + os: ${{ matrix.os }} + directory: ${{ matrix.directory }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ test-core-build-process, test-core-build-process-additional-os ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/test-coverage.yml b/.github/workflows/test-coverage.yml index b11ae0348eb66..f2b0afce3256f 100644 --- a/.github/workflows/test-coverage.yml +++ b/.github/workflows/test-coverage.yml @@ -4,10 +4,20 @@ on: # Verify push: branches: - - master - trunk paths: - '.github/workflows/test-coverage.yml' + - '.github/workflows/reusable-phpunit-tests-v3.yml' + - 'docker-compose.yml' + - 'phpunit.xml.dist' + - 'tests/phpunit/multisite.xml' + pull_request: + branches: + - trunk + paths: + - '.github/workflows/test-coverage.yml' + - '.github/workflows/reusable-phpunit-tests-v3.yml' + - 'docker-compose.yml' - 'phpunit.xml.dist' - 'tests/phpunit/multisite.xml' # Once daily at 00:00 UTC. @@ -16,168 +26,90 @@ on: # Allow manually triggering the workflow. workflow_dispatch: +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: ${{ true }} - LOCAL_PHP: '7.4-fpm' LOCAL_PHP_XDEBUG: true - LOCAL_PHP_MEMCACHED: ${{ false }} + LOCAL_PHP_XDEBUG_MODE: 'coverage' + LOCAL_PHP_MEMCACHED: false + PUPPETEER_SKIP_DOWNLOAD: true jobs: - # Sets up WordPress for testing or development use. # - # Performs the following steps: - # - Set environment variables. - # - Checks out the repository. - # - Checks out the WordPress Importer plugin (needed for the Core PHPUnit tests). - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Logs Docker debug information (about the Docker installation within the runner). - # - Starts the WordPress Docker container. - # - Logs debug general information. - # - Logs the running Docker containers. - # - Logs WordPress Docker container debug information. - # - Logs debug information about what's installed within the WordPress Docker containers. - # - Install WordPress within the Docker container. - # - Run the PHPUnit tests as a single site. - # - Ensures version-controlled files are not modified or deleted. - # - Upload the single site code coverage report to Codecov.io. - # - Run the PHPUnit tests as a multisite. - # - Ensures version-controlled files are not modified or deleted. - # - Upload the multisite code coverage report to Codecov.io. + # Creates a PHPUnit test jobs for generating code coverage reports. + # test-coverage-report: name: ${{ matrix.multisite && 'Multisite' || 'Single site' }} report - runs-on: ubuntu-latest + uses: ./.github/workflows/reusable-phpunit-tests-v3.yml + permissions: + contents: read if: ${{ github.repository == 'WordPress/wordpress-develop' }} strategy: fail-fast: false matrix: multisite: [ false, true ] - - steps: - - name: Configure environment variables - run: | - echo "PHP_FPM_UID=$(id -u)" >> $GITHUB_ENV - echo "PHP_FPM_GID=$(id -g)" >> $GITHUB_ENV - - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - echo "$GITHUB_REF" - echo "$GITHUB_EVENT_NAME" - npm --version - node --version - curl --version - git --version - svn --version - php --version - php -i - locale -a - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Install Dependencies - run: npm ci - - # This date is used to ensure that the Composer cache is refreshed at least once every week. - # http://man7.org/linux/man-pages/man1/date.1.html - - name: "Get last Monday's date" - id: get-date - run: echo "::set-output name=date::$(/bin/date -u --date='last Mon' "+%F")" - - - name: Get Composer cache directory - id: composer-cache - run: echo "::set-output name=dir::$(composer config cache-files-dir)" - - - name: Cache Composer dependencies - uses: actions/cache@26968a09c0ea4f3e233fdddbafd1166051a095f6 # v2.1.4 - env: - cache-name: cache-composer-dependencies - with: - path: ${{ steps.composer-cache.outputs.dir }} - key: ${{ runner.os }}-php-${{ matrix.php }}-date-${{ steps.get-date.outputs.date }}-composer-${{ hashFiles('**/composer.json') }} - - - name: Install Composer dependencies - run: | - docker-compose run --rm php composer --version - - # Install using `composer update` as there is no `composer.lock` file. - docker-compose run --rm php composer update - - - name: Docker debug information - run: | - docker -v - docker-compose -v - - - name: Start Docker environment - run: | - npm run env:start - - - name: General debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - - - name: Log running Docker containers - run: docker ps -a - - - name: WordPress Docker container debug information - run: | - docker-compose run --rm mysql mysql --version - docker-compose run --rm php php --version - docker-compose run --rm php php -m - docker-compose run --rm php php -i - docker-compose run --rm php locale -a - - - name: Install WordPress - run: npm run env:install - - - name: Run tests as a single site - if: ${{ ! matrix.multisite }} - run: npm run test:php -- --verbose -c phpunit.xml.dist --coverage-clover wp-code-coverage-single-clover-${{ github.sha }}.xml - - - name: Ensure version-controlled files are not modified during the tests - run: git diff --exit-code - - - name: Upload single site report to Codecov - if: ${{ ! matrix.multisite }} - uses: codecov/codecov-action@e156083f13aff6830c92fc5faa23505779fbf649 # v1.2.1 - with: - file: wp-code-coverage-single-clover-${{ github.sha }}.xml - flags: single,php - - - name: Run tests as a multisite install - if: ${{ matrix.multisite }} - run: npm run test:php -- --verbose -c tests/phpunit/multisite.xml --coverage-clover wp-code-coverage-multisite-clover-${{ github.sha }}.xml - - - name: Ensure version-controlled files are not modified during the tests - run: git diff --exit-code - - - name: Upload multisite report to Codecov - if: ${{ matrix.multisite }} - uses: codecov/codecov-action@e156083f13aff6830c92fc5faa23505779fbf649 # v1.2.1 - with: - file: wp-code-coverage-multisite-clover-${{ github.sha }}.xml - flags: multisite,php + coverage-report: [ true ] + with: + php: '8.3' + multisite: ${{ matrix.multisite }} + coverage-report: ${{ matrix.coverage-report }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ test-coverage-report ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.test-coverage-report.result == 'success' && 'success' || needs.test-coverage-report.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/test-npm.yml b/.github/workflows/test-npm.yml deleted file mode 100644 index 0b49e9ab7cb83..0000000000000 --- a/.github/workflows/test-npm.yml +++ /dev/null @@ -1,169 +0,0 @@ -name: Test NPM - -on: - push: - branches: - - master - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - pull_request: - branches: - - master - - trunk - - '3.[7-9]' - - '[4-9].[0-9]' - paths: - # These files configure NPM. Changes could affect the outcome. - - 'package*.json' - # JavaScript files are built using NPM. - - '**.js' - # CSS and SCSS files are built using NPM. - - '**.scss' - - '**.css' - # Changes to workflow files should always verify all workflows are successful. - - '.github/workflows/**.yml' - workflow_dispatch: - -# Cancels all previous workflow runs for pull requests that have not completed. -concurrency: - # The concurrency group contains the workflow name and the branch name for pull requests - # or the commit hash for any other events. - group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} - cancel-in-progress: true - -env: - PUPPETEER_SKIP_CHROMIUM_DOWNLOAD: ${{ true }} - -jobs: - # Verifies that installing NPM dependencies and building WordPress works as expected. - # - # Performs the following steps: - # - Checks out the repository. - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Builds WordPress to run from the `build` directory. - # - Cleans up after building WordPress to the `build` directory. - # - Ensures version-controlled files are not modified or deleted. - # - Builds WordPress to run from the `src` directory. - # - Cleans up after building WordPress to the `src` directory. - # - Ensures version-controlled files are not modified or deleted. - test-npm: - name: Test NPM on ${{ matrix.os }} - runs-on: ${{ matrix.os }} - if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, windows-latest ] - - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Install Dependencies - run: npm ci - - - name: Build WordPress - run: npm run build - - - name: Clean after building - run: npm run grunt clean - - - name: Ensure version-controlled files are not modified or deleted during building and cleaning - run: git diff --exit-code - - - name: Build WordPress in /src - run: npm run build:dev - - - name: Clean after building in /src - run: npm run grunt clean -- --dev - - - name: Ensure version-controlled files are not modified or deleted during building and cleaning - run: git diff --exit-code - - # Verifies that installing NPM dependencies and building WordPress works as expected on MacOS. - # - # This is a separate job in order to that more strict conditions can be used. - # - # Performs the following steps: - # - Checks out the repository. - # - Logs debug information about the runner container. - # - Installs NodeJS 14. - # _ Installs NPM dependencies using install-changed to hash the `package.json` file. - # - Builds WordPress to run from the `build` directory. - # - Cleans up after building WordPress to the `build` directory. - # - Ensures version-controlled files are not modified or deleted. - # - Builds WordPress to run from the `src` directory. - # - Cleans up after building WordPress to the `src` directory. - # - Ensures version-controlled files are not modified or deleted. - test-npm-macos: - name: Test NPM on MacOS - runs-on: macos-latest - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - steps: - - name: Checkout repository - uses: actions/checkout@5a4ac9002d0be2fb38bd78e4b4dbde5606d7042f # v2.3.4 - - - name: Log debug information - run: | - npm --version - node --version - curl --version - git --version - svn --version - - - name: Install NodeJS - uses: actions/setup-node@38d90ce44d5275ad62cc48384b3d8a58c500bb5f # v2.2.2 - with: - node-version: 14 - cache: npm - - - name: Install Dependencies - run: npm ci - - - name: Build WordPress - run: npm run build - - - name: Clean after building - run: npm run grunt clean - - - name: Ensure version-controlled files are not modified or deleted during building and cleaning - run: git diff --exit-code - - - name: Build WordPress in /src - run: npm run build:dev - - - name: Clean after building in /src - run: npm run grunt clean -- --dev - - - name: Ensure version-controlled files are not modified or deleted during building and cleaning - run: git diff --exit-code - - slack-notifications: - name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master - needs: [ test-npm, test-npm-macos ] - if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} - with: - calling_status: ${{ needs.test-npm.result == 'success' && needs.test-npm-macos.result == 'success' && 'success' || ( needs.test-npm.result == 'cancelled' || needs.test-npm-macos.result == 'cancelled' ) && 'cancelled' || 'failure' }} - secrets: - SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} - SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} - SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} - SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} diff --git a/.github/workflows/test-old-branches.yml b/.github/workflows/test-old-branches.yml index 1d0bc59a7eaa5..74f9c2d43d54c 100644 --- a/.github/workflows/test-old-branches.yml +++ b/.github/workflows/test-old-branches.yml @@ -4,19 +4,36 @@ on: # Verify the workflow is successful when this file is updated. push: branches: - - master - trunk paths: - '.github/workflows/test-old-branches.yml' + - '.github/workflows/reusable-phpunit-tests-v[1-2].yml' # Run twice a month on the 1st and 15th at 00:00 UTC. schedule: - cron: '0 0 1 * *' - cron: '0 0 15 * *' + workflow_dispatch: + inputs: + strategy: + description: 'The branches to test. Accepts X.Y branch names, or "all". Defaults to only the currently supported branch.' + required: false + type: string + default: '' + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +env: + CURRENTLY_SUPPORTED_BRANCH: '7.0' jobs: dispatch-workflows-for-old-branches: name: ${{ matrix.workflow }} for ${{ matrix.branch }} - runs-on: ubuntu-latest + runs-on: ubuntu-24.04 + permissions: + actions: write + timeout-minutes: 20 if: ${{ github.repository == 'WordPress/wordpress-develop' }} strategy: fail-fast: false @@ -25,15 +42,40 @@ jobs: 'coding-standards.yml', 'javascript-tests.yml', 'phpunit-tests.yml', - 'test-npm.yml' + 'test-build-processes.yml' ] branch: [ - '5.8', '5.7', '5.6', '5.5', '5.4', '5.3', '5.2', '5.1', '5.0', - '4.9', '4.8', '4.7', '4.6', '4.5', '4.4', '4.3', '4.2', '4.1', '4.0', - '3.9', '3.8', '3.7' + '7.0', + '6.9', '6.8', '6.7', '6.6', '6.5', '6.4', '6.3', '6.2', '6.1','6.0', + '5.9', '5.8', '5.7', '5.6', '5.5', '5.4', '5.3', '5.2', '5.1', '5.0', + '4.9', '4.8', '4.7' ] include: # PHP Compatibility testing was introduced in 5.5. + - branch: '7.0' + workflow: 'php-compatibility.yml' + - branch: '6.9' + workflow: 'php-compatibility.yml' + - branch: '6.8' + workflow: 'php-compatibility.yml' + - branch: '6.7' + workflow: 'php-compatibility.yml' + - branch: '6.6' + workflow: 'php-compatibility.yml' + - branch: '6.5' + workflow: 'php-compatibility.yml' + - branch: '6.4' + workflow: 'php-compatibility.yml' + - branch: '6.3' + workflow: 'php-compatibility.yml' + - branch: '6.2' + workflow: 'php-compatibility.yml' + - branch: '6.1' + workflow: 'php-compatibility.yml' + - branch: '6.0' + workflow: 'php-compatibility.yml' + - branch: '5.9' + workflow: 'php-compatibility.yml' - branch: '5.8' workflow: 'php-compatibility.yml' - branch: '5.7' @@ -43,27 +85,54 @@ jobs: - branch: '5.5' workflow: 'php-compatibility.yml' - # End to End testing was introduced in 5.3 but later removed as there were no meaningful assertions. - # Only the officially supported major branch runs E2E tests so that more assertions can be added, and the - # workflow does not continue to run needlessly on old branches. + # End-to-end testing was introduced in 5.3 but was later removed as there were no meaningful assertions. + # Starting in 5.8 with #52905, some additional tests with real assertions were introduced. + # Branches 5.8 and newer should be tested to confirm no regressions are introduced. + - branch: '7.0' + workflow: 'end-to-end-tests.yml' + - branch: '6.9' + workflow: 'end-to-end-tests.yml' + - branch: '6.8' + workflow: 'end-to-end-tests.yml' + - branch: '6.7' + workflow: 'end-to-end-tests.yml' + - branch: '6.6' + workflow: 'end-to-end-tests.yml' + - branch: '6.5' + workflow: 'end-to-end-tests.yml' + - branch: '6.4' + workflow: 'end-to-end-tests.yml' + - branch: '6.3' + workflow: 'end-to-end-tests.yml' + - branch: '6.2' + workflow: 'end-to-end-tests.yml' + - branch: '6.1' + workflow: 'end-to-end-tests.yml' + - branch: '6.0' + workflow: 'end-to-end-tests.yml' + - branch: '5.9' + workflow: 'end-to-end-tests.yml' - branch: '5.8' workflow: 'end-to-end-tests.yml' - exclude: - # Coding standards and JavaScript testing did not take place in 3.7. - - branch: '3.7' - workflow: 'coding-standards.yml' - - branch: '3.7' - workflow: 'javascript-tests.yml' + + # Performance testing was introduced in 6.2 using Puppeteer but was overhauled to use Playwright instead in 6.4. + # Since the workflow frequently failed for 6.2 and 6.3 due to the flaky nature of the Puppeteer tests, + # the workflow was removed from those two branches. + - branch: '7.0' + workflow: 'performance.yml' + - branch: '6.9' + workflow: 'performance.yml' # Run all branches monthly, but only the currently supported one twice per month. steps: - name: Dispatch workflow run - uses: actions/github-script@47f7cf65b5ced0830a325f705cad64f2f58dddf7 # v3.1.0 - if: ${{ github.event_name == 'push' || github.event.schedule == '0 0 15 * *' || matrix.branch == '5.7' }} + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + if: ${{ github.event_name == 'push' || ( github.event_name == 'workflow_dispatch' && matrix.branch == inputs.strategy || inputs.strategy == 'all' ) || github.event.schedule == '0 0 15 * *' || matrix.branch == env.CURRENTLY_SUPPORTED_BRANCH }} with: - github-token: ${{ secrets.GHA_OLD_BRANCH_DISPATCH }} + retries: 2 + retry-exempt-status-codes: 418 script: | - github.actions.createWorkflowDispatch({ + github.rest.actions.createWorkflowDispatch({ owner: context.repo.owner, repo: context.repo.repo, workflow_id: '${{ matrix.workflow }}', @@ -72,11 +141,14 @@ jobs: slack-notifications: name: Slack Notifications - uses: WordPress/wordpress-develop/.github/workflows/slack-notifications.yml@master + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read needs: [ dispatch-workflows-for-old-branches ] if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} with: - calling_status: ${{ needs.dispatch-workflows-for-old-branches.result == 'success' && 'success' || needs.dispatch-workflows-for-old-branches.result == 'cancelled' && 'cancelled' || 'failure' }} + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} secrets: SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} diff --git a/.github/workflows/upgrade-develop-testing.yml b/.github/workflows/upgrade-develop-testing.yml new file mode 100644 index 0000000000000..5f86e170fcdae --- /dev/null +++ b/.github/workflows/upgrade-develop-testing.yml @@ -0,0 +1,162 @@ +# Confirms that updating WordPress using WP-CLI works successfully. +# +# This workflow tests upgrading from a previous version to the current wordpress-develop checkout, not to a version available on WordPress.org. +name: Upgrade Develop Version Tests + +on: + push: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + tags: + - '[0-9]+.[0-9]' + - '[0-9]+.[0-9].[0-9]+' + paths: + # Any change to a source PHP file should run checks. + - 'src/**.php' + # Confirm any changes to relevant workflow files. + - '.github/workflows/upgrade-develop-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + pull_request: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Any change to a source PHP file should run checks. + - 'src/**.php' + # Confirm any changes to relevant workflow files. + - '.github/workflows/upgrade-develop-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + # Build WordPress from the current branch ready for the upgrade tests. + build: + name: Build + uses: ./.github/workflows/reusable-build-package.yml + if: ${{ startsWith( github.repository, 'WordPress/' ) && ( github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' ) }} + permissions: + contents: read + +# Because the number of jobs spawned can quickly balloon out of control, the following methodology is applied when +# building out the matrix below: +# +# - The two most recent releases of WordPress are always tested. +# - After a branch is created, the pre-release version is also added. +# - The oldest version of WordPress receiving security updates as a courtesy that also runs on a PHP version supported by trunk +# should always be tested. +# - PHP and DB versions are kept to a minimum. In general this should be the highest and lowest supported versions of each with excludes +# being updated to keep the matrix as small as is reasonable. + upgrade-tests-develop: + name: Upgrade from ${{ matrix.wp }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + needs: [ build ] + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.4' ] + # WordPress 5.3 is the oldest version that supports PHP 7.4. + wp: [ '5.3', '6.8', '6.9', '7.0-RC3' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: develop + multisite: ${{ matrix.multisite }} + + # Run a limited set of upgrade tests for the current branch on forks. + upgrade-tests-develop-forks: + name: Upgrade from ${{ matrix.wp }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository != 'WordPress/wordpress-develop' }} + needs: [ build ] + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '8.4' ] + wp: [ '6.8', '6.9', '7.0-RC3' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: develop + multisite: ${{ matrix.multisite }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ upgrade-tests-develop ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/upgrade-testing.yml b/.github/workflows/upgrade-testing.yml new file mode 100644 index 0000000000000..1e54a946c03eb --- /dev/null +++ b/.github/workflows/upgrade-testing.yml @@ -0,0 +1,241 @@ +# Confirms that updating WordPress using WP-CLI works successfully. +# +# This workflow is not meant to test wordpress-develop checkouts, but rather tagged versions officially available on WordPress.org. +name: Upgrade Tests + +on: + push: + branches: + - trunk + # Always test the workflow after it's updated. + paths: + - '.github/workflows/upgrade-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + pull_request: + # This workflow is only meant to run from trunk. Pull requests changing this file with different BASE branches should be ignored. + branches: + - trunk + # Always test the workflow when changes are suggested. + paths: + - '.github/workflows/upgrade-testing.yml' + - '.github/workflows/reusable-upgrade-testing.yml' + workflow_dispatch: + inputs: + new-version: + description: 'The version to test installing. Accepts major and minor versions, "latest", or "nightly". Major releases must not end with ".0".' + type: string + default: 'latest' + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ inputs.new-version || github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +# Because the number of jobs spawned can quickly balloon out of control, the following methodology is applied when +# building out the matrix below: +# +# - The two most recent releases of WordPress are tested against all PHP/MySQL LTS version combinations and the +# most recent innovation release. +# - The next 6 oldest versions of WordPress are tested against both the oldest and newest releases of PHP currently +# supported for both PHP 7 & 8 along with the oldest and newest MySQL LTS versions currently supported (no innovation +# releases). At the current 3 releases per year pace, this accounts for 2 additional years worth of releases. +# - Of the remaining versions of WordPress still receiving security updates, only test the ones where the database +# version was updated since the previous major release. +# - The oldest version of WordPress receiving security updates should always be tested against the same combinations as +# detailed for the two most recent releases. + +# Notes about chosen MySQL versions: +# - Only the most recent innovation release should be included in testing. +# - Even though MySQL >= 5.5.5 is currently supported, there are no 5.5.x Docker containers available that work on +# modern architectures. +# - 5.6.x Docker containers are available and work, but 5.6 only accounts for ~2.3% of installs as of 12/6/2024.defaults: +# - 5.7.x accounts for ~20% of installs, so this is used below instead. +jobs: + # Tests the full list of PHP/MySQL combinations for the two most recent versions of WordPress. + upgrade-tests-recent-releases: + name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4', '8.0', '8.1', '8.2', '8.3', '8.4', '8.5' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.0', '8.4', '9.6' ] + wp: [ '6.8', '6.9', '7.0-RC3' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} + multisite: ${{ matrix.multisite }} + + # Tests 6.x releases where the WordPress database version changed on the oldest and newest supported versions of PHP 7 & 8. + upgrade-tests-wp-6x-mysql: + name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4', '8.0', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.4' ] + wp: [ '6.0', '6.3', '6.4', '6.5' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} + multisite: ${{ matrix.multisite }} + + # Tests 5.x releases where the WordPress database version changed on the only supported version of PHP 7. + upgrade-tests-wp-5x-php-7x-mysql: + name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.4' ] + wp: [ '5.0', '5.1', '5.3', '5.4', '5.5', '5.6', '5.9' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} + multisite: ${{ matrix.multisite }} + + # Tests 5.x releases where the WordPress database version changed on the oldest and newest supported versions of PHP 8. + # + # WordPress 5.0-5.2 are excluded from PHP 8+ testing because of the following fatal errors: + # - Use of __autoload(). + # - array/string offset with curly braces. + upgrade-tests-wp-5x-php-8x-mysql: + name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '8.0', '8.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.4' ] + wp: [ '5.3', '5.4', '5.5', '5.6', '5.9' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} + multisite: ${{ matrix.multisite }} + + # The oldest version of WordPress receiving security updates should always be tested against + # the widest possible list of PHP/MySQL combinations. + # + # WordPress 4.7 is excluded from PHP 8+ testing because of the following fatal errors: + # - Use of __autoload(). + # - array/string offset with curly braces. + upgrade-tests-oldest-wp-mysql: + name: ${{ matrix.wp }} to ${{ inputs.new-version && inputs.new-version || 'latest' }} + uses: ./.github/workflows/reusable-upgrade-testing.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' }} + permissions: + contents: read + strategy: + fail-fast: false + matrix: + os: [ 'ubuntu-24.04' ] + php: [ '7.4' ] + db-type: [ 'mysql' ] + db-version: [ '5.7', '8.0', '8.4', '9.6' ] + wp: [ '4.7' ] + multisite: [ false, true ] + with: + os: ${{ matrix.os }} + php: ${{ matrix.php }} + db-type: ${{ matrix.db-type }} + db-version: ${{ matrix.db-version }} + wp: ${{ matrix.wp }} + new-version: ${{ inputs.new-version && inputs.new-version || 'latest' }} + multisite: ${{ matrix.multisite }} + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ upgrade-tests-recent-releases, upgrade-tests-wp-6x-mysql, upgrade-tests-wp-5x-php-7x-mysql, upgrade-tests-wp-5x-php-8x-mysql, upgrade-tests-oldest-wp-mysql ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.github/workflows/welcome-new-contributors.yml b/.github/workflows/welcome-new-contributors.yml deleted file mode 100644 index f475a432a0b86..0000000000000 --- a/.github/workflows/welcome-new-contributors.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Welcome New Contributors - -on: - pull_request_target: - types: [ opened ] - -jobs: - # Comments on a pull request when the author is a new contributor. - post-welcome-message: - runs-on: ubuntu-latest - if: ${{ github.repository == 'WordPress/wordpress-develop' }} - - steps: - - uses: bubkoo/welcome-action@8dbbac2540d155744c90e4e37da6b05ffc9c5e2c # v1.0.3 - with: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - FIRST_PR_COMMENT: > - Hi @{{ author }}! 👋 - - - Thank you for your contribution to WordPress! 💖 - - - It looks like this is your first pull request to `wordpress-develop`. Here are a few things to be aware of that may help you out! - - - **No one monitors this repository for new pull requests.** Pull requests **must** be attached to a Trac ticket to be considered for inclusion in WordPress Core. To attach a pull request to a Trac ticket, please include the ticket's full URL in your pull request description. - - - **Pull requests are never merged on GitHub.** The WordPress codebase continues to be managed through the SVN repository that this GitHub repository mirrors. Please feel free to open pull requests to work on any contribution you are making. - - - More information about how GitHub pull requests can be used to contribute to WordPress can be found in [this blog post](https://make.wordpress.org/core/2020/02/21/working-on-trac-tickets-using-github-pull-requests/). - - - **Please include automated tests.** Including tests in your pull request is one way to help your patch be considered faster. To learn about WordPress' test suites, visit the [Automated Testing](https://make.wordpress.org/core/handbook/testing/automated-testing/) page in the handbook. - - - If you have not had a chance, please review the [Contribute with Code page](https://make.wordpress.org/core/handbook/contribute/) in the [WordPress Core Handbook](https://make.wordpress.org/core/handbook/). - - - The [Developer Hub](https://developer.wordpress.org/) also documents the various [coding standards](https://make.wordpress.org/core/handbook/best-practices/coding-standards/) that are followed: - - - [PHP Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/php/) - - - [CSS Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/css/) - - - [HTML Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/html/) - - - [JavaScript Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/javascript/) - - - [Accessibility Coding Standards](https://developer.wordpress.org/coding-standards/wordpress-coding-standards/accessibility/) - - - [Inline Documentation Standards](https://developer.wordpress.org/coding-standards/inline-documentation-standards/) - - - Thank you, - - The WordPress Project diff --git a/.github/workflows/workflow-lint.yml b/.github/workflows/workflow-lint.yml new file mode 100644 index 0000000000000..0aae098543e21 --- /dev/null +++ b/.github/workflows/workflow-lint.yml @@ -0,0 +1,89 @@ +name: Lint GitHub Actions workflow files + +on: + push: + branches: + - trunk + - '6.[8-9]' + - '[7-9].[0-9]' + paths: + # Only run when changes are made to workflow files. + - '.github/workflows/**' + pull_request: + branches: + - trunk + - '[0-9].[0-9]' + paths: + # Only run when changes are made to workflow files. + - '.github/workflows/**' + workflow_dispatch: + +# Cancels all previous workflow runs for pull requests that have not completed. +concurrency: + # The concurrency group contains the workflow name and the branch name for pull requests + # or the commit hash for any other events. + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.head_ref || github.sha }} + cancel-in-progress: true + +# Disable permissions for all available scopes by default. +# Any needed permissions should be configured at the job level. +permissions: {} + +jobs: + lint: + name: Lint GitHub Action files + uses: ./.github/workflows/reusable-workflow-lint.yml + if: ${{ github.repository == 'WordPress/wordpress-develop' || github.event_name == 'pull_request' }} + permissions: + security-events: write + actions: read + contents: read + + slack-notifications: + name: Slack Notifications + uses: ./.github/workflows/slack-notifications.yml + permissions: + actions: read + contents: read + needs: [ lint ] + if: ${{ github.repository == 'WordPress/wordpress-develop' && github.event_name != 'pull_request' && always() }} + with: + calling_status: ${{ contains( needs.*.result, 'cancelled' ) && 'cancelled' || contains( needs.*.result, 'failure' ) && 'failure' || 'success' }} + secrets: + SLACK_GHA_SUCCESS_WEBHOOK: ${{ secrets.SLACK_GHA_SUCCESS_WEBHOOK }} + SLACK_GHA_CANCELLED_WEBHOOK: ${{ secrets.SLACK_GHA_CANCELLED_WEBHOOK }} + SLACK_GHA_FIXED_WEBHOOK: ${{ secrets.SLACK_GHA_FIXED_WEBHOOK }} + SLACK_GHA_FAILURE_WEBHOOK: ${{ secrets.SLACK_GHA_FAILURE_WEBHOOK }} + + failed-workflow: + name: Failed workflow tasks + runs-on: ubuntu-24.04 + permissions: + actions: write + needs: [ slack-notifications ] + if: | + always() && + github.repository == 'WordPress/wordpress-develop' && + github.event_name != 'pull_request' && + github.run_attempt < 2 && + ( + contains( needs.*.result, 'cancelled' ) || + contains( needs.*.result, 'failure' ) + ) + + steps: + - name: Dispatch workflow run + uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0 + with: + retries: 2 + retry-exempt-status-codes: 418 + script: | + github.rest.actions.createWorkflowDispatch({ + owner: context.repo.owner, + repo: context.repo.repo, + workflow_id: 'failed-workflow.yml', + ref: 'trunk', + inputs: { + run_id: `${context.runId}`, + } + }); diff --git a/.gitignore b/.gitignore index fbae7cad2715d..15876fa47fee8 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ # gitignore file for WordPress Core # Configuration files with possibly sensitive information +.env wp-config.php wp-tests-config.php .htaccess @@ -10,15 +11,20 @@ wp-tests-config.php /phpunit.xml /.phpcs.xml /phpcs.xml +.cache/* /tests/phpunit/data/plugins/wordpress-importer /tests/phpunit/data/.trac-ticket-cache* /tests/qunit/compiled.html +/tests/performance/**/*.test.results.json /src/.wp-tests-version /node_modules /npm-debug.log /build +/gutenberg /tests/phpunit/build /wp-cli.local.yml +/phpstan.neon +/*.tsbuildinfo /jsdoc /composer.lock /vendor @@ -26,6 +32,10 @@ wp-tests-config.php /src/wp-admin/css/*-rtl.css /src/wp-admin/css/colors/*/*.css /src/wp-admin/js +/src/wp-includes/assets/* +!/src/wp-includes/assets/icon-library-manifest.php +!/src/wp-includes/assets/script-loader-packages.php +!/src/wp-includes/assets/script-modules-packages.php /src/wp-includes/js /src/wp-includes/css/dist /src/wp-includes/css/*.min.css @@ -34,10 +44,15 @@ wp-tests-config.php /src/wp-includes/blocks/**/*.js /src/wp-includes/blocks/**/*.js.map /packagehash.txt +/.gutenberg-hash /artifacts +/setup.log +/coverage # Files and folders that get created in wp-content /src/wp-content/blogs.dir +/src/wp-content/database +/src/wp-content/fonts /src/wp-content/languages /src/wp-content/mu-plugins /src/wp-content/plugins @@ -53,6 +68,10 @@ wp-tests-config.php !/src/wp-content/themes/twentynineteen !/src/wp-content/themes/twentytwenty !/src/wp-content/themes/twentytwentyone +!/src/wp-content/themes/twentytwentytwo +!/src/wp-content/themes/twentytwentythree +!/src/wp-content/themes/twentytwentyfour +!/src/wp-content/themes/twentytwentyfive /src/wp-content/upgrade /src/wp-content/uploads /src/wp-content/advanced-cache.php @@ -73,6 +92,12 @@ wp-tests-config.php /src/wp-content/themes/twentynineteen/node_modules /src/wp-content/themes/twentytwentyone/node_modules /src/wp-content/themes/twentytwenty/node_modules +/src/wp-content/themes/twentytwentytwo/node_modules +/src/wp-content/themes/twentytwentyfive/node_modules + +# Minified files in bundled themes +/src/wp-content/themes/twentytwentytwo/*.min.css +/src/wp-content/themes/twentytwentyfive/*.min.css # Operating system specific files .DS_Store @@ -92,4 +117,4 @@ wp-tests-config.php /docker-compose.override.yml # Visual regression test diffs -tests/visual-regression/specs/__image_snapshots__ \ No newline at end of file +tests/visual-regression/specs/__snapshots__ diff --git a/.jshintrc b/.jshintrc index 20dadcd1fa305..2f1dae4e44dc9 100644 --- a/.jshintrc +++ b/.jshintrc @@ -3,14 +3,12 @@ "curly": true, "eqeqeq": true, "eqnull": true, - "esversion": 3, + "esversion": 11, "expr": true, "immed": true, "noarg": true, "nonbsp": true, - "onevar": true, "quotmark": "single", - "trailing": true, "undef": true, "unused": true, @@ -24,6 +22,10 @@ "wp": false, "export": false, "module": false, - "require": false + "require": false, + "WorkerGlobalScope": false, + "self": false, + "OffscreenCanvas": false, + "Promise": false } } diff --git a/.mailmap b/.mailmap new file mode 100644 index 0000000000000..bbcf17b3ec9d4 --- /dev/null +++ b/.mailmap @@ -0,0 +1,129 @@ +# Aliases names and emails inside of commits. +# See https://git-scm.com/docs/gitmailmap +# +# Some entries appear as duplicates, but are both required to correct +# identities with just the wrong name as well as those with the wrong +# name and also the wrong email address. + +# Accounts with a display name. +Aaron D. Campbell +Aaron Jorbin +Adam Silverstein +Adam Zieliński +Adam Zieliński +Alex King +Alex Shiels +André +André +Andrea Fercia +Andrew Duthie +Andrew Nacin +Andrew Ozz +Anthony Burchell +Anton Timmermans +Bernie Reiter +Bernie Reiter +Boone Gorges +Carlos Bravo +Colin Stewart +Daniel Bachhuber +Daniel Richards +Daryl Koopersmith +David A. Kennedy +David Baumwald +Dennis Snell +Dion Hulse +Dominik Schilling +Dominik Schilling +Donncha O Caoimh +Dougal Campbell +Drew Jaynes +Drew Jaynes +Drew Jaynes +Ella +Ella +Ella +Ella +Eric Andrew Lewis +Felix Arntz +Gary Pendergast +Greg Ziółkowski +Greg Ziółkowski +Helen Hou-Sandi +Ian Belanger +Ian Dunn +Ian Stewart +Isabel Brison +Jake Spurlock +James Nylen +Jb Audras +Jeff Ong +Jeremy Felt +Joe Dolson +Joe Hoyle +Joe McGill +John Blackbourn +John James Jacoby +Jon Cave +Jonathan Desrosiers +Jonny Harris +Jorge Costa +Joseph Scott +Juliette Reinders Folmer +Juliette Reinders Folmer +K. Adam White +Kelly Choyce-Dwan +Kelly Choyce-Dwan +Kira Schroder +Kira Schroder +Konstantin Kovshenin +Konstantin Obenland +Konstantin Obenland +Lance Willett +Marius L. J +Mark Jaquith +Matias Ventura +Matt Mullenweg +Matt Thomas +Mel Choyce +Michael Adams +Michael Adams +Michael Arestad +Michal Czaplinski +Miguel Fonseca +Mike Little +Nikolay Bachiyski +Omar Reiss +Pascal Birchler +Pete Mall +Peter Westwood +Peter Wilson +Rachel Baker +Riad Benguella +Robert Anderson +Ron Rennick +Ryan Boren +Ryan McCue +Scott Taylor +Sergey Biryukov +Sergey Biryukov +Tammie Lister +Tammie Lister +Timothy Jacobs +Timothy Jacobs +Tonya Mork +Tonya Mork +Weston Ruter + +# Accounts without a corresponding display name. +allancole +bumpbot +jverber +laurelfulford +luisherranz +michelvaldrighi +potbot +ramonopoly +rob1n +scribu +zieladam diff --git a/.npmrc b/.npmrc index 1dab4ed4c3020..01593aba1c0ef 100644 --- a/.npmrc +++ b/.npmrc @@ -1 +1,5 @@ +engine-strict = true +legacy-peer-deps = true +lockfile-version = 3 +prefer-dedupe = true save-exact = true diff --git a/.nvmrc b/.nvmrc index b009dfb9d9f98..209e3ef4b6247 100644 --- a/.nvmrc +++ b/.nvmrc @@ -1 +1 @@ -lts/* +20 diff --git a/.prettierrc.js b/.prettierrc.js new file mode 100644 index 0000000000000..51b8aeb41505a --- /dev/null +++ b/.prettierrc.js @@ -0,0 +1,3 @@ +// Import the default config file and expose it in the project root. +// Useful for editor integrations. +module.exports = require( '@wordpress/prettier-config' ); diff --git a/.version-support-mysql.json b/.version-support-mysql.json new file mode 100644 index 0000000000000..6e81f2eff0f09 --- /dev/null +++ b/.version-support-mysql.json @@ -0,0 +1,215 @@ +{ + "7-1": [ + "9.6", + "9.5", + "9.4", + "9.3", + "9.2", + "9.1", + "9.0", + "8.4", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "7-0": [ + "9.6", + "9.5", + "9.4", + "9.3", + "9.2", + "9.1", + "9.0", + "8.4", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-9": [ + "9.4", + "9.3", + "9.2", + "9.1", + "9.0", + "8.4", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-8": [ + "9.1", + "9.0", + "8.4", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-7": [ + "8.4", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-6": [ + "8.3", + "8.2", + "8.1", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-5": [ + "8.3", + "8.2", + "8.1", + "8.0", + "5.7", + "5.6", + "5.5" + ], + "6-4": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "6-3": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "6-2": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "6-1": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "6-0": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-9": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-8": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-7": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-6": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-5": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-4": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-3": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-2": [ + "8.0", + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-1": [ + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "5-0": [ + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "4-9": [ + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "4-8": [ + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ], + "4-7": [ + "5.7", + "5.6", + "5.5", + "5.1", + "5.0" + ] +} diff --git a/.version-support-php.json b/.version-support-php.json new file mode 100644 index 0000000000000..de510694c65c9 --- /dev/null +++ b/.version-support-php.json @@ -0,0 +1,242 @@ +{ + "7-1": [ + "7.4", + "8.0", + "8.1", + "8.2", + "8.3", + "8.4", + "8.5" + ], + "7-0": [ + "7.4", + "8.0", + "8.1", + "8.2", + "8.3", + "8.4", + "8.5" + ], + "6-9": [ + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3", + "8.4", + "8.5" + ], + "6-8": [ + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3", + "8.4" + ], + "6-7": [ + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3", + "8.4" + ], + "6-6": [ + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3" + ], + "6-5": [ + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3" + ], + "6-4": [ + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2", + "8.3" + ], + "6-3": [ + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2" + ], + "6-2": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2" + ], + "6-1": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1", + "8.2" + ], + "6-0": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1" + ], + "5-9": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0", + "8.1" + ], + "5-8": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0" + ], + "5-7": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0" + ], + "5-6": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4", + "8.0" + ], + "5-5": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4" + ], + "5-4": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4" + ], + "5-3": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3", + "7.4" + ], + "5-2": [ + "5.6", + "7.0", + "7.1", + "7.2", + "7.3" + ], + "5-1": [ + "5.2", + "5.3", + "5.4", + "5.5", + "5.6", + "7.0", + "7.1", + "7.2", + "7.3" + ], + "5-0": [ + "5.2", + "5.3", + "5.4", + "5.5", + "5.6", + "7.0", + "7.1", + "7.2", + "7.3" + ], + "4-9": [ + "5.2", + "5.3", + "5.4", + "5.5", + "5.6", + "7.0", + "7.1", + "7.2" + ], + "4-8": [ + "5.2", + "5.3", + "5.4", + "5.5", + "5.6", + "7.0", + "7.1" + ], + "4-7": [ + "5.2", + "5.3", + "5.4", + "5.5", + "5.6", + "7.0", + "7.1" + ] +} diff --git a/Gruntfile.js b/Gruntfile.js index 369079dc1db22..8863d030627b8 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -1,5 +1,5 @@ /* jshint node:true */ -/* jshint esversion: 6 */ +/* eslint-env es6 */ /* globals Set */ var webpackConfig = require( './webpack.config' ); var installChanged = require( 'install-changed' ); @@ -13,7 +13,7 @@ module.exports = function(grunt) { SOURCE_DIR = 'src/', BUILD_DIR = 'build/', WORKING_DIR = grunt.option( 'dev' ) ? SOURCE_DIR : BUILD_DIR, - BANNER_TEXT = '/*! This file is auto-generated */', + BANNER_TEXT = '/*! This file is auto-generated */', autoprefixer = require( 'autoprefixer' ), sass = require( 'sass' ), phpUnitWatchGroup = grunt.option( 'group' ), @@ -31,6 +31,63 @@ module.exports = function(grunt) { 'wp-content/plugins/akismet/**', '!wp-content/themes/twenty*/node_modules/**', ], + + // All built CSS files, in /src or /build. + cssFiles = [ + 'wp-admin/css/*.min.css', + 'wp-admin/css/*-rtl*.css', + 'wp-includes/css/*.min.css', + 'wp-includes/css/*-rtl*.css', + 'wp-admin/css/colors/**/*.css', + ], + + // Built js files, in /src or /build. + jsFiles = [ + 'wp-admin/js/', + 'wp-includes/js/', + ], + + // All files copied from the Gutenberg repository excluded from version control. + gutenbergFiles = [ + 'wp-includes/js/dist', + 'wp-includes/css/dist', + // Old location kept temporarily to ensure they are cleaned up. + 'wp-includes/icons', + ], + + // All files built by Webpack, in /src or /build. + // Webpack only builds Core-specific media files and development scripts. + // Blocks, packages, script modules, and vendors come from the Gutenberg build. + webpackFiles = [ + 'wp-includes/js/media-*.js', + 'wp-includes/js/media-*.min.js', + 'wp-includes/js/dist/development', + ], + + // All workflow files that should be deleted from non-default branches. + workflowFiles = [ + // Reusable workflows should be called from `trunk` within branches. + '.github/workflows/reusable-*.yml', + // These workflows are only intended to run from `trunk`. + '.github/workflows/commit-built-file-changes.yml', + '.github/workflows/failed-workflow.yml', + '.github/workflows/install-testing.yml', + '.github/workflows/test-and-zip-default-themes.yml', + '.github/workflows/install-testing.yml', + '.github/workflows/slack-notifications.yml', + '.github/workflows/test-coverage.yml', + '.github/workflows/test-old-branches.yml', + '.github/workflows/upgrade-testing.yml' + ], + + // Prepend `dir` to `file`, and keep `!` in place. + setFilePath = function( dir, file ) { + if ( '!' === file.charAt( 0 ) ) { + return '!' + dir + file.substring( 1 ); + } + + return dir + file; + }, changedFiles = { php: [] }; @@ -47,11 +104,50 @@ module.exports = function(grunt) { // First do `npm install` if package.json has changed. installChanged.watchPackage(); - // Load tasks. - require('matchdep').filterDev(['grunt-*', '!grunt-legacy-util']).forEach( grunt.loadNpmTasks ); // Load legacy utils. grunt.util = require('grunt-legacy-util'); + var gruntDependencies = { + 'contrib': [ + 'clean', + 'concat', + 'copy', + 'cssmin', + 'imagemin', + 'jshint', + 'qunit', + 'uglify', + 'watch' + ], + 'standard': [ + 'banner', + 'file-append', + 'jsdoc', + 'patch-wordpress', + 'replace-lts', + 'rtlcss', + 'sass', + 'webpack' + ] + }; + + // Load grunt-* tasks. + function loadGruntTasks( dependency ) { + var contrib = key === 'contrib' ? 'contrib-' : ''; + grunt.loadNpmTasks( 'grunt-' + contrib + dependency ); + } + + for ( var key in gruntDependencies ) { + if ( ! gruntDependencies.hasOwnProperty( key ) ) { + continue; + } + + gruntDependencies[key].forEach( loadGruntTasks ); + } + + // Load PostCSS tasks. + grunt.loadNpmTasks('@lodder/grunt-postcss'); + // Project configuration. grunt.initConfig({ postcss: { @@ -80,12 +176,23 @@ module.exports = function(grunt) { ] } }, - usebanner: { + usebanner: { options: { position: 'top', banner: BANNER_TEXT, linebreak: true }, + codemirror: { + options: { + linebreak: false, + banner: require( './tools/webpack/codemirror-banner' ) + }, + files: { + src: [ + WORKING_DIR + 'wp-includes/js/codemirror/codemirror.min.css' + ] + } + }, files: { src: [ WORKING_DIR + 'wp-admin/css/*.min.css', @@ -102,37 +209,61 @@ module.exports = function(grunt) { clean: { plugins: [BUILD_DIR + 'wp-content/plugins'], themes: [BUILD_DIR + 'wp-content/themes'], + + // Clean the files from /build and the JS, CSS, and Webpack files from /src. files: buildFiles.concat( [ '!wp-config.php', ] ).map( function( file ) { - if ( '!' === file.charAt( 0 ) ) { - return '!' + BUILD_DIR + file.substring( 1 ); - } - return BUILD_DIR + file; + return setFilePath( BUILD_DIR, file ); + } ).concat( + cssFiles.map( function( file ) { + return setFilePath( SOURCE_DIR, file ); + } ) + ).concat( + jsFiles.map( function( file ) { + return setFilePath( SOURCE_DIR, file ); + } ) + ).concat( + webpackFiles.map( function( file ) { + return setFilePath( SOURCE_DIR, file ); + } ) + ), + + // Clean built JS, CSS, and Webpack files from either /src or /build. + css: cssFiles.map( function( file ) { + return setFilePath( WORKING_DIR, file ); + } ), + js: jsFiles.map( function( file ) { + return setFilePath( WORKING_DIR, file ); } ), - css: [ - WORKING_DIR + 'wp-admin/css/*.min.css', - WORKING_DIR + 'wp-admin/css/*-rtl*.css', - WORKING_DIR + 'wp-includes/css/*.min.css', - WORKING_DIR + 'wp-includes/css/*-rtl*.css', - WORKING_DIR + 'wp-admin/css/colors/**/*.css' - ], - js: [ - WORKING_DIR + 'wp-admin/js/', - WORKING_DIR + 'wp-includes/js/' - ], - 'webpack-assets': [ - WORKING_DIR + 'wp-includes/assets/*', - WORKING_DIR + 'wp-includes/css/dist/', - '!' + WORKING_DIR + 'wp-includes/assets/script-loader-packages.php' - ], + + // Clean files built by Webpack. + 'webpack-assets': webpackFiles.map( function( file ) { + return setFilePath( WORKING_DIR, file ); + } ), + + // Clean files built by the tools/gutenberg scripts. + gutenberg: gutenbergFiles.map( function( file ) { + return setFilePath( WORKING_DIR, file ); + }), dynamic: { dot: true, expand: true, cwd: WORKING_DIR, src: [] }, - qunit: ['tests/qunit/compiled.html'] + qunit: ['tests/qunit/compiled.html'], + + // This is only meant to run within a numbered branch after branching has occurred. + workflows: { + filter: function() { + var allowedTasks = [ 'post-branching', 'clean:workflows' ]; + return allowedTasks.some( function( task ) { + return grunt.cli.tasks.indexOf( task ) !== -1; + } ); + }, + src: workflowFiles + }, }, file_append: { // grunt-file-append supports only strings for input and output. @@ -163,7 +294,7 @@ module.exports = function(grunt) { '!js/**', // JavaScript is extracted into separate copy tasks. '!.{svn,git}', // Exclude version control folders. '!wp-includes/version.php', // Exclude version.php. - '!**/*.map', // The build doesn't need .map files. + '!{wp-admin,wp-includes,wp-content/themes/twenty*,wp-content/plugins/akismet}/**/*.map', // The build doesn't need .map files. '!index.php', '!wp-admin/index.php', '!_index.php', '!wp-admin/_index.php' ] ), @@ -188,16 +319,51 @@ module.exports = function(grunt) { // Renamed to avoid conflict with jQuery hoverIntent.min.js (after minifying). [ WORKING_DIR + 'wp-includes/js/hoverintent-js.min.js' ]: [ './node_modules/hoverintent/dist/hoverintent.min.js' ], + [ WORKING_DIR + 'wp-includes/js/imagesloaded.min.js' ]: [ './node_modules/imagesloaded/imagesloaded.pkgd.min.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.js' ]: [ './node_modules/jquery/dist/jquery.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.min.js' ]: [ './node_modules/jquery/dist/jquery.min.js' ], [ WORKING_DIR + 'wp-includes/js/jquery/jquery.form.js' ]: [ './node_modules/jquery-form/src/jquery.form.js' ], + [ WORKING_DIR + 'wp-includes/js/jquery/jquery.color.min.js' ]: [ './node_modules/jquery-color/dist/jquery.color.min.js' ], [ WORKING_DIR + 'wp-includes/js/masonry.min.js' ]: [ './node_modules/masonry-layout/dist/masonry.pkgd.min.js' ], - [ WORKING_DIR + 'wp-includes/js/twemoji.js' ]: [ './node_modules/twemoji/dist/twemoji.js' ], [ WORKING_DIR + 'wp-includes/js/underscore.js' ]: [ './node_modules/underscore/underscore.js' ], } ] }, + 'codemirror': { + options: { + process: function( content, srcpath ) { + if ( srcpath.includes( 'htmlhint.min.js' ) ) { + return content + '\nif ( window.HTMLHint && window.HTMLHint.HTMLHint ) { window.HTMLHint = window.HTMLHint.HTMLHint; }'; + } + return content; + } + }, + files: [ + { + [ WORKING_DIR + 'wp-includes/js/codemirror/csslint.js' ]: [ './node_modules/csslint/dist/csslint.js' ], + [ WORKING_DIR + 'wp-includes/js/codemirror/esprima.js' ]: [ './node_modules/esprima/dist/esprima.js' ], + [ WORKING_DIR + 'wp-includes/js/codemirror/htmlhint.js' ]: [ './node_modules/htmlhint/dist/htmlhint.min.js' ], + [ WORKING_DIR + 'wp-includes/js/codemirror/jsonlint.js' ]: [ './node_modules/jsonlint/web/jsonlint.js' ], + }, + { + expand: true, + cwd: SOURCE_DIR + 'js/_enqueues/lib/codemirror/', + src: [ + 'htmlhint-kses.js', + ], + dest: WORKING_DIR + 'wp-includes/js/codemirror/' + }, + { + expand: true, + cwd: SOURCE_DIR + 'js/_enqueues/deprecated/', + src: [ + 'fakejshint.js', + ], + dest: WORKING_DIR + 'wp-includes/js/codemirror/' + } + ] + }, 'vendor-js': { files: [ { @@ -233,13 +399,63 @@ module.exports = function(grunt) { dest: WORKING_DIR + 'wp-includes/js/jquery/' }, { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/lodash.js' ]: [ './node_modules/lodash/lodash.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/lodash.min.js' ]: [ './node_modules/lodash/lodash.min.js' ], + }, + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/moment.js' ]: [ './node_modules/moment/moment.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/moment.min.js' ]: [ './node_modules/moment/min/moment.min.js' ], + }, + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.js' ]: [ './node_modules/regenerator-runtime/runtime.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.min.js' ]: [ './node_modules/regenerator-runtime/runtime.js' ], + }, + // React libraries: react, react-dom + { + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react.js' ]: [ './node_modules/react/umd/react.development.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react.min.js' ]: [ './node_modules/react/umd/react.production.min.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react-dom.js' ]: [ './node_modules/react-dom/umd/react-dom.development.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/react-dom.min.js' ]: [ './node_modules/react-dom/umd/react-dom.production.min.js' ], + }, + // Polyfills + { + // @wordpress/babel-preset-default + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill.js' ]: [ './node_modules/@wordpress/babel-preset-default/build/polyfill.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill.min.js' ]: [ './node_modules/@wordpress/babel-preset-default/build/polyfill.min.js' ], + // polyfill-library (DOMRect) + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-dom-rect.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/DOMRect/raw.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-dom-rect.min.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/DOMRect/min.js' ], + // element-closest + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-element-closest.js' ]: [ './node_modules/element-closest/browser.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-element-closest.min.js' ]: [ './node_modules/element-closest/browser.js' ], + // whatwg-fetch + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.js' ]: [ './node_modules/whatwg-fetch/dist/fetch.umd.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js' ]: [ './node_modules/whatwg-fetch/dist/fetch.umd.js' ], + // formdata-polyfill + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-formdata.js' ]: [ './node_modules/formdata-polyfill/FormData.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-formdata.min.js' ]: [ './node_modules/formdata-polyfill/formdata.min.js' ], + // wicg-inert + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-inert.js' ]: [ './node_modules/wicg-inert/dist/inert.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-inert.min.js' ]: [ './node_modules/wicg-inert/dist/inert.min.js' ], + // polyfill-library (Node.prototype.contains) + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-node-contains.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/Node.prototype.contains/raw.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-node-contains.min.js' ]: [ './node_modules/polyfill-library/polyfills/__dist/Node.prototype.contains/min.js' ], + // objectFitPolyfill + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-object-fit.js' ]: [ './node_modules/objectFitPolyfill/src/objectFitPolyfill.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-object-fit.min.js' ]: [ './node_modules/objectFitPolyfill/dist/objectFitPolyfill.min.js' ], + // core-js-url-browser + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-url.js' ]: [ './node_modules/core-js-url-browser/url.js' ], + [ WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-url.min.js' ]: [ './node_modules/core-js-url-browser/url.min.js' ], + } + ].concat( + // Copy tinymce.js only when building to /src. + WORKING_DIR === SOURCE_DIR ? { expand: true, cwd: SOURCE_DIR + 'js/_enqueues/vendor/tinymce/', src: 'tinymce.js', dest: SOURCE_DIR + 'wp-includes/js/tinymce/' - }, - - ] + } : [] + ) }, 'admin-js': { files: { @@ -270,6 +486,7 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/media.js' ]: [ './src/js/_enqueues/admin/media.js' ], [ WORKING_DIR + 'wp-admin/js/nav-menu.js' ]: [ './src/js/_enqueues/lib/nav-menu.js' ], [ WORKING_DIR + 'wp-admin/js/password-strength-meter.js' ]: [ './src/js/_enqueues/wp/password-strength-meter.js' ], + [ WORKING_DIR + 'wp-admin/js/password-toggle.js' ]: [ './src/js/_enqueues/admin/password-toggle.js' ], [ WORKING_DIR + 'wp-admin/js/plugin-install.js' ]: [ './src/js/_enqueues/admin/plugin-install.js' ], [ WORKING_DIR + 'wp-admin/js/post.js' ]: [ './src/js/_enqueues/admin/post.js' ], [ WORKING_DIR + 'wp-admin/js/postbox.js' ]: [ './src/js/_enqueues/admin/postbox.js' ], @@ -280,6 +497,7 @@ module.exports = function(grunt) { [ WORKING_DIR + 'wp-admin/js/tags-suggest.js' ]: [ './src/js/_enqueues/admin/tags-suggest.js' ], [ WORKING_DIR + 'wp-admin/js/tags.js' ]: [ './src/js/_enqueues/admin/tags.js' ], [ WORKING_DIR + 'wp-admin/js/site-health.js' ]: [ './src/js/_enqueues/admin/site-health.js' ], + [ WORKING_DIR + 'wp-admin/js/site-icon.js' ]: [ './src/js/_enqueues/admin/site-icon.js' ], [ WORKING_DIR + 'wp-admin/js/privacy-tools.js' ]: [ './src/js/_enqueues/admin/privacy-tools.js' ], [ WORKING_DIR + 'wp-admin/js/theme-plugin-editor.js' ]: [ './src/js/_enqueues/wp/theme-plugin-editor.js' ], [ WORKING_DIR + 'wp-admin/js/theme.js' ]: [ './src/js/_enqueues/wp/theme.js' ], @@ -402,7 +620,161 @@ module.exports = function(grunt) { } ); } } - } + }, + 'workflow-references-local-to-remote': { + options: { + processContent: function( src ) { + return src.replace( /uses: \.\/\.github\/workflows\/([^\.]+)\.yml/g, function( match, $1 ) { + return 'uses: WordPress/wordpress-develop/.github/workflows/' + $1 + '.yml@trunk'; + } ); + } + }, + src: '.github/workflows/*.yml', + dest: './' + }, + 'workflow-references-remote-to-local': { + options: { + processContent: function( src ) { + return src.replace( /uses: WordPress\/wordpress-develop\/\.github\/workflows\/([^\.]+)\.yml@trunk/g, function( match, $1 ) { + return 'uses: ./.github/workflows/' + $1 + '.yml'; + } ); + } + }, + src: '.github/workflows/*.yml', + dest: './' + }, + certificates: { + src: 'vendor/composer/ca-bundle/res/cacert.pem', + dest: SOURCE_DIR + 'wp-includes/certificates/ca-bundle.crt' + }, + // Gutenberg PHP infrastructure files (routes.php, pages.php, constants.php, pages/). + 'gutenberg-php': { + options: { + process: function( content ) { + // Fix boot module asset file path for Core's different directory structure. + return content.replace( + /__DIR__\s*\.\s*(['"])\/..\/\..\/modules\/boot\/index\.min\.asset\.php\1/g, + 'ABSPATH . WPINC . \'/js/dist/script-modules/boot/index.min.asset.php\'' + ); + } + }, + files: [ { + expand: true, + cwd: 'gutenberg/build', + src: [ + 'routes.php', + 'pages.php', + 'constants.php', + 'pages/**/*.php', + ], + dest: WORKING_DIR + 'wp-includes/build/', + } ], + }, + /* + * Only copy files relevant to the routes specified in the registry file. + * + * While the registry file does not contain any experimental routes, the `gutenberg/build/routes` directory + * includes the files for all registered routes. Only the files related to the routes specified in the + * registry should be included in the WordPress build. + * + * The `src` list is populated at task runtime by `routes:setup`, which reads the registry after + * `gutenberg:download` has run. See the `routes:setup` task registration for implementation details. + */ + routes: { + expand: true, + cwd: 'gutenberg/build', + src: [], + dest: WORKING_DIR + 'wp-includes/build/', + }, + 'gutenberg-js': { + files: [ { + expand: true, + cwd: 'gutenberg/build', + src: [ + 'pages/**/*.js', + ], + dest: WORKING_DIR + 'wp-includes/build/', + } ], + }, + 'gutenberg-modules': { + files: [ { + expand: true, + cwd: 'gutenberg/build/modules', + src: [ + '**/*', + '!**/*.map', + '!vips/**', + ], + dest: WORKING_DIR + 'wp-includes/js/dist/script-modules/', + } ], + }, + 'gutenberg-styles': { + files: [ { + expand: true, + cwd: 'gutenberg/build/styles', + src: [ + '**/*', + '!**/*.map', + // Per-block CSS is copied to wp-includes/blocks/ by tools/gutenberg/copy.js. + '!block-library/*/**', + ], + dest: WORKING_DIR + 'wp-includes/css/dist/', + } ], + }, + 'gutenberg-theme-json': { + options: { + process: function( content, srcpath ) { + // Replace the local schema URL with the canonical public URL for Core. + if ( path.basename( srcpath ) === 'theme.json' ) { + return content.replace( + '"$schema": "../schemas/json/theme.json"', + '"$schema": "https://schemas.wp.org/trunk/theme.json"' + ); + } + return content; + } + }, + files: [ + { + src: 'gutenberg/lib/theme.json', + dest: WORKING_DIR + 'wp-includes/theme.json', + }, + { + src: 'gutenberg/lib/theme-i18n.json', + dest: WORKING_DIR + 'wp-includes/theme-i18n.json', + }, + ], + }, + 'icon-library-images': { + files: [ { + expand: true, + cwd: 'gutenberg/packages/icons/src/library', + src: '*.svg', + dest: WORKING_DIR + 'wp-includes/images/icon-library', + } ], + }, + 'icon-library-manifest': { + options: { + process: function( content ) { + return content + // Remove the 'gutenberg' text domain from _x() calls. + .replace( + /_x\(\s*([^,]+),\s*([^,]+),\s*['"]gutenberg['"]\s*\)/g, + '_x( $1, $2 )' + ) + // Strip the 'library/' prefix from filePath values so they + // resolve correctly relative to wp-includes/images/icon-library/. + .replace( + /'filePath' => 'library\//g, + '\'filePath\' => \'' + ); + } + }, + files: [ { + src: 'gutenberg/packages/icons/src/manifest.php', + dest: WORKING_DIR + 'wp-includes/assets/icon-library-manifest.php', + } ], + }, }, sass: { colors: { @@ -420,6 +792,22 @@ module.exports = function(grunt) { options: { compatibility: 'ie11' }, + codemirror: { + files: { + [ WORKING_DIR + 'wp-includes/js/codemirror/codemirror.min.css' ]: [ + 'node_modules/codemirror/lib/codemirror.css', + 'node_modules/codemirror/addon/hint/show-hint.css', + 'node_modules/codemirror/addon/lint/lint.css', + 'node_modules/codemirror/addon/dialog/dialog.css', + 'node_modules/codemirror/addon/display/fullscreen.css', + 'node_modules/codemirror/addon/fold/foldgutter.css', + 'node_modules/codemirror/addon/merge/merge.css', + 'node_modules/codemirror/addon/scroll/simplescrollbars.css', + 'node_modules/codemirror/addon/search/matchesonscrollbar.css', + 'node_modules/codemirror/addon/tern/tern.css' + ] + } + }, core: { expand: true, cwd: WORKING_DIR, @@ -451,6 +839,16 @@ module.exports = function(grunt) { src: [ 'wp-admin/css/colors/*/*.css' ] + }, + themes: { + expand: true, + cwd: WORKING_DIR, + dest: WORKING_DIR, + ext: '.min.css', + src: [ + 'wp-content/themes/twentytwentytwo/style.css', + 'wp-content/themes/twentytwentyfive/style.css', + ] } }, rtlcss: { @@ -515,8 +913,10 @@ module.exports = function(grunt) { 'wp-admin/css/*.css', 'wp-includes/css/*.css', - // Exclude minified and already processed files, and files from external packages. - // These are present when running `grunt build` after `grunt --dev`. + /* + * Exclude minified and already processed files, and files from external packages. + * These are present when running `grunt build` after `grunt --dev`. + */ '!wp-admin/css/*-rtl.css', '!wp-includes/css/*-rtl.css', '!wp-admin/css/*.min.css', @@ -690,7 +1090,14 @@ module.exports = function(grunt) { }, uglify: { options: { + parse: { + module: false + }, + compress: { + module: false + }, output: { + module: false, ascii_only: true } }, @@ -710,24 +1117,19 @@ module.exports = function(grunt) { 'wp-includes/js/tinymce/plugins/wp*/plugin.js', // Exceptions. - '!**/*.min.js', + '!{wp-admin,wp-includes}/**/*.min.js', '!wp-admin/js/custom-header.js', // Why? We should minify this. '!wp-admin/js/farbtastic.js', - '!wp-includes/js/swfobject.js', - '!wp-includes/js/wp-embed.js' // We have extra options for this, see uglify:embed. + '!wp-includes/js/wp-emoji-loader.js', // This is a module. See the emoji-loader task below. ] }, - embed: { + 'emoji-loader': { options: { - compress: { - conditionals: false - } + module: true, + toplevel: true, }, - expand: true, - cwd: WORKING_DIR, - dest: WORKING_DIR, - ext: '.min.js', - src: ['wp-includes/js/wp-embed.js'] + src: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.js', + dest: WORKING_DIR + 'wp-includes/js/wp-emoji-loader.min.js', }, 'jquery-ui': { options: { @@ -754,6 +1156,14 @@ module.exports = function(grunt) { src: WORKING_DIR + 'wp-includes/js/dist/vendor/moment.js', dest: WORKING_DIR + 'wp-includes/js/dist/vendor/moment.min.js' }, + 'regenerator-runtime': { + src: WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.js', + dest: WORKING_DIR + 'wp-includes/js/dist/vendor/regenerator-runtime.min.js' + }, + 'wp-polyfill-fetch': { + src: WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.js', + dest: WORKING_DIR + 'wp-includes/js/dist/vendor/wp-polyfill-fetch.min.js' + }, dynamic: { expand: true, cwd: WORKING_DIR, @@ -765,7 +1175,8 @@ module.exports = function(grunt) { webpack: { prod: webpackConfig( { environment: 'production', buildTarget: WORKING_DIR } ), dev: webpackConfig( { environment: 'development', buildTarget: WORKING_DIR } ), - watch: webpackConfig( { environment: 'development', watch: true } ) + watch: webpackConfig( { environment: 'development', watch: true } ), + codemirror: require( './tools/webpack/codemirror.config.js' )( { buildTarget: WORKING_DIR } ), }, concat: { tinymce: { @@ -986,50 +1397,18 @@ module.exports = function(grunt) { } } }, - jsvalidate:{ - options: { - globals: {}, - esprimaOptions:{}, - verbose: false - }, - build: { - files: { - src: [ - WORKING_DIR + 'wp-{admin,includes}/**/*.js', - WORKING_DIR + 'wp-content/themes/twenty*/**/*.js', - '!' + WORKING_DIR + 'wp-content/themes/twenty*/node_modules/**/*.js', - '!' + WORKING_DIR + 'wp-includes/blocks/**/*.js', - '!' + WORKING_DIR + 'wp-includes/js/dist/**/*.js', - ] - } - }, - dynamic: { - files: { - src: [] - } - } - }, imagemin: { core: { expand: true, cwd: SOURCE_DIR, src: [ 'wp-{admin,includes}/images/**/*.{png,jpg,gif,jpeg}', + 'wp-content/themes/**/*.{png,jpg,gif,jpeg}', 'wp-includes/js/tinymce/skins/wordpress/images/*.{png,jpg,gif,jpeg}' ], dest: SOURCE_DIR } }, - includes: { - emoji: { - src: BUILD_DIR + 'wp-includes/formatting.php', - dest: '.' - }, - embed: { - src: BUILD_DIR + 'wp-includes/embed.php', - dest: '.' - } - }, replace: { 'emoji-regex': { options: { @@ -1037,19 +1416,34 @@ module.exports = function(grunt) { { match: /\/\/ START: emoji arrays[\S\s]*\/\/ END: emoji arrays/g, replacement: function() { - var regex, files, + var regex, files, ghCli, partials, partialsSet, - entities, emojiArray; + entities, emojiArray, + apiResponse, query; grunt.log.writeln( 'Fetching list of Twemoji files...' ); + // Ensure that the GitHub CLI is installed. + ghCli = spawn( 'gh', [ '--version' ] ); + if ( 0 !== ghCli.status ) { + grunt.fatal( 'Emoji precommit script requires GitHub CLI. See https://cli.github.com/.' ); + } + // Fetch a list of the files that Twemoji supplies. - files = spawn( 'svn', [ 'ls', 'https://github.com/twitter/twemoji.git/trunk/assets/svg' ] ); + query = 'query={repository(owner: "jdecked", name: "twemoji") {object(expression: "gh-pages:v/17.0.2/svg") {... on Tree {entries {name}}}}}'; + files = spawn( 'gh', [ 'api', 'graphql', '-f', query] ); + if ( 0 !== files.status ) { - grunt.fatal( 'Unable to fetch Twemoji file list' ); + grunt.fatal( files.stderr.toString() ); } - entities = files.stdout.toString(); + try { + apiResponse = JSON.parse( files.stdout.toString() ); + } catch ( e ) { + grunt.fatal( 'Unable to parse Twemoji file list' ); + } + entities = apiResponse.data.repository.object.entries; + entities = entities.reduce( function( accumulator, val ) { return accumulator + val.name + '\n'; }, '' ); // Tidy up the file list. entities = entities.replace( /\.svg/g, '' ); @@ -1103,31 +1497,11 @@ module.exports = function(grunt) { } ] }, - 'emoji-banner-text': { - options: { - patterns: [ - { - match: new RegExp( '\\s*' + BANNER_TEXT.replace( /[\/\*\!]/g, '\\$&' ) ), - replacement: '' - } - ] - }, - files: [ - { - expand: true, - flatten: true, - src: [ - BUILD_DIR + 'wp-includes/formatting.php' - ], - dest: BUILD_DIR + 'wp-includes/' - } - ] - }, 'source-maps': { options: { patterns: [ { - match: new RegExp( '//# sourceMappingURL=.*\\s*' ), + match: new RegExp( '\/\/# sourceMappingURL=.*\\s*', 'g' ), replacement: '' } ] @@ -1140,6 +1514,24 @@ module.exports = function(grunt) { BUILD_DIR + 'wp-includes/js/underscore.js' ], dest: BUILD_DIR + 'wp-includes/js/' + }, + { + expand: true, + cwd: BUILD_DIR + 'wp-includes/js/dist/', + src: [ '*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/', + }, + { + expand: true, + cwd: BUILD_DIR + 'wp-includes/js/dist/vendor/', + src: [ '**/*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/vendor/', + }, + { + expand: true, + cwd: BUILD_DIR + 'wp-includes/js/dist/script-modules/', + src: [ '**/*.js' ], + dest: BUILD_DIR + 'wp-includes/js/dist/script-modules/', } ] } @@ -1153,7 +1545,9 @@ module.exports = function(grunt) { SOURCE_DIR + '**', '!' + SOURCE_DIR + 'js/**/*.js', // Ignore version control directories. - '!' + SOURCE_DIR + '**/.{svn,git}/**' + '!' + SOURCE_DIR + '**/.{svn,git}/**', + // Ignore third-party plugins. + '!' + SOURCE_DIR + 'wp-content/plugins/**' ], tasks: ['clean:dynamic', 'copy:dynamic'], options: { @@ -1163,7 +1557,7 @@ module.exports = function(grunt) { }, 'js-enqueues': { files: [SOURCE_DIR + 'js/_enqueues/**/*.js'], - tasks: ['clean:dynamic', 'copy:dynamic-js', 'uglify:dynamic', 'jsvalidate:dynamic'], + tasks: ['clean:dynamic', 'copy:dynamic-js', 'uglify:dynamic'], options: { dot: true, spawn: false @@ -1175,7 +1569,7 @@ module.exports = function(grunt) { '!' + SOURCE_DIR + 'js/_enqueues/**/*.js', 'webpack-dev.config.js' ], - tasks: ['clean:dynamic', 'webpack:dev', 'uglify:dynamic', 'jsvalidate:dynamic'], + tasks: ['clean:dynamic', 'webpack:dev', 'uglify:dynamic'], options: { dot: true, spawn: false @@ -1245,6 +1639,59 @@ module.exports = function(grunt) { 'qunit:compiled' ] ); + // Gutenberg integration tasks. + grunt.registerTask( 'gutenberg:verify', 'Verifies the installed Gutenberg version matches the expected SHA.', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/utils.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + + grunt.registerTask( 'gutenberg:download', 'Downloads the built Gutenberg artifact.', function() { + const done = this.async(); + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/download.js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + if ( error ) { + done( false ); + return; + } + /* + * Build block editor files into the src directory every time assets + * are downloaded. This prevents failures when running from src + * without running `build:dev` after those files were removed from + * version control in https://core.trac.wordpress.org/changeset/61438. + * + * See https://core.trac.wordpress.org/ticket/64393. + */ + grunt.util.spawn( { + grunt: true, + args: [ 'build:gutenberg', '--dev' ], + opts: { stdio: 'inherit' } + }, function( buildError ) { + done( ! buildError ); + } ); + } ); + } ); + + grunt.registerTask( 'gutenberg:copy', 'Copies Gutenberg JS packages and block assets to WordPress Core.', function() { + const done = this.async(); + const buildDir = grunt.option( 'dev' ) ? 'src' : 'build'; + grunt.util.spawn( { + cmd: 'node', + args: [ 'tools/gutenberg/copy.js', `--build-dir=${ buildDir }` ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.renameTask( 'watch', '_watch' ); grunt.registerTask( 'watch', function() { @@ -1269,9 +1716,12 @@ module.exports = function(grunt) { grunt.registerTask( 'precommit:js', [ 'webpack:prod', 'jshint:corejs', + 'typecheck:js', 'uglify:imgareaselect', 'uglify:jqueryform', 'uglify:moment', + 'uglify:regenerator-runtime', + 'uglify:wp-polyfill-fetch', 'qunit:compiled' ] ); @@ -1280,6 +1730,7 @@ module.exports = function(grunt) { ] ); grunt.registerTask( 'precommit:php', [ + 'phpstan', 'phpunit' ] ); @@ -1409,11 +1860,20 @@ module.exports = function(grunt) { grunt.registerTask( 'uglify:all', [ 'uglify:core', - 'uglify:embed', + 'uglify:emoji-loader', 'uglify:jquery-ui', 'uglify:imgareaselect', 'uglify:jqueryform', - 'uglify:moment' + 'uglify:moment', + 'uglify:regenerator-runtime', + 'uglify:wp-polyfill-fetch' + ] ); + + grunt.registerTask( 'build:codemirror', [ + 'webpack:codemirror', + 'cssmin:codemirror', + 'usebanner:codemirror', + 'copy:codemirror' ] ); grunt.registerTask( 'build:webpack', [ @@ -1429,8 +1889,7 @@ module.exports = function(grunt) { 'file_append', 'uglify:all', 'concat:tinymce', - 'concat:emoji', - 'jsvalidate:build' + 'concat:emoji' ] ); grunt.registerTask( 'build:css', [ @@ -1442,7 +1901,95 @@ module.exports = function(grunt) { 'rtl', 'cssmin:rtl', 'cssmin:colors', - 'usebanner' + 'cssmin:themes', + 'usebanner:files' + ] ); + + grunt.registerTask( 'certificates:upgrade-package', 'Upgrades the package responsible for supplying the certificate authority certificate store bundled with WordPress.', function() { + var done = this.async(); + var flags = this.flags; + var spawn = require( 'child_process' ).spawnSync; + var fs = require( 'fs' ); + + // Ensure that `composer update` has been run and the dependency is installed. + if ( ! fs.existsSync( 'vendor' ) || ! fs.existsSync( 'vendor/composer' ) || ! fs.existsSync( 'vendor/composer/ca-bundle' ) ) { + grunt.log.error( 'composer/ca-bundle dependency is missing. Please run `composer update` before attempting to upgrade the certificate bundle.' ); + done( false ); + return; + } + + /* + * Because the `composer/ca-bundle` is pinned to an exact version to ensure upgrades are applied intentionally, + * the `composer update` command will not upgrade the dependency. Instead, `composer require` must be called, + * but the specific version being upgraded to must be known and passed to the command. + */ + var outdatedResult = spawn( 'composer', [ 'outdated', 'composer/ca-bundle', '--format=json' ] ); + + if ( outdatedResult.status !== 0 ) { + grunt.log.error( 'Failed to get the package information for composer/ca-bundle.' ); + done( false ); + return; + } + + var packageInfo; + try { + var stdout = outdatedResult.stdout.toString().trim(); + if ( ! stdout ) { + grunt.log.writeln( 'The latest version is already installed.' ); + done( true ); + return; + } + packageInfo = JSON.parse( stdout ); + } catch ( e ) { + grunt.log.error( 'Failed to parse the package information for composer/ca-bundle.' ); + done( false ); + return; + } + + // Check for the version information needed to perform the necessary comparisons. + if ( ! packageInfo.versions || ! packageInfo.versions[0] || ! packageInfo.latest ) { + grunt.log.error( 'Could not determine version information for composer/ca-bundle.' ); + done( false ); + return; + } + + var currentVersion = packageInfo.versions[0]; + var latestVersion = packageInfo.latest; + + // Compare versions to ensure we actually need to update + if ( currentVersion === latestVersion ) { + grunt.log.writeln( 'The latest version is already installed: ' + latestVersion + '.' ); + done( true ); + return; + } + + grunt.log.writeln( 'Installed version: ' + currentVersion ); + grunt.log.writeln( 'New version found: ' + latestVersion ); + + // Upgrade to the latest version and change the pinned version in composer.json. + var args = [ 'require', 'composer/ca-bundle:' + latestVersion, '--dev' ]; + + grunt.util.spawn( { + cmd: 'composer', + args: args, + opts: { stdio: 'inherit' } + }, function( error ) { + if ( flags.error && error ) { + done( false ); + } else { + grunt.log.writeln( 'Successfully updated composer/ca-bundle to ' + latestVersion ); + done( true ); + } + } ); + } ); + + grunt.registerTask( 'build:certificates', [ + 'copy:certificates' + ] ); + + grunt.registerTask( 'certificates:upgrade', [ + 'certificates:upgrade-package', + 'copy:certificates' ] ); grunt.registerTask( 'build:files', [ @@ -1451,42 +1998,27 @@ module.exports = function(grunt) { 'copy:version', ] ); + grunt.registerTask( 'replace:workflow-references-local-to-remote', [ + 'copy:workflow-references-local-to-remote', + ]); + + grunt.registerTask( 'replace:workflow-references-remote-to-local', [ + 'copy:workflow-references-remote-to-local', + ]); + + grunt.registerTask( 'post-branching', [ + 'clean:workflows', + 'replace:workflow-references-local-to-remote' + ]); + /** * Build verification tasks. */ grunt.registerTask( 'verify:build', [ - 'verify:wp-embed', 'verify:old-files', 'verify:source-maps', ] ); - /** - * Build assertions for wp-embed.min.js. - * - * @ticket 34698 - */ - grunt.registerTask( 'verify:wp-embed', function() { - const file = `${ BUILD_DIR }/wp-includes/js/wp-embed.min.js`; - - assert( - fs.existsSync( file ), - 'The build/wp-includes/js/wp-embed.min.js file does not exist.' - ); - - const contents = fs.readFileSync( file, { - encoding: 'utf8', - } ); - - assert( - contents.length > 0, - 'The build/wp-includes/js/wp-embed.min.js file must not be empty.' - ); - assert( - false === contents.includes( '&' ), - 'The build/wp-includes/js/wp-embed.min.js file must not contain ampersands.' - ); - } ); - /** * Build assertions to ensure no project files are inside `$_old_files` in the build directory. * @@ -1517,19 +2049,19 @@ module.exports = function(grunt) { ); const files = match[1].split( '\n\t' ).filter( function( file ) { - // Filter out empty lines + // Filter out empty lines. if ( '' === file ) { return false; } - // Filter out commented out lines + // Filter out commented out lines. if ( 0 === file.indexOf( '/' ) ) { return false; } return true; } ).map( function( file ) { - // Strip leading and trailing single quotes and commas + // Strip leading and trailing single quotes and commas. return file.replace( /^\'|\',$/g, '' ); } ); @@ -1543,12 +2075,20 @@ module.exports = function(grunt) { } ); /** - * Build assertions for the lack of source maps in JavaScript files. + * Compiled JavaScript files may link to sourcemaps. In some cases, + * the source map may not be available, which can cause 404 errors when + * browsers try to download the sourcemap from the referenced URLs. + * Ensure that sourcemap links are not included in JavaScript files. * * @ticket 24994 * @ticket 46218 + * @ticket 60348 */ grunt.registerTask( 'verify:source-maps', function() { + const ignoredFiles = [ + 'build/wp-includes/js/dist/components.js', + 'build/wp-includes/js/dist/data.js', + ]; const files = buildFiles.reduce( ( acc, path ) => { // Skip excluded paths and any path that isn't a file. if ( '!' === path[0] || '**' !== path.substr( -2 ) ) { @@ -1563,34 +2103,96 @@ module.exports = function(grunt) { 'No JavaScript files found in the build directory.' ); - files.forEach( function( file ) { - const contents = fs.readFileSync( file, { - encoding: 'utf8', + files + .filter(file => ! ignoredFiles.includes( file) ) + .forEach( function( file ) { + const contents = fs.readFileSync( file, { + encoding: 'utf8', + } ); + // `data:` URLs are allowed: + const doesNotHaveSourceMap = ! /^\/\/# sourceMappingURL=((?!data:).)/m.test(contents); + + assert( + doesNotHaveSourceMap, + `The ${ file } file must not contain a sourceMappingURL.` + ); } ); - // `data:` URLs are allowed: - const match = contents.match( /sourceMappingURL=((?!data:).)/ ); + } ); - assert( - match === null, - `The ${ file } file must not contain a sourceMappingURL.` + grunt.registerTask( 'routes:setup', 'Reads the routes registry and configures the copy:routes task.', function() { + const registryPath = 'gutenberg/build/routes/registry.php'; + let registryContent; + try { + registryContent = fs.readFileSync( registryPath, 'utf8' ); + } catch ( e ) { + grunt.fatal( + 'Route registry not found at ' + registryPath + '. Run `grunt gutenberg:download` first.' + ); + } + const namePattern = /'name'\s*=>\s*'([^']+)'/g; + const routeNames = []; + let match; + while ( ( match = namePattern.exec( registryContent ) ) !== null ) { + routeNames.push( match[ 1 ] ); + } + + if ( routeNames.length === 0 ) { + grunt.fatal( + 'No route names found in ' + registryPath + '. The format of the file may have changed.' ); + } + + const validName = /^[A-Za-z0-9_-]+$/; + routeNames.forEach( function( name ) { + if ( ! validName.test( name ) ) { + grunt.fatal( + 'Invalid route name \'' + name + '\' in ' + registryPath + '. Expected only letters, digits, hyphens, and underscores.' + ); + } } ); + + grunt.config( [ 'copy', 'routes', 'src' ], [ 'routes/registry.php' ].concat( + routeNames.flatMap( function( name ) { + return [ + 'routes/' + name + '/**/*.php', + 'routes/' + name + '/**/*.js', + ]; + } ) + ) ); } ); + grunt.registerTask( 'build:gutenberg', [ + 'copy:gutenberg-php', + 'routes:setup', + 'copy:routes', + 'copy:gutenberg-js', + 'gutenberg:copy', + 'copy:gutenberg-modules', + 'copy:gutenberg-styles', + 'copy:gutenberg-theme-json', + 'copy:icon-library-images', + 'copy:icon-library-manifest', + ] ); + grunt.registerTask( 'build', function() { if ( grunt.option( 'dev' ) ) { grunt.task.run( [ + 'gutenberg:verify', 'build:js', 'build:css', + 'build:codemirror', + 'build:gutenberg', + 'build:certificates' ] ); } else { grunt.task.run( [ + 'gutenberg:verify', + 'build:certificates', 'build:files', 'build:js', 'build:css', - 'includes:emoji', - 'includes:embed', - 'replace:emoji-banner-text', + 'build:codemirror', + 'build:gutenberg', 'replace:source-maps', 'verify:build' ] ); @@ -1624,6 +2226,30 @@ module.exports = function(grunt) { grunt.registerTask( 'test', 'Runs all QUnit and PHPUnit tasks.', ['qunit:compiled', 'phpunit'] ); + grunt.registerTask( 'typecheck:js', 'Runs TypeScript type checking.', function() { + var done = this.async(); + + grunt.util.spawn( { + cmd: 'npm', + args: [ 'run', 'typecheck:js' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + + grunt.registerTask( 'phpstan', 'Runs PHPStan on the entire codebase.', function() { + var done = this.async(); + + grunt.util.spawn( { + cmd: 'composer', + args: [ 'phpstan' ], + opts: { stdio: 'inherit' } + }, function( error ) { + done( ! error ); + } ); + } ); + grunt.registerTask( 'format:php', 'Runs the code formatter on changed files.', function() { var done = this.async(); var flags = this.flags; @@ -1664,6 +2290,23 @@ module.exports = function(grunt) { } ); } ); + grunt.registerTask( 'wp-packages:update', 'Update WordPress packages', function() { + const distTag = grunt.option('dist-tag') || 'latest'; + grunt.log.writeln( `Updating WordPress packages (--dist-tag=${distTag})` ); + spawn( 'npx', [ 'wp-scripts', 'packages-update', `--dist-tag=${distTag}` ], { + cwd: __dirname, + stdio: 'inherit', + } ); + } ); + + grunt.registerTask( 'browserslist:update', 'Update the local database of browser supports', function() { + grunt.log.writeln( `Updating browsers list` ); + spawn( 'npx', [ 'browserslist@latest', '--update-db' ], { + cwd: __dirname, + stdio: 'inherit', + } ); + } ); + // Patch task. grunt.renameTask('patch_wordpress', 'patch'); @@ -1732,7 +2375,7 @@ module.exports = function(grunt) { if ( action !== 'deleted' ) { grunt.config( [ 'copy', 'dynamic-js', 'files' ], files ); } - // For the webpack builds configure the jsvalidate task to only check those files build by webpack. + // For the webpack builds configure the task to only check those files built by webpack. } else if ( target === 'js-webpack' ) { src = [ 'wp-includes/js/media-audiovideo.js', @@ -1754,17 +2397,19 @@ module.exports = function(grunt) { // Clean up only those files that were deleted. grunt.config( [ 'clean', 'dynamic', 'src' ], src ); } else { - // Otherwise copy over only the changed file. - grunt.config( [ 'copy', 'dynamic', 'src' ], src ); + if ( ! grunt.option( 'dev' ) ) { + // Otherwise copy over only the changed file. + grunt.config(['copy', 'dynamic', 'src'], src); + } // For javascript also minify and validate the changed file. if ( target === 'js-enqueues' ) { grunt.config( [ 'uglify', 'dynamic', 'src' ], src ); - grunt.config( [ 'jsvalidate', 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); + grunt.config( [ 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); } // For webpack only validate the file, minification is handled by webpack itself. if ( target === 'js-webpack' ) { - grunt.config( [ 'jsvalidate', 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); + grunt.config( [ 'dynamic', 'files', 'src' ], src.map( function( dir ) { return WORKING_DIR + dir; } ) ); } // For css run the rtl task on just the changed file. if ( target === 'rtl' ) { diff --git a/README.md b/README.md index 5f6fc5951bde7..5201a5180c1da 100644 --- a/README.md +++ b/README.md @@ -7,6 +7,8 @@ Welcome to the WordPress development repository! Please check out the [contribut ## Getting Started +### Local development + WordPress is a PHP, MySQL, and JavaScript based project, and uses Node for its JavaScript dependencies. A local development environment is available to quickly get up and running. You will need a basic understanding of how to use the command line on your computer. This will allow you to set up the local development environment, to start it and stop it when necessary, and to run the tests. @@ -19,15 +21,34 @@ You will need Node and npm installed on your computer. Node is a JavaScript runt If you are not using a package manager, see the [Node.js download page](https://nodejs.org/en/download/) for installers and binaries. -You will also need [Docker](https://www.docker.com/products/docker-desktop) installed and running on your computer. Docker is the virtualization software that powers the local development environment. Docker can be installed just like any other regular application. +**Note:** WordPress currently only officially supports Node.js `20.x` and npm `10.x`. + +You will also need a container environment such as [Docker Desktop](https://www.docker.com/products/docker-desktop) installed and running on your computer. The container environment is the virtualization software that powers the local development environment and can be installed just like any other regular application. + +**Note:** WordPress currently only officially supports Docker but several container environments are available and should generally be compatible, such as [Colima](https://github.com/abiosoft/colima), [OrbStack](https://orbstack.dev/), [Podman Desktop](https://podman-desktop.io/), and [Rancher Desktop](https://rancherdesktop.io/). ### Development Environment Commands -Ensure [Docker](https://www.docker.com/products/docker-desktop) is running before using these commands. +Ensure your container environment is running before using these commands. #### To start the development environment for the first time -Clone the current repository using `git clone https://github.com/WordPress/wordpress-develop.git`. Then in your terminal move to the repository folder `cd wordpress-develop` and run the following commands: +You can get started using the local development environment with these steps: + +1. Go to https://github.com/WordPress/wordpress-develop and fork the repository to your own GitHub account. +1. Then clone the forked repository to your computer using `git clone https://github.com//wordpress-develop.git`. +1. Navigate into the directory for the cloned repository using `cd wordpress-develop`. +1. Add the origin repo as an `upstream` remote via `git remote add upstream https://github.com/WordPress/wordpress-develop.git`. +1. Then you can keep your branches up to date via `git pull --ff upstream/trunk`, for example. + +Alternatively, if you have the [GitHub CLI](https://cli.github.com/) installed, you can simply run `gh repo fork WordPress/wordpress-develop --clone --remote` ([docs](https://cli.github.com/manual/gh_repo_fork)). This command will: +1. Fork the repository to your account (use the `--org` flag to clone into an organization). +1. Clone the repository to your machine. +1. Add `WordPress/wordpress-develop` as `upstream` and set it to the default `remote` repository + +After this, remember to run `cd wordpress-develop`. + +Once you have forked and cloned the repository to your computer, run the following commands in a terminal: ``` npm install @@ -36,7 +57,7 @@ npm run env:start npm run env:install ``` -Your WordPress site will accessible at http://localhost:8889. You can see or change configurations in the `.env` file located at the root of the project directory. +Your WordPress site will be accessible at http://localhost:8889. You can see or change configurations in the `.env` file located at the root of the project directory. #### To watch for changes @@ -51,13 +72,13 @@ To stop the watcher, press `ctrl+c`. #### To run a [WP-CLI](https://make.wordpress.org/cli/handbook/) command ``` -npm run env:cli +npm run env:cli -- ``` -WP-CLI has a lot of [useful commands](https://developer.wordpress.org/cli/commands/) you can use to work on your WordPress site. Where the documentation mentions running `wp`, run `npm run env:cli` instead. For example: +WP-CLI has [many useful commands](https://developer.wordpress.org/cli/commands/) you can use to work on your WordPress site. Where the documentation mentions running `wp`, run `npm run env:cli --` instead. For example: ``` -npm run env:cli help +npm run env:cli -- help ``` #### To run the tests @@ -69,6 +90,49 @@ npm run test:php npm run test:e2e ``` +You can pass extra parameters into the PHP tests by adding `--` and then the [command-line options](https://docs.phpunit.de/en/10.4/textui.html#command-line-options): + +``` +npm run test:php -- --filter +npm run test:php -- --group +``` + +#### To lint the workflow files + +GitHub Actions workflows operate in a privileged software supply chain environment, therefore all workflow files must adhere to a high degree of quality and security standards. + +All YAML workflow files within the `.github/workflows` directory are statically scanned when modified using [Actionlint](https://github.com/rhysd/actionlint) and [Zizmor](https://github.com/zizmorcore/zizmor). It's recommended that you install both of these tools locally using a package manager to run prior to submitting changes to workflow files. + +- [Actionlint installations instructions](https://github.com/rhysd/actionlint/blob/main/docs/install.md) +- [Zizmor installation instructions](https://docs.zizmor.sh/installation/) + +To run Actionlint: + +``` +actionlint +``` + +To run Zizmor for all workflow files (note the trailing period): + +``` +zizmor . +``` + +**Note:** A workflow run failure will not occur when issues are detected by Zizmor. Instead, the generated report is submitted to GitHub Code Scanning and surfaced through a status check. Some locally reported issues may be ignored based on the repository's configured Code Scanning settings. + +#### Generating a code coverage report +PHP code coverage reports are [generated daily](https://github.com/WordPress/wordpress-develop/actions/workflows/test-coverage.yml) and [submitted to Codecov.io](https://app.codecov.io/gh/WordPress/wordpress-develop). + +After the local container environment has [been installed and started](#to-start-the-development-environment-for-the-first-time), the following command can be used to generate a code coverage report. + +``` +npm run test:coverage +``` + +The command will generate three coverage reports in HTML, PHP, and text formats, saving them in the `coverage` folder. + +**Note:** xDebug is required to generate a code coverage report, which can slow down PHPUnit significantly. Passing selection-based options such as `--group` or `--filter` can decrease the overall time required but will result in an incomplete report. + #### To restart the development environment You may want to restart the environment if you've made changes to the configuration in the `docker-compose.yml` or `.env` files. Restart the environment with: @@ -93,6 +157,32 @@ Starting the environment again is a single command: npm run env:start ``` +#### Resetting the development environment + +The development environment can be reset. This will destroy the database and attempt to remove the pulled container images. + +``` +npm run env:reset +``` + +### Apple Silicon machines and old MySQL/MariaDB versions + +Older MySQL and MariaDB container images do not support Apple Silicon processors (M1, M2, etc.). This is true for: + +- MySQL versions 5.7 and earlier +- MariaDB 5.5 + +When using these versions on an Apple Silicon machine, you must create a `docker-compose.override.yml` file with the following contents: + +``` +services: + + mysql: + platform: linux/amd64 +``` + +Additionally, the "Use Rosetta for x86/AMD64 emulation on Apple Silicon" setting in your container environment (if applicable) needs to be disabled for this workaround. + ## Credentials These are the default environment credentials: @@ -106,6 +196,8 @@ To login to the site, navigate to http://localhost:8889/wp-admin. * Username: `admin` * Password: `password` +**Note:** With Codespaces, open the portforwarded URL from the ports tab in the terminal, and append `/wp-admin` to login to the site. + To generate a new password (recommended): 1. Go to the Dashboard diff --git a/SECURITY.md b/SECURITY.md index fe8b683d6a816..20b9b8e4b3890 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -1,91 +1,41 @@ # Security Policy -Full details of the WordPress Security Policy can be found on [HackerOne](https://hackerone.com/wordpress). You can also read more in a detailed white paper about [WordPress Security](https://wordpress.org/about/security/). +WordPress is an open-source publishing platform. The WordPress Security Team believes in Responsible Disclosure by alerting the security team immediately and privately of any potential vulnerabilities. -## Supported Versions - -Use this section to tell people about which versions of your project are -currently being supported with security updates. - -| Version | Supported | -| ------- | ------------------ | -| 5.8.x | :white_check_mark: | -| 5.7.x | :white_check_mark: | -| 5.6.x | :white_check_mark: | -| 5.5.x | :white_check_mark: | -| 5.4.x | :white_check_mark: | -| 5.3.x | :white_check_mark: | -| 5.2.x | :white_check_mark: | -| 5.1.x | :white_check_mark: | -| 5.0.x | :white_check_mark: | -| 4.9.x | :white_check_mark: | -| 4.8.x | :white_check_mark: | -| 4.7.x | :white_check_mark: | -| 4.6.x | :white_check_mark: | -| 4.5.x | :white_check_mark: | -| 4.4.x | :white_check_mark: | -| 4.3.x | :white_check_mark: | -| 4.2.x | :white_check_mark: | -| 4.1.x | :white_check_mark: | -| 4.0.x | :white_check_mark: | -| 3.9.x | :white_check_mark: | -| 3.8.x | :white_check_mark: | -| 3.7.x | :white_check_mark: | -| < 3.7.0 | :x: | - -## Reporting a Vulnerability - -[WordPress](https://wordpress.org/) is an open-source publishing platform. Our HackerOne program covers the Core software, as well as a variety of related projects and infrastructure. - -Our most critical targets are: - -* WordPress Core [software](https://wordpress.org/download/source/), [API](https://codex.wordpress.org/WordPress.org_API), and [website](https://wordpress.org/). -* Gutenberg [software](https://github.com/WordPress/gutenberg/) and Classic Editor [software](https://wordpress.org/plugins/classic-editor/). -* WP-CLI [software](https://github.com/wp-cli/) and [website](https://wp-cli.org/). -* BuddyPress [software](https://buddypress.org/download/) and [website](https://buddypress.org/). -* bbPress [software](https://bbpress.org/download/) and [website](https://bbpress.org/). -* GlotPress [software](https://github.com/glotpress/glotpress-wp) (but not the website). -* WordCamp.org [website](https://central.wordcamp.org). +Our HackerOne program covers the Core software, as well as a variety of related projects and infrastructure. -Source code for most websites can be found in the Meta repository (`git clone git://meta.git.wordpress.org/`). [The Meta Environment](https://github.com/WordPress/meta-environment) will automatically provision a local copy of some sites for you. +Full details of the WordPress Security Policy and the list of covered projects and infrastructure can be found on [HackerOne](https://hackerone.com/wordpress). You can also read more in a detailed white paper about [WordPress Security](https://wordpress.org/about/security/). -For more targets, see the `In Scope` section below. - -_Please note that **WordPress.com is a separate entity** from the main WordPress open source project. Please report vulnerabilities for WordPress.com or the WordPress mobile apps through [Automattic's HackerOne page](https://hackerone.com/automattic)._ - -## Qualifying Vulnerabilities - -Any reproducible vulnerability that has a severe effect on the security or privacy of our users is likely to be in scope for the program. Common examples include XSS, CSRF, SSRF, RCE, SQLi, and privilege escalation. - -We generally **aren’t** interested in the following problems: - -* Any vulnerability with a [CVSS 3](https://www.first.org/cvss/calculator/3.0) score lower than `4.0`, unless it can be combined with other vulnerabilities to achieve a higher score. -* Brute force, DoS, phishing, text injection, or social engineering attacks. Wikis, Tracs, forums, etc are intended to allow users to edit them. -* Security vulnerabilities in WordPress plugins not _specifically_ listed as an in-scope asset. Out of scope plugins can be [reported to the Plugin Review team](https://developer.wordpress.org/plugins/wordpress-org/plugin-developer-faq/#how-can-i-send-a-security-report). -* Reports for hacked websites. The site owner can [learn more about restoring their site](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/#ive-been-hacked-what-do-i-do-now). -* [Users with administrator or editor privileges can post arbitrary JavaScript](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/#why-are-some-users-allowed-to-post-unfiltered-html) -* [Disclosure of user IDs](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/#why-are-disclosures-of-usernames-or-user-ids-not-a-security-issue) -* Open API endpoints serving public data (Including [usernames and user IDs](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/#why-are-disclosures-of-usernames-or-user-ids-not-a-security-issue)) -* [Path disclosures for errors, warnings, or notices](https://make.wordpress.org/core/handbook/testing/reporting-security-vulnerabilities/#why-are-there-path-disclosures-when-directly-loading-certain-files) -* WordPress version number disclosure -* Mixed content warnings for passive assets like images and videos -* Lack of HTTP security headers (CSP, X-XSS, etc.) -* Output from automated scans - please manually verify issues and include a valid proof of concept. -* Any non-severe vulnerability on `irclogs.wordpress.org`, `lists.wordpress.org`, or any other low impact site. -* Clickjacking with minimal security implications -* Vulnerabilities in Composer/NPM `devDependencies`, unless there's a practical way to exploit it remotely. -* Theoretical vulnerabilities where you can't demonstrate a significant security impact with a PoC. - -## Guidelines +## Supported Versions -We're committed to working with security researchers to resolve the vulnerabilities they discover. You can help us by following these guidelines: +| Version | Supported | +|---------| --------- | +| 7.0.x | Yes | +| 6.9.x | Yes | +| 6.8.x | Yes | +| 6.7.x | Yes | +| 6.6.x | Yes | +| 6.5.x | Yes | +| 6.4.x | Yes | +| 6.3.x | Yes | +| 6.2.x | Yes | +| 6.1.x | Yes | +| 6.0.x | Yes | +| 5.9.x | Yes | +| 5.8.x | Yes | +| 5.7.x | Yes | +| 5.6.x | Yes | +| 5.5.x | Yes | +| 5.4.x | Yes | +| 5.3.x | Yes | +| 5.2.x | Yes | +| 5.1.x | Yes | +| 5.0.x | Yes | +| 4.9.x | Yes | +| 4.8.x | Yes | +| 4.7.x | Yes | +| < 4.7.0 | No | -* Follow [HackerOne's disclosure guidelines](https://www.hackerone.com/disclosure-guidelines). -* Pen-testing Production: - * Please **setup a local environment** instead whenever possible. Most of our code is open source (see above). - * If that's not possible, **limit any data access/modification** to the bare minimum necessary to reproduce a PoC. - * **_Don't_ automate form submissions!** That's very annoying for us, because it adds extra work for the volunteers who manage those systems, and reduces the signal/noise ratio in our communication channels. - * If you don't follow these guidelines **we will not award a bounty for the report.** -* Be Patient - Give us a reasonable time to correct the issue before you disclose the vulnerability. We care deeply about security, but we're an open-source project and our team is mostly comprised of volunteers. WordPress powers over 30% of the Web, so changes must undergo multiple levels of peer-review and testing, to make sure that they don't break millions of websites when they're installed automatically. +## Reporting a Vulnerability -We also expect you to comply with all applicable laws. You're responsible to pay any taxes associated with your bounties. +Security issues must be submitted via [HackerOne](https://hackerone.com/wordpress) and it is recommended you read the full policy document before submitting your report. diff --git a/composer.json b/composer.json index 7910a75687ce4..aee7a09524994 100644 --- a/composer.json +++ b/composer.json @@ -1,5 +1,6 @@ { "name": "wordpress/wordpress", + "version": "7.1.0", "license": "GPL-2.0-or-later", "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", "homepage": "https://wordpress.org", @@ -10,20 +11,38 @@ "issues": "https://core.trac.wordpress.org/" }, "require": { - "php": ">=5.6" + "ext-hash": "*", + "ext-json": "*", + "php": ">=7.4" + }, + "suggest": { + "ext-dom": "*" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.0", - "squizlabs/php_codesniffer": "3.6.0", - "wp-coding-standards/wpcs": "~2.3.0", - "phpcompatibility/phpcompatibility-wp": "~2.1.2", - "yoast/phpunit-polyfills": "^1.0.1" + "composer/ca-bundle": "1.5.11", + "squizlabs/php_codesniffer": "3.13.5", + "wp-coding-standards/wpcs": "~3.3.0", + "phpcompatibility/phpcompatibility-wp": "~2.1.3", + "phpstan/phpstan": "2.1.39", + "yoast/phpunit-polyfills": "^1.1.0" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "lock": false + }, + "autoload-dev": { + "psr-4": { + "WordPress\\PHPStan\\": "tests/phpstan/" + } }, "scripts": { + "phpstan": "@php ./vendor/bin/phpstan analyse --memory-limit=2G", "compat": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --standard=phpcompat.xml.dist --report=summary,source", "format": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcbf --report=summary,source", "lint": "@php ./vendor/squizlabs/php_codesniffer/bin/phpcs --report=summary,source", "lint:errors": "@lint -n", - "test": "@php ./vendor/phpunit/phpunit/phpunit" + "test": [ "Composer\\Config::disableProcessTimeout", "@php ./vendor/phpunit/phpunit/phpunit" ] } } diff --git a/docker-compose.yml b/docker-compose.yml index 7fe9b524c7aab..cc2ed8d94975e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,5 +1,3 @@ -version: '3.7' - services: ## @@ -25,7 +23,10 @@ services: command: /bin/sh -c "envsubst '$$LOCAL_DIR' < /etc/nginx/conf.d/default.template > /etc/nginx/conf.d/default.conf && exec nginx -g 'daemon off;'" depends_on: - - php + php: + condition: service_started + mysql: + condition: service_healthy ## # The PHP container. @@ -37,17 +38,27 @@ services: - wpdevnet environment: - - LOCAL_PHP_XDEBUG=${LOCAL_PHP_XDEBUG-false} - - LOCAL_PHP_MEMCACHED=${LOCAL_PHP_MEMCACHED-false} - - PHP_FPM_UID=${PHP_FPM_UID-1000} - - PHP_FPM_GID=${PHP_FPM_GID-1000} + LOCAL_PHP_XDEBUG: ${LOCAL_PHP_XDEBUG-false} + XDEBUG_MODE: ${LOCAL_PHP_XDEBUG_MODE-develop,debug} + LOCAL_PHP_MEMCACHED: ${LOCAL_PHP_MEMCACHED-false} + PHP_FPM_UID: ${PHP_FPM_UID-1000} + PHP_FPM_GID: ${PHP_FPM_GID-1000} + GITHUB_REF: ${GITHUB_REF-false} + GITHUB_EVENT_NAME: ${GITHUB_EVENT_NAME-false} + HOST_PATH: ${PWD-}/${LOCAL_DIR-src} volumes: - ./tools/local-env/php-config.ini:/usr/local/etc/php/conf.d/php-config.ini - ./:/var/www - depends_on: - - mysql + # Copy or delete the Memcached dropin plugin file as appropriate. + command: /bin/sh -c "if [ $LOCAL_PHP_MEMCACHED = true ]; then cp -n /var/www/tests/phpunit/includes/object-cache.php /var/www/src/wp-content/object-cache.php; else rm -f /var/www/src/wp-content/object-cache.php; fi && exec php-fpm" + + # The init directive ensures the command runs with a PID > 1, so Ctrl+C works correctly. + init: true + + extra_hosts: + - localhost:host-gateway ## # The MySQL container. @@ -59,7 +70,7 @@ services: - wpdevnet ports: - - "3306" + - "${LOCAL_DB_PORTS-3306}" environment: MYSQL_ROOT_PASSWORD: password @@ -68,8 +79,14 @@ services: - ./tools/local-env/mysql-init.sql:/docker-entrypoint-initdb.d/mysql-init.sql - mysql:/var/lib/mysql - # For compatibility with PHP versions that don't support the caching_sha2_password auth plugin used in MySQL 8.0. - command: --default-authentication-plugin=mysql_native_password + healthcheck: + test: [ + 'CMD-SHELL', + 'if [ "$LOCAL_DB_TYPE" = "mariadb" ]; then case "$LOCAL_DB_VERSION" in 5.5|10.0|10.1|10.2|10.3) mysqladmin ping -h localhost || exit $$?;; *) mariadb-admin ping -h localhost || exit $$?;; esac; else mysqladmin ping -h localhost || exit $$?; fi' + ] + timeout: 5s + interval: 5s + retries: 10 ## # The WP CLI container. @@ -81,17 +98,47 @@ services: - wpdevnet environment: - - LOCAL_PHP_XDEBUG=${LOCAL_PHP_XDEBUG-false} - - LOCAL_PHP_MEMCACHED=${LOCAL_PHP_MEMCACHED-false} - - PHP_FPM_UID=${PHP_FPM_UID-1000} - - PHP_FPM_GID=${PHP_FPM_GID-1000} + LOCAL_PHP_XDEBUG: ${LOCAL_PHP_XDEBUG-false} + LOCAL_PHP_MEMCACHED: ${LOCAL_PHP_MEMCACHED-false} + PHP_FPM_UID: ${PHP_FPM_UID-1000} + PHP_FPM_GID: ${PHP_FPM_GID-1000} + HOST_PATH: ${PWD-}/${LOCAL_DIR-src} + WP_CONFIG_PATH: /var/www/wp-config.php volumes: - ./:/var/www + # Keeps the service alive. + command: 'sleep infinity' + # The init directive ensures the command runs with a PID > 1, so Ctrl+C works correctly. init: true + extra_hosts: + - localhost:host-gateway + + depends_on: + php: + condition: service_started + mysql: + condition: service_healthy + + ## + # The Memcached container. + ## + memcached: + image: memcached + + networks: + - wpdevnet + + ports: + - 11211:11211 + + depends_on: + php: + condition: service_started + volumes: # So that sites aren't wiped every time containers are restarted, MySQL uses a persistent volume. mysql: {} diff --git a/jsdoc.conf.json b/jsdoc.conf.json index 6b966970fc70c..2e7058a96d3dd 100644 --- a/jsdoc.conf.json +++ b/jsdoc.conf.json @@ -19,7 +19,6 @@ "linenums": true }, "opts": { - "template": "./node_modules/ink-docstrap/template", "recurse": true, "private": true } diff --git a/package-lock.json b/package-lock.json index 68a5b5998827c..c043f24fa34c5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,24672 +1,31214 @@ { "name": "WordPress", - "version": "5.9.0", - "lockfileVersion": 1, + "version": "7.1.0", + "lockfileVersion": 3, "requires": true, - "dependencies": { - "@babel/code-frame": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.14.5.tgz", - "integrity": "sha512-9pzDqyc6OLDaqe+zbACgFkb6fKMNG6CObKpnYXChRsvYGyEdc7CA2BaqeOM+vOtCS5ndmJicPJhKAwYRI6UfFw==", - "requires": { - "@babel/highlight": "^7.14.5" + "packages": { + "": { + "name": "WordPress", + "version": "7.1.0", + "license": "GPL-2.0-or-later", + "dependencies": { + "backbone": "1.6.1", + "clipboard": "2.0.11", + "codemirror": "5.65.20", + "core-js-url-browser": "3.6.4", + "csslint": "1.0.5", + "element-closest": "3.0.2", + "espree": "9.6.1", + "esprima": "4.0.1", + "formdata-polyfill": "4.0.10", + "hoverintent": "2.2.1", + "htmlhint": "1.8.0", + "imagesloaded": "5.0.0", + "jquery": "3.7.1", + "jquery-color": "3.0.0", + "jquery-form": "4.3.0", + "jquery-hoverintent": "1.10.2", + "jsonlint": "1.6.3", + "lodash": "4.18.1", + "masonry-layout": "4.2.2", + "moment": "2.30.1", + "objectFitPolyfill": "2.3.5", + "polyfill-library": "4.8.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-is": "18.3.1", + "regenerator-runtime": "0.14.1", + "underscore": "1.13.8", + "whatwg-fetch": "3.6.20", + "wicg-inert": "3.1.3" + }, + "devDependencies": { + "@lodder/grunt-postcss": "^3.1.1", + "@playwright/test": "1.58.2", + "@pmmmwh/react-refresh-webpack-plugin": "0.6.2", + "@types/codemirror": "5.60.17", + "@types/espree": "10.1.0", + "@types/htmlhint": "1.1.5", + "@types/jquery": "3.5.34", + "@types/underscore": "1.13.0", + "@wordpress/e2e-test-utils-playwright": "1.42.0", + "@wordpress/prettier-config": "4.42.0", + "@wordpress/scripts": "31.7.0", + "autoprefixer": "10.4.27", + "chalk": "5.6.2", + "check-node-version": "4.2.1", + "cssnano": "7.1.3", + "dotenv": "17.3.1", + "dotenv-expand": "12.0.3", + "grunt": "1.6.1", + "grunt-banner": "^0.6.0", + "grunt-contrib-clean": "~2.0.1", + "grunt-contrib-concat": "2.1.0", + "grunt-contrib-copy": "~1.0.0", + "grunt-contrib-cssmin": "~5.0.0", + "grunt-contrib-imagemin": "~4.0.0", + "grunt-contrib-jshint": "3.2.0", + "grunt-contrib-qunit": "~10.1.1", + "grunt-contrib-uglify": "~5.2.2", + "grunt-contrib-watch": "~1.1.0", + "grunt-file-append": "0.0.7", + "grunt-jsdoc": "2.4.1", + "grunt-legacy-util": "^2.0.1", + "grunt-patch-wordpress": "~4.0.0", + "grunt-replace-lts": "~1.1.0", + "grunt-rtlcss": "~2.0.2", + "grunt-sass": "~4.1.0", + "grunt-webpack": "7.0.1", + "install-changed": "1.1.0", + "json2php": "0.0.12", + "php-array-reader": "2.1.3", + "postcss": "8.5.8", + "prettier": "npm:wp-prettier@3.0.3", + "qunit": "~2.25.0", + "react-refresh": "0.14.0", + "sass": "1.98.0", + "sinon": "16.1.3", + "sinon-test": "~3.1.6", + "source-map-loader": "5.0.0", + "terser-webpack-plugin": "5.4.0", + "typescript": "5.9.3", + "uuid": "13.0.0", + "wait-on": "9.0.4", + "webpack": "5.105.4" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.0.tgz", + "integrity": "sha512-qRmjj8nj9qmLTQXXmaR1cck3UXSRMPrbsLJAasZpF+t3riI71BXed5ebIOYwQntykeZuhjsdweEc9BxH5Jc26w==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.1.0", + "@jridgewell/trace-mapping": "^0.3.9" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/compat-data": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.15.0.tgz", - "integrity": "sha512-0NqAC1IJE0S0+lL1SWFMxMkz1pKCNCjI4tr2Zx4LJSXxCLAdr6KyArnY+sno5m3yH9g737ygOyPABDsnXkpxiA==", - "dev": true - }, - "@babel/core": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.15.0.tgz", - "integrity": "sha512-tXtmTminrze5HEUPn/a0JtOzzfp0nk+UEXQ/tqIJo3WDGypl/2OFQEMll/zSFU8f/lfmfLXvTaORHF3cfXIQMw==", - "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.0", - "@babel/helper-compilation-targets": "^7.15.0", - "@babel/helper-module-transforms": "^7.15.0", - "@babel/helpers": "^7.14.8", - "@babel/parser": "^7.15.0", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.15.0", - "@babel/types": "^7.15.0", - "convert-source-map": "^1.7.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.1.2", - "semver": "^6.3.0", - "source-map": "^0.5.0" - }, + "node_modules/@asamuzakjp/css-color": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/@asamuzakjp/css-color/-/css-color-3.2.0.tgz", + "integrity": "sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==", + "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", - "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@csstools/css-calc": "^2.1.3", + "@csstools/css-color-parser": "^3.0.9", + "@csstools/css-parser-algorithms": "^3.0.4", + "@csstools/css-tokenizer": "^3.0.3", + "lru-cache": "^10.4.3" } }, - "@babel/generator": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.15.0.tgz", - "integrity": "sha512-eKl4XdMrbpYvuB505KTta4AV9g+wWzmVBW69tX0H2NwKVKd2YJbKgyK6M8j/rgLbmHOYJn6rUklV677nOyJrEQ==", + "node_modules/@asamuzakjp/css-color/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", "dev": true, - "requires": { - "@babel/types": "^7.15.0", - "jsesc": "^2.5.1", - "source-map": "^0.5.0" - } + "license": "ISC" }, - "@babel/helper-annotate-as-pure": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.14.5.tgz", - "integrity": "sha512-EivH9EgBIb+G8ij1B2jAwSH36WnGvkQSEC6CkX/6v6ZFlw5fVOHvsgGF4uiEHO2GzMvunZb6tDLQEQSdrdocrA==", + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-builder-binary-assignment-operator-visitor": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.14.5.tgz", - "integrity": "sha512-YTA/Twn0vBXDVGJuAX6PwW7x5zQei1luDDo2Pl6q1qZ7hVNl0RZrhHCQG/ArGpR29Vl7ETiB8eJyrvpuRp300w==", + "node_modules/@babel/compat-data": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", + "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", "dev": true, - "requires": { - "@babel/helper-explode-assignable-expression": "^7.14.5", - "@babel/types": "^7.14.5" + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-compilation-targets": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.15.0.tgz", - "integrity": "sha512-h+/9t0ncd4jfZ8wsdAsoIxSa61qhBYlycXiHWqJaQBCXAhDCMbPRSMTGnZIkkmt1u4ag+UQmuqcILwqKzZ4N2A==", + "node_modules/@babel/core": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.7.tgz", + "integrity": "sha512-yJ474Zv3cwiSOO9nXJuqzvwEeM+chDuQ8GJirw+pZ91sCGCyOZ3dJkVE09fTV0VEVzXyLWhh3G/AolYTPX7Mow==", "dev": true, - "requires": { - "@babel/compat-data": "^7.15.0", - "@babel/helper-validator-option": "^7.14.5", - "browserslist": "^4.16.6", - "semver": "^6.3.0" - }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.25.7", + "@babel/generator": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-module-transforms": "^7.25.7", + "@babel/helpers": "^7.25.7", + "@babel/parser": "^7.25.7", + "@babel/template": "^7.25.7", + "@babel/traverse": "^7.25.7", + "@babel/types": "^7.25.7", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" } }, - "@babel/helper-create-class-features-plugin": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.15.0.tgz", - "integrity": "sha512-MdmDXgvTIi4heDVX/e9EFfeGpugqm9fobBVg/iioE8kueXrOHdRDe36FAY7SnE9xXLVeYCoJR/gdrBEIHRC83Q==", + "node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-member-expression-to-functions": "^7.15.0", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/helper-replace-supers": "^7.15.0", - "@babel/helper-split-export-declaration": "^7.14.5" - } + "license": "MIT" }, - "@babel/helper-create-regexp-features-plugin": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.14.5.tgz", - "integrity": "sha512-TLawwqpOErY2HhWbGJ2nZT5wSkR192QpN+nBg1THfBfftrlvOh+WbhrxXCH4q4xJ9Gl16BGPR/48JA+Ryiho/A==", + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "regexpu-core": "^4.7.1" + "bin": { + "semver": "bin/semver.js" } }, - "@babel/helper-define-polyfill-provider": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.2.3.tgz", - "integrity": "sha512-RH3QDAfRMzj7+0Nqu5oqgO5q9mFtQEVvCRsi8qCEfzLR9p2BHfn5FzhSB2oj1fF7I2+DcTORkYaQ6aTR9Cofew==", - "dev": true, - "requires": { - "@babel/helper-compilation-targets": "^7.13.0", - "@babel/helper-module-imports": "^7.12.13", - "@babel/helper-plugin-utils": "^7.13.0", - "@babel/traverse": "^7.13.0", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2", - "semver": "^6.1.2" - }, + "node_modules/@babel/eslint-parser": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/eslint-parser/-/eslint-parser-7.25.7.tgz", + "integrity": "sha512-B+BO9x86VYsQHimucBAL1fxTJKF4wyKY6ZVzee9QgzdZOUfs3BaR6AQrgoGrRI+7IFS1wUz/VyQ+SoBcSpdPbw==", + "dev": true, + "license": "MIT", "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@nicolo-ribaudo/eslint-scope-5-internals": "5.1.1-v1", + "eslint-visitor-keys": "^2.1.0", + "semver": "^6.3.1" + }, + "engines": { + "node": "^10.13.0 || ^12.13.0 || >=14.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.11.0", + "eslint": "^7.5.0 || ^8.0.0 || ^9.0.0" } }, - "@babel/helper-explode-assignable-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-explode-assignable-expression/-/helper-explode-assignable-expression-7.14.5.tgz", - "integrity": "sha512-Htb24gnGJdIGT4vnRKMdoXiOIlqOLmdiUYpAQ0mYfgVT/GDm8GOYhgi4GL+hMKrkiPRohO4ts34ELFsGAPQLDQ==", + "node_modules/@babel/eslint-parser/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "@babel/helper-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.14.5.tgz", - "integrity": "sha512-Gjna0AsXWfFvrAuX+VKcN/aNNWonizBj39yGwUzVDVTlMYJMK2Wp6xdpy72mfArFq5uK+NOuexfzZlzI1z9+AQ==", + "node_modules/@babel/generator": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", + "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", "dev": true, - "requires": { - "@babel/helper-get-function-arity": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/parser": "^7.26.5", + "@babel/types": "^7.26.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-get-function-arity": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-get-function-arity/-/helper-get-function-arity-7.14.5.tgz", - "integrity": "sha512-I1Db4Shst5lewOM4V+ZKJzQ0JGGaZ6VY1jYvMghRjqs6DWgxLCIyFt30GlnKkfUeFLpJt2vzbMVEXVSXlIFYUg==", + "node_modules/@babel/generator/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "@babel/helper-hoist-variables": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.14.5.tgz", - "integrity": "sha512-R1PXiz31Uc0Vxy4OEOm07x0oSjKAdPPCh3tPivn/Eo8cvz6gveAeuyUUPB21Hoiif0uoPQSSdhIPS3352nvdyQ==", + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", + "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-member-expression-to-functions": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.15.0.tgz", - "integrity": "sha512-Jq8H8U2kYiafuj2xMTPQwkTBnEEdGKpT35lJEQsRRjnG0LW3neucsaMWLgKcwu3OHKNeYugfw+Z20BXBSEs2Lg==", + "node_modules/@babel/helper-compilation-targets": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", + "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", "dev": true, - "requires": { - "@babel/types": "^7.15.0" - } - }, - "@babel/helper-module-imports": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.14.5.tgz", - "integrity": "sha512-SwrNHu5QWS84XlHwGYPDtCxcA0hrSlL2yhWYLgeOc0w7ccOl2qv4s/nARI0aYZW+bSwAL5CukeXA47B/1NKcnQ==", - "requires": { - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/compat-data": "^7.26.5", + "@babel/helper-validator-option": "^7.25.9", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helper-module-transforms": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.15.0.tgz", - "integrity": "sha512-RkGiW5Rer7fpXv9m1B3iHIFDZdItnO2/BLfWVW/9q7+KqQSDY5kUfQEbzdXM1MVhJGcugKV7kRrNVzNxmk7NBg==", + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-replace-supers": "^7.15.0", - "@babel/helper-simple-access": "^7.14.8", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.9", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.15.0", - "@babel/types": "^7.15.0" + "dependencies": { + "yallist": "^3.0.2" } }, - "@babel/helper-optimise-call-expression": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.14.5.tgz", - "integrity": "sha512-IqiLIrODUOdnPU9/F8ib1Fx2ohlgDhxnIDU7OEVi+kAbEZcyiF7BLU8W6PfvPi9LzztjS7kcbzbmL7oG8kD6VA==", + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "bin": { + "semver": "bin/semver.js" } }, - "@babel/helper-plugin-utils": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.14.5.tgz", - "integrity": "sha512-/37qQCE3K0vvZKwoK4XU/irIJQdIfCJuhU5eKnNxpFDsOkgFaUAwbv+RYw6eYgsC0E4hS7r5KqGULUogqui0fQ==", + "node_modules/@babel/helper-compilation-targets/node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", "dev": true }, - "@babel/helper-remap-async-to-generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.14.5.tgz", - "integrity": "sha512-rLQKdQU+HYlxBwQIj8dk4/0ENOUEhA/Z0l4hN8BexpvmSMN9oA9EagjnhnDpNsRdWCfjwa4mn/HyBXO9yhQP6A==", + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", + "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-wrap-function": "^7.14.5", - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/helper-replace-supers": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.15.0.tgz", - "integrity": "sha512-6O+eWrhx+HEra/uJnifCwhwMd6Bp5+ZfZeJwbqUTuqkhIT6YcRhiZCOOFChRypOIe0cV46kFrRBlm+t5vHCEaA==", + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/helper-member-expression-to-functions": "^7.15.0", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/traverse": "^7.15.0", - "@babel/types": "^7.15.0" + "bin": { + "semver": "bin/semver.js" } }, - "@babel/helper-simple-access": { - "version": "7.14.8", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.14.8.tgz", - "integrity": "sha512-TrFN4RHh9gnWEU+s7JloIho2T76GPwRHhdzOWLqTrMnlas8T9O7ec+oEDNsRXndOmru9ymH9DFrEOxpzPoSbdg==", + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", + "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", "dev": true, - "requires": { - "@babel/types": "^7.14.8" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "regexpu-core": "^6.2.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.14.5.tgz", - "integrity": "sha512-dmqZB7mrb94PZSAOYtr+ZN5qt5owZIAgqtoTuqiFbHFtxgEcmQlRJVI+bO++fciBunXtB6MK7HrzrfcAzIz2NQ==", + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "bin": { + "semver": "bin/semver.js" } }, - "@babel/helper-split-export-declaration": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.14.5.tgz", - "integrity": "sha512-hprxVPu6e5Kdp2puZUmvOGjaLv9TCe58E/Fl6hRq4YiVQxIcNvuq6uTM2r1mT/oPskuS9CgR+I94sqAYv0NGKA==", + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", + "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", "dev": true, - "requires": { - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "@babel/helper-validator-identifier": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.14.9.tgz", - "integrity": "sha512-pQYxPY0UP6IHISRitNe8bsijHex4TWZXi2HwKVsjPiltzlhse2znVcm9Ace510VT1kxIHjGJCZZQBX2gJDbo0g==" - }, - "@babel/helper-validator-option": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.14.5.tgz", - "integrity": "sha512-OX8D5eeX4XwcroVW45NMvoYaIuFI+GQpA2a8Gi+X/U/cDUIRsV37qQfF905F0htTRCREQIB4KqPeaveRJUl3Ow==", - "dev": true - }, - "@babel/helper-wrap-function": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.14.5.tgz", - "integrity": "sha512-YEdjTCq+LNuNS1WfxsDCNpgXkJaIyqco6DAelTUjT4f2KIWC1nBcaCaSdHTBqQVLnTBexBcVcFhLSU1KnYuePQ==", + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", + "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", "dev": true, - "requires": { - "@babel/helper-function-name": "^7.14.5", - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.14.5", - "@babel/types": "^7.14.5" + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/helpers": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.15.3.tgz", - "integrity": "sha512-HwJiz52XaS96lX+28Tnbu31VeFSQJGOeKHJeaEPQlTl7PnlhFElWPj8tUXtqFIzeN86XxXoBr+WFAyK2PPVz6g==", + "node_modules/@babel/helper-module-imports": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", + "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", "dev": true, - "requires": { - "@babel/template": "^7.14.5", - "@babel/traverse": "^7.15.0", - "@babel/types": "^7.15.0" + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/highlight": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.14.5.tgz", - "integrity": "sha512-qf9u2WFWVV0MppaL877j2dBtQIDgmidgjGk5VIMw3OadXvYaXn66U1BFlH2t4+t3i+8PhedppRv+i40ABzd+gg==", - "requires": { - "@babel/helper-validator-identifier": "^7.14.5", - "chalk": "^2.0.0", - "js-tokens": "^4.0.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "node_modules/@babel/helper-module-transforms": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", + "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/parser": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.12.11.tgz", - "integrity": "sha512-N3UxG+uuF4CMYoNj8AhnbAcJF0PiuJ9KHuy1lQmkYsxTer/MAH9UBNHsBoAX/4s6NvlDD047No8mYVGGzLL4hg==", - "dev": true + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", + "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.14.5.tgz", - "integrity": "sha512-ZoJS2XCKPBfTmL122iP6NM9dOg+d4lc9fFk3zxc8iDjvt8Pk4+TlsHSKhIPf6X+L5ORCdBzqMZDjL/WHj7WknQ==", + "node_modules/@babel/helper-plugin-utils": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", + "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5" + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-async-generator-functions": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-async-generator-functions/-/plugin-proposal-async-generator-functions-7.14.9.tgz", - "integrity": "sha512-d1lnh+ZnKrFKwtTYdw320+sQWCTwgkB9fmUhNXRADA4akR6wLjaruSGnIEUjpt9HCOwTr4ynFTKu19b7rFRpmw==", + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", + "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.14.5", - "@babel/plugin-syntax-async-generators": "^7.8.4" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-wrap-function": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-class-properties": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-properties/-/plugin-proposal-class-properties-7.14.5.tgz", - "integrity": "sha512-q/PLpv5Ko4dVc1LYMpCY7RVAAO4uk55qPwrIuJ5QJ8c6cVuAmhu7I/49JOppXL6gXf7ZHzpRVEUZdYoPLM04Gg==", + "node_modules/@babel/helper-replace-supers": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", + "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.25.9", + "@babel/helper-optimise-call-expression": "^7.25.9", + "@babel/traverse": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-class-static-block": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-class-static-block/-/plugin-proposal-class-static-block-7.14.5.tgz", - "integrity": "sha512-KBAH5ksEnYHCegqseI5N9skTdxgJdmDoAOc0uXa+4QMYKeZD0w5IARh4FMlTNtaHhbB8v+KzMdTgxMMzsIy6Yg==", + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", + "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-class-static-block": "^7.14.5" + "dependencies": { + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-dynamic-import": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-dynamic-import/-/plugin-proposal-dynamic-import-7.14.5.tgz", - "integrity": "sha512-ExjiNYc3HDN5PXJx+bwC50GIx/KKanX2HiggnIUAYedbARdImiCU4RhhHfdf0Kd7JNXGpsBBBCOm+bBVy3Gb0g==", + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-export-namespace-from": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-export-namespace-from/-/plugin-proposal-export-namespace-from-7.14.5.tgz", - "integrity": "sha512-g5POA32bXPMmSBu5Dx/iZGLGnKmKPc5AiY7qfZgurzrCYgIztDlHFbznSNCoQuv57YQLnQfaDi7dxCtLDIdXdA==", + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-json-strings": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-json-strings/-/plugin-proposal-json-strings-7.14.5.tgz", - "integrity": "sha512-NSq2fczJYKVRIsUJyNxrVUMhB27zb7N7pOFGQOhBKJrChbGcgEAqyZrmZswkPk18VMurEeJAaICbfm57vUeTbQ==", + "node_modules/@babel/helper-validator-option": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", + "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-json-strings": "^7.8.3" + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-logical-assignment-operators": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-logical-assignment-operators/-/plugin-proposal-logical-assignment-operators-7.14.5.tgz", - "integrity": "sha512-YGn2AvZAo9TwyhlLvCCWxD90Xq8xJ4aSgaX3G5D/8DW94L8aaT+dS5cSP+Z06+rCJERGSr9GxMBZ601xoc2taw==", + "node_modules/@babel/helper-wrap-function": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", + "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + "dependencies": { + "@babel/template": "^7.25.9", + "@babel/traverse": "^7.25.9", + "@babel/types": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-nullish-coalescing-operator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-nullish-coalescing-operator/-/plugin-proposal-nullish-coalescing-operator-7.14.5.tgz", - "integrity": "sha512-gun/SOnMqjSb98Nkaq2rTKMwervfdAoz6NphdY0vTfuzMfryj+tDGb2n6UkDKwez+Y8PZDhE3D143v6Gepp4Hg==", + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" } }, - "@babel/plugin-proposal-numeric-separator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-numeric-separator/-/plugin-proposal-numeric-separator-7.14.5.tgz", - "integrity": "sha512-yiclALKe0vyZRZE0pS6RXgjUOt87GWv6FYa5zqj15PvhOGFO69R5DusPlgK/1K5dVnCtegTiWu9UaBSrLLJJBg==", + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-numeric-separator": "^7.10.4" + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" } }, - "@babel/plugin-proposal-object-rest-spread": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-object-rest-spread/-/plugin-proposal-object-rest-spread-7.14.7.tgz", - "integrity": "sha512-082hsZz+sVabfmDWo1Oct1u1AgbKbUAyVgmX4otIc7bdsRgHBXwTwb3DpDmD4Eyyx6DNiuz5UAATT655k+kL5g==", + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", + "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", "dev": true, - "requires": { - "@babel/compat-data": "^7.14.7", - "@babel/helper-compilation-targets": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-transform-parameters": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-optional-catch-binding": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-catch-binding/-/plugin-proposal-optional-catch-binding-7.14.5.tgz", - "integrity": "sha512-3Oyiixm0ur7bzO5ybNcZFlmVsygSIQgdOa7cTfOYCMY+wEPAYhZAJxi3mixKFCTCKUhQXuCTtQ1MzrpL3WT8ZQ==", + "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", + "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-optional-chaining": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-optional-chaining/-/plugin-proposal-optional-chaining-7.14.5.tgz", - "integrity": "sha512-ycz+VOzo2UbWNI1rQXxIuMOzrDdHGrI23fRiz/Si2R4kv2XZQ1BK8ccdHwehMKBlcH/joGW/tzrUmo67gbJHlQ==", + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", + "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5", - "@babel/plugin-syntax-optional-chaining": "^7.8.3" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-private-methods": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-methods/-/plugin-proposal-private-methods-7.14.5.tgz", - "integrity": "sha512-838DkdUA1u+QTCplatfq4B7+1lnDa/+QMI89x5WZHBcnNv+47N8QEj2k9I2MUU9xIv8XJ4XvPCviM/Dj7Uwt9g==", + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-transform-optional-chaining": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" } }, - "@babel/plugin-proposal-private-property-in-object": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.14.5.tgz", - "integrity": "sha512-62EyfyA3WA0mZiF2e2IV9mc9Ghwxcg8YTu8BS4Wss4Y3PY725OmS9M0qLORbJwLqFtGh+jiE4wAmocK2CTUK2Q==", + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", + "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-create-class-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-proposal-unicode-property-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-unicode-property-regex/-/plugin-proposal-unicode-property-regex-7.14.5.tgz", - "integrity": "sha512-6axIeOU5LnY471KenAB9vI8I5j7NQ2d652hIYwVyRfgaZT5UpiqFKCuVXCDMSrU+3VFafnu2c5m3lrWIlr6A5Q==", + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-async-generators": { + "node_modules/@babel/plugin-syntax-async-generators": { "version": "7.8.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-bigint": { + "node_modules/@babel/plugin-syntax-bigint": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-bigint/-/plugin-syntax-bigint-7.8.3.tgz", "integrity": "sha512-wnTnFlG+YxQm3vDxpGE57Pj0srRU4sHE/mDkt1qv2YJJSeUAec2ma4WLUnUPeKjyrfntVwe/N6dCXpU+zL3Npg==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-class-properties": { + "node_modules/@babel/plugin-syntax-class-properties": { "version": "7.12.13", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-class-static-block": { + "node_modules/@babel/plugin-syntax-class-static-block": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-dynamic-import": { + "node_modules/@babel/plugin-syntax-dynamic-import": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-export-namespace-from": { + "node_modules/@babel/plugin-syntax-export-namespace-from": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", + "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", + "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-import-meta": { + "node_modules/@babel/plugin-syntax-import-meta": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-json-strings": { + "node_modules/@babel/plugin-syntax-json-strings": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-jsx": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.14.5.tgz", - "integrity": "sha512-ohuFIsOMXJnbOMRfX7/w7LocdR6R7whhuRD4ax8IipLcLPlZGJKkBxgHp++U4N/vKyU16/YDQr2f5seajD3jIw==", + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.25.9.tgz", + "integrity": "sha512-ld6oezHQMZsZfp6pWtbjaNDF2tiiCYYDqQszHt5VV437lewP9aSi2Of99CK0D0XB21k7FLgnLcmQKyKzynfeAA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-logical-assignment-operators": { + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-nullish-coalescing-operator": { + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-numeric-separator": { + "node_modules/@babel/plugin-syntax-numeric-separator": { "version": "7.10.4", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-object-rest-spread": { + "node_modules/@babel/plugin-syntax-object-rest-spread": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-catch-binding": { + "node_modules/@babel/plugin-syntax-optional-catch-binding": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-optional-chaining": { + "node_modules/@babel/plugin-syntax-optional-chaining": { "version": "7.8.3", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-private-property-in-object": { + "node_modules/@babel/plugin-syntax-private-property-in-object": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-top-level-await": { + "node_modules/@babel/plugin-syntax-top-level-await": { "version": "7.14.5", "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", "dev": true, - "requires": { + "dependencies": { "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-syntax-typescript": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.14.5.tgz", - "integrity": "sha512-u6OXzDaIXjEstBRRoBCQ/uKQKlbuaeE5in0RvWdA4pN6AhqxTIwUsnHPU1CFZA/amYObMsuWhYfRl3Ch90HD0Q==", + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.25.9.tgz", + "integrity": "sha512-hjMgRy5hb8uJJjUcdWunWVcoi9bGpJp8p5Ol1229PoN6aytsLwNMgmdftO23wnCLMfVmTwZDWMPNq/D1SY60JQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-arrow-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.14.5.tgz", - "integrity": "sha512-KOnO0l4+tD5IfOdi4x8C1XmEIRWUjNRV8wc6K2vz/3e8yAOoZZvsRXRRIF/yo/MAOFb4QjtAw9xSxMXbSMRy8A==", + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-transform-async-to-generator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.14.5.tgz", - "integrity": "sha512-szkbzQ0mNk0rpu76fzDdqSyPu0MuvpXgC+6rz5rpMb5OIRxdmHfQxrktL8CYolL2d8luMCZTR0DpIMIdL27IjA==", + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", + "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-remap-async-to-generator": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-block-scoped-functions": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.14.5.tgz", - "integrity": "sha512-dtqWqdWZ5NqBX3KzsVCWfQI3A53Ft5pWFCT2eCVUftWZgjc5DpDponbIF1+c+7cSGk2wN0YK7HGL/ezfRbpKBQ==", + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", + "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-block-scoping": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.15.3.tgz", - "integrity": "sha512-nBAzfZwZb4DkaGtOes1Up1nOAp9TDRRFw4XBzBBSG9QK7KVFmYzgj9o9sbPv7TX5ofL4Auq4wZnxCoPnI/lz2Q==", + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", + "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-module-imports": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-remap-async-to-generator": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-classes": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.14.9.tgz", - "integrity": "sha512-NfZpTcxU3foGWbl4wxmZ35mTsYJy8oQocbeIMoDAGGFarAmSQlL+LWMkDx/tj6pNotpbX3rltIA4dprgAPOq5A==", + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.26.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", + "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-optimise-call-expression": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "globals": "^11.1.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-computed-properties": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.14.5.tgz", - "integrity": "sha512-pWM+E4283UxaVzLb8UBXv4EIxMovU4zxT1OPnpHJcmnvyY9QbPPTKZfEj31EUvG3/EQRbYAGaYEUZ4yWOBC2xg==", + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", + "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-destructuring": { - "version": "7.14.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.14.7.tgz", - "integrity": "sha512-0mDE99nK+kVh3xlc5vKwB6wnP9ecuSj+zQCa/n0voENtP/zymdT4HH6QEb65wjjcbqr1Jb/7z9Qp7TF5FtwYGw==", + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", + "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-dotall-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.14.5.tgz", - "integrity": "sha512-loGlnBdj02MDsFaHhAIJzh7euK89lBrGIdM9EAtHFo6xKygCUGuuWe07o1oZVk287amtW1n0808sQM99aZt3gw==", + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.26.0", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", + "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" } }, - "@babel/plugin-transform-duplicate-keys": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.14.5.tgz", - "integrity": "sha512-iJjbI53huKbPDAsJ8EmVmvCKeeq21bAze4fu9GBQtSLqfvzj2oRuHVx4ZkDwEhg1htQ+5OBZh/Ab0XDf5iBZ7A==", + "node_modules/@babel/plugin-transform-classes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", + "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9", + "@babel/traverse": "^7.25.9", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-exponentiation-operator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.14.5.tgz", - "integrity": "sha512-jFazJhMBc9D27o9jDnIE5ZErI0R0m7PbKXVq77FFvqFbzvTMuv8jaAwLZ5PviOLSFttqKIW0/wxNSDbjLk0tYA==", + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", + "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", "dev": true, - "requires": { - "@babel/helper-builder-binary-assignment-operator-visitor": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/template": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-for-of": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.14.5.tgz", - "integrity": "sha512-CfmqxSUZzBl0rSjpoQSFoR9UEj3HzbGuGNL21/iFTmjb5gFggJp3ph0xR1YBhexmLoKRHzgxuFvty2xdSt6gTA==", + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", + "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-function-name": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.14.5.tgz", - "integrity": "sha512-vbO6kv0fIzZ1GpmGQuvbwwm+O4Cbm2NrPzwlup9+/3fdkuzo1YqOZcXw26+YUJB84Ja7j9yURWposEHLYwxUfQ==", + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", + "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", "dev": true, - "requires": { - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" - } - }, - "@babel/plugin-transform-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.14.5.tgz", - "integrity": "sha512-ql33+epql2F49bi8aHXxvLURHkxJbSmMKl9J5yHqg4PLtdE6Uc48CH1GS6TQvZ86eoB/ApZXwm7jlA+B3kra7A==", - "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-member-expression-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.14.5.tgz", - "integrity": "sha512-WkNXxH1VXVTKarWFqmso83xl+2V3Eo28YY5utIkbsmXoItO8Q3aZxN4BTS2k0hz9dGUloHK26mJMyQEYfkn/+Q==", + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", + "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-modules-amd": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.14.5.tgz", - "integrity": "sha512-3lpOU8Vxmp3roC4vzFpSdEpGUWSMsHFreTWOMMLzel2gNGfHE5UWIh/LN6ghHs2xurUp4jRFYMUIZhuFbody1g==", + "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "babel-plugin-dynamic-import-node": "^2.3.3" + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-transform-modules-commonjs": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.15.0.tgz", - "integrity": "sha512-3H/R9s8cXcOGE8kgMlmjYYC9nqr5ELiPkJn4q0mypBrjhYQoc+5/Maq69vV4xRPWnkzZuwJPf5rArxpB/35Cig==", + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", + "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.15.0", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-simple-access": "^7.14.8", - "babel-plugin-dynamic-import-node": "^2.3.3" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-modules-systemjs": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.14.5.tgz", - "integrity": "sha512-mNMQdvBEE5DcMQaL5LbzXFMANrQjd2W7FPzg34Y4yEz7dBgdaC+9B84dSO+/1Wba98zoDbInctCDo4JGxz1VYA==", + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", + "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", "dev": true, - "requires": { - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-identifier": "^7.14.5", - "babel-plugin-dynamic-import-node": "^2.3.3" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-modules-umd": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.14.5.tgz", - "integrity": "sha512-RfPGoagSngC06LsGUYyM9QWSXZ8MysEjDJTAea1lqRjNECE3y0qIJF/qbvJxc4oA4s99HumIMdXOrd+TdKaAAA==", + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", + "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", "dev": true, - "requires": { - "@babel/helper-module-transforms": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.14.9.tgz", - "integrity": "sha512-l666wCVYO75mlAtGFfyFwnWmIXQm3kSH0C3IRnJqWcZbWkoihyAdDhFm2ZWaxWTqvBvhVFfJjMRQ0ez4oN1yYA==", + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", + "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-new-target": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.14.5.tgz", - "integrity": "sha512-Nx054zovz6IIRWEB49RDRuXGI4Gy0GMgqG0cII9L3MxqgXz/+rgII+RU58qpo4g7tNEx1jG7rRVH4ihZoP4esQ==", + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", + "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-object-super": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.14.5.tgz", - "integrity": "sha512-MKfOBWzK0pZIrav9z/hkRqIk/2bTv9qvxHzPQc12RcVkMOzpIKnFCNYJip00ssKWYkd8Sf5g0Wr7pqJ+cmtuFg==", + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", + "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-replace-supers": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-parameters": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.14.5.tgz", - "integrity": "sha512-Tl7LWdr6HUxTmzQtzuU14SqbgrSKmaR77M0OKyq4njZLQTPfOvzblNKyNkGwOfEFCEx7KeYHQHDI0P3F02IVkA==", + "node_modules/@babel/plugin-transform-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", + "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-property-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.14.5.tgz", - "integrity": "sha512-r1uilDthkgXW8Z1vJz2dKYLV1tuw2xsbrp3MrZmD99Wh9vsfKoob+JTgri5VUb/JqyKRXotlOtwgu4stIYCmnw==", + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", + "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-react-constant-elements": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.14.5.tgz", - "integrity": "sha512-NBqLEx1GxllIOXJInJAQbrnwwYJsV3WaMHIcOwD8rhYS0AabTWn7kHdHgPgu5RmHLU0q4DMxhAMu8ue/KampgQ==", + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", + "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-react-display-name": { - "version": "7.15.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.15.1.tgz", - "integrity": "sha512-yQZ/i/pUCJAHI/LbtZr413S3VT26qNrEm0M5RRxQJA947/YNYwbZbBaXGDrq6CG5QsZycI1VIP6d7pQaBfP+8Q==", + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", + "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-react-jsx": { - "version": "7.14.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.14.9.tgz", - "integrity": "sha512-30PeETvS+AeD1f58i1OVyoDlVYQhap/K20ZrMjLmmzmC2AYR/G43D4sdJAaDAqCD3MYpSWbmrz3kES158QSLjw==", + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.26.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", + "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-jsx": "^7.14.5", - "@babel/types": "^7.14.9" + "dependencies": { + "@babel/helper-module-transforms": "^7.26.0", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-react-jsx-development": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.14.5.tgz", - "integrity": "sha512-rdwG/9jC6QybWxVe2UVOa7q6cnTpw8JRRHOxntG/h6g/guAOe6AhtQHJuJh5FwmnXIT1bdm5vC2/5huV8ZOorQ==", + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", + "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", "dev": true, - "requires": { - "@babel/plugin-transform-react-jsx": "^7.14.5" + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-validator-identifier": "^7.25.9", + "@babel/traverse": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-react-pure-annotations": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.14.5.tgz", - "integrity": "sha512-3X4HpBJimNxW4rhUy/SONPyNQHp5YRr0HhJdT2OH1BRp0of7u3Dkirc7x9FRJMKMqTBI079VZ1hzv7Ouuz///g==", + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", + "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", "dev": true, - "requires": { - "@babel/helper-annotate-as-pure": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-module-transforms": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-regenerator": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.14.5.tgz", - "integrity": "sha512-NVIY1W3ITDP5xQl50NgTKlZ0GrotKtLna08/uGY6ErQt6VEQZXla86x/CTddm5gZdcr+5GSsvMeTmWA5Ii6pkg==", + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", + "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", "dev": true, - "requires": { - "regenerator-transform": "^0.14.2" + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@babel/plugin-transform-reserved-words": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.14.5.tgz", - "integrity": "sha512-cv4F2rv1nD4qdexOGsRQXJrOcyb5CrgjUH9PKrrtyhSDBNWGxd0UIitjyJiWagS+EbUGjG++22mGH1Pub8D6Vg==", + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", + "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-runtime": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.15.0.tgz", - "integrity": "sha512-sfHYkLGjhzWTq6xsuQ01oEsUYjkHRux9fW1iUA68dC7Qd8BS1Unq4aZ8itmQp95zUzIcyR2EbNMTzAicFj+guw==", + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.26.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", + "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", "dev": true, - "requires": { - "@babel/helper-module-imports": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5", - "babel-plugin-polyfill-corejs2": "^0.2.2", - "babel-plugin-polyfill-corejs3": "^0.2.2", - "babel-plugin-polyfill-regenerator": "^0.2.2", - "semver": "^6.3.0" - }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-shorthand-properties": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.14.5.tgz", - "integrity": "sha512-xLucks6T1VmGsTB+GWK5Pl9Jl5+nRXD1uoFdA5TSO6xtiNjtXTjKkmPdFXVLGlK5A2/or/wQMKfmQ2Y0XJfn5g==", + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", + "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-spread": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.14.6.tgz", - "integrity": "sha512-Zr0x0YroFJku7n7+/HH3A2eIrGMjbmAIbJSVv0IZ+t3U2WUQUA64S/oeied2e+MaGSjmt4alzBCsK9E8gh+fag==", + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", + "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-skip-transparent-expression-wrappers": "^7.14.5" + "dependencies": { + "@babel/helper-compilation-targets": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/plugin-transform-parameters": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-sticky-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.14.5.tgz", - "integrity": "sha512-Z7F7GyvEMzIIbwnziAZmnSNpdijdr4dWt+FJNBnBLz5mwDFkqIXU9wmBcWWad3QeJF5hMTkRe4dAq2sUZiG+8A==", + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", + "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-replace-supers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-template-literals": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.14.5.tgz", - "integrity": "sha512-22btZeURqiepOfuy/VkFr+zStqlujWaarpMErvay7goJS6BWwdd6BY9zQyDLDa4x2S3VugxFb162IZ4m/S/+Gg==", + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", + "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-typeof-symbol": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.14.5.tgz", - "integrity": "sha512-lXzLD30ffCWseTbMQzrvDWqljvZlHkXU+CnseMhkMNqU1sASnCsz3tSzAaH3vCUXb9PHeUb90ZT1BdFTm1xxJw==", + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", + "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-typescript": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.15.0.tgz", - "integrity": "sha512-WIIEazmngMEEHDaPTx0IZY48SaAmjVWe3TRSX7cmJXn0bEv9midFzAjxiruOWYIVf5iQ10vFx7ASDpgEO08L5w==", + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", + "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", "dev": true, - "requires": { - "@babel/helper-create-class-features-plugin": "^7.15.0", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/plugin-syntax-typescript": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-unicode-escapes": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.14.5.tgz", - "integrity": "sha512-crTo4jATEOjxj7bt9lbYXcBAM3LZaUrbP2uUdxb6WIorLmjNKSpHfIybgY4B8SRpbf8tEVIWH3Vtm7ayCrKocA==", + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", + "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/plugin-transform-unicode-regex": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.14.5.tgz", - "integrity": "sha512-UygduJpC5kHeCiRw/xDVzC+wj8VaYSoKl5JNVmbP7MadpNinAm3SvZCxZ42H37KZBKztz46YC73i9yV34d0Tzw==", + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", + "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", "dev": true, - "requires": { - "@babel/helper-create-regexp-features-plugin": "^7.14.5", - "@babel/helper-plugin-utils": "^7.14.5" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/preset-env": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.15.0.tgz", - "integrity": "sha512-FhEpCNFCcWW3iZLg0L2NPE9UerdtsCR6ZcsGHUX6Om6kbCQeL5QZDqFDmeNHC6/fy6UH3jEge7K4qG5uC9In0Q==", - "dev": true, - "requires": { - "@babel/compat-data": "^7.15.0", - "@babel/helper-compilation-targets": "^7.15.0", - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.14.5", - "@babel/plugin-proposal-async-generator-functions": "^7.14.9", - "@babel/plugin-proposal-class-properties": "^7.14.5", - "@babel/plugin-proposal-class-static-block": "^7.14.5", - "@babel/plugin-proposal-dynamic-import": "^7.14.5", - "@babel/plugin-proposal-export-namespace-from": "^7.14.5", - "@babel/plugin-proposal-json-strings": "^7.14.5", - "@babel/plugin-proposal-logical-assignment-operators": "^7.14.5", - "@babel/plugin-proposal-nullish-coalescing-operator": "^7.14.5", - "@babel/plugin-proposal-numeric-separator": "^7.14.5", - "@babel/plugin-proposal-object-rest-spread": "^7.14.7", - "@babel/plugin-proposal-optional-catch-binding": "^7.14.5", - "@babel/plugin-proposal-optional-chaining": "^7.14.5", - "@babel/plugin-proposal-private-methods": "^7.14.5", - "@babel/plugin-proposal-private-property-in-object": "^7.14.5", - "@babel/plugin-proposal-unicode-property-regex": "^7.14.5", - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-class-properties": "^7.12.13", - "@babel/plugin-syntax-class-static-block": "^7.14.5", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-syntax-export-namespace-from": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.10.4", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-private-property-in-object": "^7.14.5", - "@babel/plugin-syntax-top-level-await": "^7.14.5", - "@babel/plugin-transform-arrow-functions": "^7.14.5", - "@babel/plugin-transform-async-to-generator": "^7.14.5", - "@babel/plugin-transform-block-scoped-functions": "^7.14.5", - "@babel/plugin-transform-block-scoping": "^7.14.5", - "@babel/plugin-transform-classes": "^7.14.9", - "@babel/plugin-transform-computed-properties": "^7.14.5", - "@babel/plugin-transform-destructuring": "^7.14.7", - "@babel/plugin-transform-dotall-regex": "^7.14.5", - "@babel/plugin-transform-duplicate-keys": "^7.14.5", - "@babel/plugin-transform-exponentiation-operator": "^7.14.5", - "@babel/plugin-transform-for-of": "^7.14.5", - "@babel/plugin-transform-function-name": "^7.14.5", - "@babel/plugin-transform-literals": "^7.14.5", - "@babel/plugin-transform-member-expression-literals": "^7.14.5", - "@babel/plugin-transform-modules-amd": "^7.14.5", - "@babel/plugin-transform-modules-commonjs": "^7.15.0", - "@babel/plugin-transform-modules-systemjs": "^7.14.5", - "@babel/plugin-transform-modules-umd": "^7.14.5", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.14.9", - "@babel/plugin-transform-new-target": "^7.14.5", - "@babel/plugin-transform-object-super": "^7.14.5", - "@babel/plugin-transform-parameters": "^7.14.5", - "@babel/plugin-transform-property-literals": "^7.14.5", - "@babel/plugin-transform-regenerator": "^7.14.5", - "@babel/plugin-transform-reserved-words": "^7.14.5", - "@babel/plugin-transform-shorthand-properties": "^7.14.5", - "@babel/plugin-transform-spread": "^7.14.6", - "@babel/plugin-transform-sticky-regex": "^7.14.5", - "@babel/plugin-transform-template-literals": "^7.14.5", - "@babel/plugin-transform-typeof-symbol": "^7.14.5", - "@babel/plugin-transform-unicode-escapes": "^7.14.5", - "@babel/plugin-transform-unicode-regex": "^7.14.5", - "@babel/preset-modules": "^0.1.4", - "@babel/types": "^7.15.0", - "babel-plugin-polyfill-corejs2": "^0.2.2", - "babel-plugin-polyfill-corejs3": "^0.2.2", - "babel-plugin-polyfill-regenerator": "^0.2.2", - "core-js-compat": "^3.16.0", - "semver": "^6.3.0" - }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", + "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "dev": true, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/preset-modules": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.4.tgz", - "integrity": "sha512-J36NhwnfdzpmH41M1DrnkkgAqhZaqr/NBdPfQ677mLzlaXo+oDiv1deyCDtgAhz8p328otdob0Du7+xgHGZbKg==", + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.22.5.tgz", + "integrity": "sha512-BF5SXoO+nX3h5OhlN78XbbDrBOffv+AxPP2ENaJOVqjWCgBDeOY3WcaUcddutGSfoap+5NEQ/q/4I3WZIvgkXA==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/plugin-proposal-unicode-property-regex": "^7.4.4", - "@babel/plugin-transform-dotall-regex": "^7.4.4", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/preset-react": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.14.5.tgz", - "integrity": "sha512-XFxBkjyObLvBaAvkx1Ie95Iaq4S/GUEIrejyrntQ/VCMKUYvKLoyKxOBzJ2kjA3b6rC9/KL6KXfDC2GqvLiNqQ==", + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.22.5.tgz", + "integrity": "sha512-PVk3WPYudRF5z4GKMEYUrLjPl38fJSKNaEOkFuoprioowGuWN6w2RKznuFNSlJx7pzzXXStPUnNSOEO0jL5EVw==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "@babel/plugin-transform-react-display-name": "^7.14.5", - "@babel/plugin-transform-react-jsx": "^7.14.5", - "@babel/plugin-transform-react-jsx-development": "^7.14.5", - "@babel/plugin-transform-react-pure-annotations": "^7.14.5" + "dependencies": { + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/preset-typescript": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.15.0.tgz", - "integrity": "sha512-lt0Y/8V3y06Wq/8H/u0WakrqciZ7Fz7mwPDHWUJAXlABL5hiUG42BNlRXiELNjeWjO5rWmnNKlx+yzJvxezHow==", + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.25.7.tgz", + "integrity": "sha512-vILAg5nwGlR9EXE8JIOX4NHXd49lrYbN8hnjffDtoULwpL9hUx/N55nqh2qd0q6FyNDfjl9V79ecKGvFbcSA0Q==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.14.5", - "@babel/helper-validator-option": "^7.14.5", - "@babel/plugin-transform-typescript": "^7.15.0" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.25.7", + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/types": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/runtime": { - "version": "7.14.6", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.14.6.tgz", - "integrity": "sha512-/PCB2uJ7oM44tz8YhC4Z/6PeOKXp4K588f+5M3clr1M4zbqztlo0XEfJ2LEzj/FgwfgGcIdl8n7YYjTCI0BYwg==", - "requires": { - "regenerator-runtime": "^0.13.4" + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/runtime-corejs3": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.15.3.tgz", - "integrity": "sha512-30A3lP+sRL6ml8uhoJSs+8jwpKzbw8CqBvDc1laeptxPm5FahumJxirigcbD2qTs71Sonvj1cyZB0OKGAmxQ+A==", + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.22.5.tgz", + "integrity": "sha512-gP4k85wx09q+brArVinTXhWiyzLl9UpmGva0+mWyKxk6JZequ05x3eUcIUE+FyttPKJFRRVtAvQaJ6YF9h1ZpA==", "dev": true, - "requires": { - "core-js-pure": "^3.16.0", - "regenerator-runtime": "^0.13.4" + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/template": { - "version": "7.14.5", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.14.5.tgz", - "integrity": "sha512-6Z3Po85sfxRGachLULUhOmvAaOo7xCvqGQtxINai2mEGPFm6pQ4z5QInFnUrRpfoSV60BnjyF5F3c+15fxFV1g==", + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", + "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/parser": "^7.14.5", - "@babel/types": "^7.14.5" - }, "dependencies": { - "@babel/parser": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", - "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.25.9", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/traverse": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.15.0.tgz", - "integrity": "sha512-392d8BN0C9eVxVWd8H6x9WfipgVH5IaIoLp23334Sc1vbKKWINnvwRpb4us0xtPaCumlwbTtIYNA0Dv/32sVFw==", + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", + "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", "dev": true, - "requires": { - "@babel/code-frame": "^7.14.5", - "@babel/generator": "^7.15.0", - "@babel/helper-function-name": "^7.14.5", - "@babel/helper-hoist-variables": "^7.14.5", - "@babel/helper-split-export-declaration": "^7.14.5", - "@babel/parser": "^7.15.0", - "@babel/types": "^7.15.0", - "debug": "^4.1.0", - "globals": "^11.1.0" - }, "dependencies": { - "@babel/parser": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.15.3.tgz", - "integrity": "sha512-O0L6v/HvqbdJawj0iBEfVQMc3/6WP+AeOsovsIgBFyJaG+W2w7eqvZB7puddATmWuARlm1SX7DwxJ/JJUnDpEA==", - "dev": true - } + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@babel/types": { - "version": "7.15.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.15.0.tgz", - "integrity": "sha512-OBvfqnllOIdX4ojTHpwZbpvz4j3EWyjkZEdmjH0/cgsd6QOdSgU8rLSk6ard/pcW7rlmjdVSX/AWOaORR1uNOQ==", - "requires": { - "@babel/helper-validator-identifier": "^7.14.9", - "to-fast-properties": "^2.0.0" + "node_modules/@babel/plugin-transform-runtime": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.7.tgz", + "integrity": "sha512-Y9p487tyTzB0yDYQOtWnC+9HGOuogtP3/wNpun1xJXEEvI6vip59BSBTsHnekZLqxmPcgsrAKt46HAAb//xGhg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@bcoe/v8-coverage": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", - "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", - "dev": true - }, - "@choojs/findup": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", - "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", + "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, - "requires": { - "commander": "^2.15.1" + "license": "ISC", + "bin": { + "semver": "bin/semver.js" } }, - "@cnakazawa/watch": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/@cnakazawa/watch/-/watch-1.0.4.tgz", - "integrity": "sha512-v9kIhKwjeZThiWrLmj0y17CWoyddASLj9O2yvbZkbvw/N3rWOYy9zkV66ursAoVr0mV15bL8g0c4QZUE6cdDoQ==", + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", + "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", "dev": true, - "requires": { - "exec-sh": "^0.3.2", - "minimist": "^1.2.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@emotion/cache": { - "version": "10.0.29", - "resolved": "https://registry.npmjs.org/@emotion/cache/-/cache-10.0.29.tgz", - "integrity": "sha512-fU2VtSVlHiF27empSbxi1O2JFdNWZO+2NFHfwO0pxgTep6Xa3uGb+3pVKfLww2l/IBGLNEZl5Xf/++A4wAYDYQ==", - "requires": { - "@emotion/sheet": "0.9.4", - "@emotion/stylis": "0.8.5", - "@emotion/utils": "0.11.3", - "@emotion/weak-memoize": "0.2.5" + "node_modules/@babel/plugin-transform-spread": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", + "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@emotion/core": { - "version": "10.1.1", - "resolved": "https://registry.npmjs.org/@emotion/core/-/core-10.1.1.tgz", - "integrity": "sha512-ZMLG6qpXR8x031NXD8HJqugy/AZSkAuMxxqB46pmAR7ze47MhNJ56cdoX243QPZdGctrdfo+s08yZTiwaUcRKA==", - "requires": { - "@babel/runtime": "^7.5.5", - "@emotion/cache": "^10.0.27", - "@emotion/css": "^10.0.27", - "@emotion/serialize": "^0.11.15", - "@emotion/sheet": "0.9.4", - "@emotion/utils": "0.11.3" - } - }, - "@emotion/css": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/css/-/css-10.0.27.tgz", - "integrity": "sha512-6wZjsvYeBhyZQYNrGoR5yPMYbMBNEnanDrqmsqS1mzDm1cOTu12shvl2j4QHNS36UaTE0USIJawCH9C8oW34Zw==", - "requires": { - "@emotion/serialize": "^0.11.15", - "@emotion/utils": "0.11.3", - "babel-plugin-emotion": "^10.0.27" - } - }, - "@emotion/hash": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", - "integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==" - }, - "@emotion/is-prop-valid": { - "version": "0.8.8", - "resolved": "https://registry.npmjs.org/@emotion/is-prop-valid/-/is-prop-valid-0.8.8.tgz", - "integrity": "sha512-u5WtneEAr5IDG2Wv65yhunPSMLIpuKsbuOktRojfrEiEvRyC85LgPMZI63cr7NUqT8ZIGdSVg8ZKGxIug4lXcA==", - "requires": { - "@emotion/memoize": "0.7.4" + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", + "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@emotion/memoize": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/@emotion/memoize/-/memoize-0.7.4.tgz", - "integrity": "sha512-Ja/Vfqe3HpuzRsG1oBtWTHk2PGZ7GR+2Vz5iYGelAw8dx32K0y7PjVuxK6z1nMpZOqAFsRUPCkK1YjJ56qJlgw==" - }, - "@emotion/serialize": { - "version": "0.11.16", - "resolved": "https://registry.npmjs.org/@emotion/serialize/-/serialize-0.11.16.tgz", - "integrity": "sha512-G3J4o8by0VRrO+PFeSc3js2myYNOXVJ3Ya+RGVxnshRYgsvErfAOglKAiy1Eo1vhzxqtUvjCyS5gtewzkmvSSg==", - "requires": { - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/unitless": "0.7.5", - "@emotion/utils": "0.11.3", - "csstype": "^2.5.7" - }, - "dependencies": { - "csstype": { - "version": "2.6.17", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-2.6.17.tgz", - "integrity": "sha512-u1wmTI1jJGzCJzWndZo8mk4wnPTZd1eOIYTYvuEyOQGfmDl3TrabCCfKnOC86FZwW/9djqTl933UF/cS425i9A==" - } + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", + "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@emotion/sheet": { - "version": "0.9.4", - "resolved": "https://registry.npmjs.org/@emotion/sheet/-/sheet-0.9.4.tgz", - "integrity": "sha512-zM9PFmgVSqBw4zL101Q0HrBVTGmpAxFZH/pYx/cjJT5advXguvcgjHFTCaIO3enL/xr89vK2bh0Mfyj9aa0ANA==" - }, - "@emotion/styled": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/@emotion/styled/-/styled-10.0.27.tgz", - "integrity": "sha512-iK/8Sh7+NLJzyp9a5+vIQIXTYxfT4yB/OJbjzQanB2RZpvmzBQOHZWhpAMZWYEKRNNbsD6WfBw5sVWkb6WzS/Q==", - "requires": { - "@emotion/styled-base": "^10.0.27", - "babel-plugin-emotion": "^10.0.27" - } - }, - "@emotion/styled-base": { - "version": "10.0.31", - "resolved": "https://registry.npmjs.org/@emotion/styled-base/-/styled-base-10.0.31.tgz", - "integrity": "sha512-wTOE1NcXmqMWlyrtwdkqg87Mu6Rj1MaukEoEmEkHirO5IoHDJ8LgCQL4MjJODgxWxXibGR3opGp1p7YvkNEdXQ==", - "requires": { - "@babel/runtime": "^7.5.5", - "@emotion/is-prop-valid": "0.8.8", - "@emotion/serialize": "^0.11.15", - "@emotion/utils": "0.11.3" - } - }, - "@emotion/stylis": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/@emotion/stylis/-/stylis-0.8.5.tgz", - "integrity": "sha512-h6KtPihKFn3T9fuIrwvXXUOwlx3rfUvfZIcP5a6rh8Y7zjE3O06hT5Ss4S/YI1AYhuZ1kjaE/5EaOOI2NqSylQ==" - }, - "@emotion/unitless": { - "version": "0.7.5", - "resolved": "https://registry.npmjs.org/@emotion/unitless/-/unitless-0.7.5.tgz", - "integrity": "sha512-OWORNpfjMsSSUBVrRBVGECkhWcULOAJz9ZW8uK9qgxD+87M7jHRcvh/A96XXNhXTLmKcoYSQtBEX7lHMO7YRwg==" - }, - "@emotion/utils": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/@emotion/utils/-/utils-0.11.3.tgz", - "integrity": "sha512-0o4l6pZC+hI88+bzuaX/6BgOvQVhbt2PfmxauVaYOGgbsAw14wdKyvMCZXnsnsHys94iadcF+RG/wZyx6+ZZBw==" - }, - "@emotion/weak-memoize": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/@emotion/weak-memoize/-/weak-memoize-0.2.5.tgz", - "integrity": "sha512-6U71C2Wp7r5XtFtQzYrW5iKFT67OixrSxjI4MptCHzdSVlgabczzqLe0ZSgnub/5Kp4hSbpDB1tMytZY9pwxxA==" - }, - "@es-joy/jsdoccomment": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.6.0.tgz", - "integrity": "sha512-zT1EtysKMITJ7vE4RvOJqitxk/Str6It8hq+fykxkwLuTyzgak+TnVuVSIyovT/qrEz3i46ypCSXgNtIDYwNOg==", + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", + "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", "dev": true, - "requires": { - "comment-parser": "^1.1.5", - "esquery": "^1.4.0", - "jsdoctypeparser": "^9.0.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.26.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@eslint/eslintrc": { - "version": "0.4.3", - "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-0.4.3.tgz", - "integrity": "sha512-J6KFFz5QCYUJq3pf0mjEcCJVERbzv71PUIDczuh9JkwGEzced6CO5ADLHB1rbf/+oPBtoPfMYNOpGDzCANlbXw==", + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.26.7.tgz", + "integrity": "sha512-5cJurntg+AT+cgelGP9Bt788DKiAw9gIMSMU2NJrLAilnj0m8WZWUNZPSLOmadYsujHutpgElO+50foX+ib/Wg==", "dev": true, - "requires": { - "ajv": "^6.12.4", - "debug": "^4.1.1", - "espree": "^7.3.0", - "globals": "^13.9.0", - "ignore": "^4.0.6", - "import-fresh": "^3.2.1", - "js-yaml": "^3.13.1", - "minimatch": "^3.0.4", - "strip-json-comments": "^3.1.1" - }, "dependencies": { - "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } + "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-create-class-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.26.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", + "@babel/plugin-syntax-typescript": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@financial-times/polyfill-useragent-normaliser": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@financial-times/polyfill-useragent-normaliser/-/polyfill-useragent-normaliser-1.8.1.tgz", - "integrity": "sha512-VJFUMJyr1DZ++wiVFgk0GThJG8LUR94p/a73lc4d0a2YL6sfWSQAMF+1qg9gtCOzZWc+nx62E5IJExrUCetjEw==", - "requires": { - "@financial-times/useragent_parser": "^1.5.1", - "semver": "^7.1.1" + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", + "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@financial-times/useragent_parser": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/@financial-times/useragent_parser/-/useragent_parser-1.6.0.tgz", - "integrity": "sha512-v6Ucl//xSVhpyTtHMVCA9uv9W7CVwj8vBAQFKFDkfGC1DquBobOMhnzH9Odc+Tunf+i4WRnNgt90fQ7CSAbU3g==" - }, - "@formatjs/ecma402-abstract": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.4.tgz", - "integrity": "sha512-PyzVaiXHCp1WtXnR30P06BYqWHHY5YIkbwxfB2WE1yNa7XXvozmh6mSc099HgSoTY5ZmOAqwh78G2qOg0j5aPw==", - "requires": { - "tslib": "^2.0.1" - }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", + "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@formatjs/intl-datetimeformat": { - "version": "3.2.9", - "resolved": "https://registry.npmjs.org/@formatjs/intl-datetimeformat/-/intl-datetimeformat-3.2.9.tgz", - "integrity": "sha512-pwmnHHJNu96XmG9Zj1bhg6f/zYW8oJxOgKwtaTl9znl+RAH5Ud6Y9IuxPbNwDBQEfcs4+X7fopbF+if0bAOc7w==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.4", - "tslib": "^2.0.1" - }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", + "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@formatjs/intl-displaynames": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-displaynames/-/intl-displaynames-4.0.7.tgz", - "integrity": "sha512-auQ8/akjhzeLPk4riG7JHulGXSoVNA5xk0IU+BzEymzFCBJyWvNoroIDCxX6RgRPumMmmIEgcc3w7BmKBo2Obw==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.4", - "tslib": "^2.0.1" - }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.25.9", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", + "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/helper-create-regexp-features-plugin": "^7.25.9", + "@babel/helper-plugin-utils": "^7.25.9" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "@formatjs/intl-getcanonicallocales": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/intl-getcanonicallocales/-/intl-getcanonicallocales-1.5.3.tgz", - "integrity": "sha512-QVBnSPZ32Y80wkXbf36hP9VbyklbOb8edppxFcgO9Lbd47zagllw65Y81QOHEn/j11JcTn2OhW0vea95LHvQmA==", - "requires": { - "cldr-core": "38", - "tslib": "^2.0.1" - }, + "node_modules/@babel/preset-env": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.25.7.tgz", + "integrity": "sha512-Gibz4OUdyNqqLj+7OAvBZxOD7CklCtMA5/j0JgUEwOnaRULsPDXmic2iKxL2DX2vQduPR5wH2hjZas/Vr/Oc0g==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/compat-data": "^7.25.7", + "@babel/helper-compilation-targets": "^7.25.7", + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.7", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.7", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.7", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.7", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.7", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.25.7", + "@babel/plugin-syntax-import-attributes": "^7.25.7", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.25.7", + "@babel/plugin-transform-async-generator-functions": "^7.25.7", + "@babel/plugin-transform-async-to-generator": "^7.25.7", + "@babel/plugin-transform-block-scoped-functions": "^7.25.7", + "@babel/plugin-transform-block-scoping": "^7.25.7", + "@babel/plugin-transform-class-properties": "^7.25.7", + "@babel/plugin-transform-class-static-block": "^7.25.7", + "@babel/plugin-transform-classes": "^7.25.7", + "@babel/plugin-transform-computed-properties": "^7.25.7", + "@babel/plugin-transform-destructuring": "^7.25.7", + "@babel/plugin-transform-dotall-regex": "^7.25.7", + "@babel/plugin-transform-duplicate-keys": "^7.25.7", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-dynamic-import": "^7.25.7", + "@babel/plugin-transform-exponentiation-operator": "^7.25.7", + "@babel/plugin-transform-export-namespace-from": "^7.25.7", + "@babel/plugin-transform-for-of": "^7.25.7", + "@babel/plugin-transform-function-name": "^7.25.7", + "@babel/plugin-transform-json-strings": "^7.25.7", + "@babel/plugin-transform-literals": "^7.25.7", + "@babel/plugin-transform-logical-assignment-operators": "^7.25.7", + "@babel/plugin-transform-member-expression-literals": "^7.25.7", + "@babel/plugin-transform-modules-amd": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-modules-systemjs": "^7.25.7", + "@babel/plugin-transform-modules-umd": "^7.25.7", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.7", + "@babel/plugin-transform-new-target": "^7.25.7", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.7", + "@babel/plugin-transform-numeric-separator": "^7.25.7", + "@babel/plugin-transform-object-rest-spread": "^7.25.7", + "@babel/plugin-transform-object-super": "^7.25.7", + "@babel/plugin-transform-optional-catch-binding": "^7.25.7", + "@babel/plugin-transform-optional-chaining": "^7.25.7", + "@babel/plugin-transform-parameters": "^7.25.7", + "@babel/plugin-transform-private-methods": "^7.25.7", + "@babel/plugin-transform-private-property-in-object": "^7.25.7", + "@babel/plugin-transform-property-literals": "^7.25.7", + "@babel/plugin-transform-regenerator": "^7.25.7", + "@babel/plugin-transform-reserved-words": "^7.25.7", + "@babel/plugin-transform-shorthand-properties": "^7.25.7", + "@babel/plugin-transform-spread": "^7.25.7", + "@babel/plugin-transform-sticky-regex": "^7.25.7", + "@babel/plugin-transform-template-literals": "^7.25.7", + "@babel/plugin-transform-typeof-symbol": "^7.25.7", + "@babel/plugin-transform-unicode-escapes": "^7.25.7", + "@babel/plugin-transform-unicode-property-regex": "^7.25.7", + "@babel/plugin-transform-unicode-regex": "^7.25.7", + "@babel/plugin-transform-unicode-sets-regex": "^7.25.7", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.38.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" } }, - "@formatjs/intl-listformat": { - "version": "5.0.6", - "resolved": "https://registry.npmjs.org/@formatjs/intl-listformat/-/intl-listformat-5.0.6.tgz", - "integrity": "sha512-9QM4elGPrSSMOsf4bKaDUYpFTgXtxNeTdtaxc4QBTreD68X3YMH7N/tve1MxxhbTMB2qsK08mYtSHp5r+y4pTw==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.3", - "tslib": "^2.0.1" - }, + "node_modules/@babel/preset-react": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.22.15.tgz", + "integrity": "sha512-Csy1IJ2uEh/PecCBXXoZGAZBeCATTuePzCSB7dLYWS0vOEj6CNpjxIhW4duWwZodBNueH7QO14WbGn8YyeuN9w==", + "dev": true, "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.3.tgz", - "integrity": "sha512-PI+C4JhJV1WFINrTbX0jHlWPOQOAsg4jmGWp23cbIb+q7+nALBlpElcik4XhsIjbJstmUGR8r83j8vvgNjO90g==", - "requires": { - "tslib": "^2.0.1" - } - }, - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/helper-validator-option": "^7.22.15", + "@babel/plugin-transform-react-display-name": "^7.22.5", + "@babel/plugin-transform-react-jsx": "^7.22.15", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@formatjs/intl-locale": { - "version": "2.4.16", - "resolved": "https://registry.npmjs.org/@formatjs/intl-locale/-/intl-locale-2.4.16.tgz", - "integrity": "sha512-alOsuHSEritrWYnnA8ihiUZwNqLfCuFgDmDzJbVUOrcCKlUwrLj4uQMAN0RbIsfI5XiVZEqNshdj2sQNNtNL0A==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.4", - "@formatjs/intl-getcanonicallocales": "1.5.3", - "cldr-core": "38", - "tslib": "^2.0.1" - }, + "node_modules/@babel/preset-typescript": { + "version": "7.25.7", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.25.7.tgz", + "integrity": "sha512-rkkpaXJZOFN45Fb+Gki0c+KMIglk4+zZXOoMJuyEK8y8Kkc8Jd3BDmP7qPsz0zQMJj+UD7EprF+AqAXcILnexw==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/helper-plugin-utils": "^7.25.7", + "@babel/helper-validator-option": "^7.25.7", + "@babel/plugin-syntax-jsx": "^7.25.7", + "@babel/plugin-transform-modules-commonjs": "^7.25.7", + "@babel/plugin-transform-typescript": "^7.25.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "@formatjs/intl-numberformat": { - "version": "6.1.5", - "resolved": "https://registry.npmjs.org/@formatjs/intl-numberformat/-/intl-numberformat-6.1.5.tgz", - "integrity": "sha512-T7qjALvZZDzTdszlFhsC9DyivUxA8yEOiwtsF8Q8/GWoYg+pEnLxSxyPd7x3EZG1eiStSkndqisEFdwBmIhblw==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.3", - "tslib": "^2.0.1" - }, - "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.3.tgz", - "integrity": "sha512-PI+C4JhJV1WFINrTbX0jHlWPOQOAsg4jmGWp23cbIb+q7+nALBlpElcik4XhsIjbJstmUGR8r83j8vvgNjO90g==", - "requires": { - "tslib": "^2.0.1" - } - }, - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" } }, - "@formatjs/intl-pluralrules": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/@formatjs/intl-pluralrules/-/intl-pluralrules-4.0.7.tgz", - "integrity": "sha512-g/BSzr8bgMv3UywE2in3zcSeAzR2aSvHhNCfFYfTqi/6q91q0LSgRFnykY26Dzs2Ag2jFp27AvOSHyoZ7DQMRw==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.3", - "tslib": "^2.0.1" - }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.3.tgz", - "integrity": "sha512-PI+C4JhJV1WFINrTbX0jHlWPOQOAsg4jmGWp23cbIb+q7+nALBlpElcik4XhsIjbJstmUGR8r83j8vvgNjO90g==", - "requires": { - "tslib": "^2.0.1" - } - }, - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" } }, - "@formatjs/intl-relativetimeformat": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/@formatjs/intl-relativetimeformat/-/intl-relativetimeformat-8.0.5.tgz", - "integrity": "sha512-061+cSR0/utsAf3fdcr787ck7KAgrtbeq06ENdR4A7x6aYq1CqchoJ8e7WOtqz0oSrj6Zmoaca1b/pDr+hM8/w==", - "requires": { - "@formatjs/ecma402-abstract": "1.5.3", - "tslib": "^2.0.1" - }, + "node_modules/@babel/traverse": { + "version": "7.26.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", + "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "dev": true, "dependencies": { - "@formatjs/ecma402-abstract": { - "version": "1.5.3", - "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-1.5.3.tgz", - "integrity": "sha512-PI+C4JhJV1WFINrTbX0jHlWPOQOAsg4jmGWp23cbIb+q7+nALBlpElcik4XhsIjbJstmUGR8r83j8vvgNjO90g==", - "requires": { - "tslib": "^2.0.1" - } - }, - "tslib": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.2.0.tgz", - "integrity": "sha512-gS9GVHRU+RGn5KQM2rllAlR3dU6m7AcpJKdtH8gFvQiC4Otgk98XnmMU+nZenHt/+VhnBPWwgrJsyrdcw6i23w==" - } + "@babel/code-frame": "^7.26.2", + "@babel/generator": "^7.26.5", + "@babel/parser": "^7.26.7", + "@babel/template": "^7.25.9", + "@babel/types": "^7.26.7", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" } }, - "@gar/promisify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@gar/promisify/-/promisify-1.1.2.tgz", - "integrity": "sha512-82cpyJyKRoQoRi+14ibCeGPu0CwypgtBAdBhq1WfvagpCZNKqwXbKwXllYSMG91DhmG4jt9gN8eP6lGOtozuaw==", - "dev": true - }, - "@hapi/address": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@hapi/address/-/address-2.1.4.tgz", - "integrity": "sha512-QD1PhQk+s31P1ixsX0H0Suoupp3VMXzIVMSwobR3F3MSUO2YCV0B7xqLcUw/Bh8yuvd3LhpyqLQWTNcRmp6IdQ==", - "dev": true - }, - "@hapi/bourne": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/@hapi/bourne/-/bourne-1.3.2.tgz", - "integrity": "sha512-1dVNHT76Uu5N3eJNTYcvxee+jzX4Z9lfciqRRHCU27ihbUcYi+iSc2iml5Ke1LXe1SyJCLA0+14Jh4tXJgOppA==", - "dev": true + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } }, - "@hapi/hoek": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-8.5.1.tgz", - "integrity": "sha512-yN7kbciD87WzLGc5539Tn0sApjyiGHAJgKvG9W8C7O+6c7qmoQMfVs0W4bX17eqz6C78QJqqFrtgdK5EWf6Qow==", + "node_modules/@bcoe/v8-coverage": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/@bcoe/v8-coverage/-/v8-coverage-0.2.3.tgz", + "integrity": "sha512-0hYQ8SB4Db5zvZB4axdMHGwEaQjkZzFjQiN9LVYvIFB2nSUHW9tYpxWriPrWDASIxiaXax83REcLxuSdnGPZtw==", "dev": true }, - "@hapi/joi": { - "version": "15.1.1", - "resolved": "https://registry.npmjs.org/@hapi/joi/-/joi-15.1.1.tgz", - "integrity": "sha512-entf8ZMOK8sc+8YfeOlM8pCfg3b5+WZIKBfUaaJT8UsjAAPjartzxIYm3TIbjvA4u+u++KbcXD38k682nVHDAQ==", + "node_modules/@cacheable/memory": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.0.8.tgz", + "integrity": "sha512-FvEb29x5wVwu/Kf93IWwsOOEuhHh6dYCJF3vcKLzXc0KXIW181AOzv6ceT4ZpBHDvAfG60eqb+ekmrnLHIy+jw==", "dev": true, - "requires": { - "@hapi/address": "2.x.x", - "@hapi/bourne": "1.x.x", - "@hapi/hoek": "8.x.x", - "@hapi/topo": "3.x.x" + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.4.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" } }, - "@hapi/topo": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-3.1.6.tgz", - "integrity": "sha512-tAag0jEcjwH+P2quUfipd7liWCNX2F8NvYjQp2wtInsZxnMlypdw0FtAOLxtvvkO+GSRRbmNi8m/5y42PQJYCQ==", + "node_modules/@cacheable/memory/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "requires": { - "@hapi/hoek": "^8.3.0" + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" } }, - "@humanwhocodes/config-array": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.5.0.tgz", - "integrity": "sha512-FagtKFz74XrTl7y6HCzQpwDfXP0yhxe9lHLD1UZxjvZIcbyRz8zTFF/yYNfSfzU414eDwZ1SrO0Qvtyf+wFMQg==", + "node_modules/@cacheable/utils": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.4.0.tgz", + "integrity": "sha512-PeMMsqjVq+bF0WBsxFBxr/WozBJiZKY0rUojuaCoIaKnEl3Ju1wfEwS+SV1DU/cSe8fqHIPiYJFif8T3MVt4cQ==", "dev": true, - "requires": { - "@humanwhocodes/object-schema": "^1.2.0", - "debug": "^4.1.1", - "minimatch": "^3.0.4" + "license": "MIT", + "dependencies": { + "hashery": "^1.5.0", + "keyv": "^5.6.0" } }, - "@humanwhocodes/object-schema": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-1.2.0.tgz", - "integrity": "sha512-wdppn25U8z/2yiaT6YGquE6X8sSv7hNMWSXYSSU1jGv/yd6XqjXgTDJ8KP4NgjTXfJ3GbRjeeb8RTV7a/VpM+w==", - "dev": true - }, - "@istanbuljs/load-nyc-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", - "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "node_modules/@cacheable/utils/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, - "requires": { - "camelcase": "^5.3.1", - "find-up": "^4.1.0", - "get-package-type": "^0.1.0", - "js-yaml": "^3.13.1", - "resolve-from": "^5.0.0" - }, + "license": "MIT", "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } + "@keyv/serialize": "^1.1.1" } }, - "@istanbuljs/schema": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", - "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", - "dev": true - }, - "@jest/console": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/console/-/console-26.6.2.tgz", - "integrity": "sha512-IY1R2i2aLsLr7Id3S6p2BA82GNWryt4oSvEXLAKc+L2zdi89dSkE8xC1C+0kpATG4JhBJREnQOH7/zmccM2B0g==", + "node_modules/@choojs/findup": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/@choojs/findup/-/findup-0.2.1.tgz", + "integrity": "sha512-YstAqNb0MCN8PjdLCDfRsBcGVRN41f3vgLvaI0IrIcBp4AqILRSS0DeWNGkicC+f/zRIPJLc+9RURVSepwvfBw==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "jest-message-util": "^26.6.2", - "jest-util": "^26.6.2", - "slash": "^3.0.0" + "dependencies": { + "commander": "^2.15.1" + }, + "bin": { + "findup": "bin/findup.js" } }, - "@jest/core": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/core/-/core-26.6.3.tgz", - "integrity": "sha512-xvV1kKbhfUqFVuZ8Cyo+JPpipAHHAV3kcDBftiduK8EICXmTFddryy3P7NfZt8Pv37rA9nEJBKCCkglCPt/Xjw==", + "node_modules/@csstools/color-helpers": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", + "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/reporters": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-changed-files": "^26.6.2", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-resolve-dependencies": "^26.6.3", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "jest-watcher": "^26.6.2", - "micromatch": "^4.0.2", - "p-each-series": "^2.1.0", - "rimraf": "^3.0.0", - "slash": "^3.0.0", - "strip-ansi": "^6.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" } }, - "@jest/environment": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-26.6.2.tgz", - "integrity": "sha512-nFy+fHl28zUrRsCeMB61VDThV1pVTtlEokBRgqPrcT1JNq4yRNIyTHfyht6PqtUvY9IsuLGTrbG8kPXjSZIZwA==", + "node_modules/@csstools/css-calc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", + "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", "dev": true, - "requires": { - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "@jest/fake-timers": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-26.6.2.tgz", - "integrity": "sha512-14Uleatt7jdzefLPYM3KLcnUl1ZNikaKq34enpb5XG9i81JpppDb5muZvonvKyrl7ftEHkKS5L5/eB/kxJ+bvA==", + "node_modules/@csstools/css-color-parser": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", + "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@sinonjs/fake-timers": "^6.0.1", - "@types/node": "*", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "dependencies": { + "@csstools/color-helpers": "^5.1.0", + "@csstools/css-calc": "^2.1.4" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "@jest/globals": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-26.6.2.tgz", - "integrity": "sha512-85Ltnm7HlB/KesBUuALwQ68YTU72w9H2xW9FjZ1eL1U3lhtefjjl5c2MiUbpXt/i6LaPRvoOFJ22yCBSfQ0JIA==", + "node_modules/@csstools/css-parser-algorithms": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", + "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", "dev": true, - "requires": { - "@jest/environment": "^26.6.2", - "@jest/types": "^26.6.2", - "expect": "^26.6.2" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-tokenizer": "^3.0.4" } }, - "@jest/reporters": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-26.6.2.tgz", - "integrity": "sha512-h2bW53APG4HvkOnVMo8q3QXa6pcaNt1HkwVsOPMBV6LD/q9oSpxNSYZQYkAnjdMjrJ86UuYeLo+aEZClV6opnw==", + "node_modules/@csstools/css-tokenizer": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", + "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", "dev": true, - "requires": { - "@bcoe/v8-coverage": "^0.2.3", - "@jest/console": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.2", - "graceful-fs": "^4.2.4", - "istanbul-lib-coverage": "^3.0.0", - "istanbul-lib-instrument": "^4.0.3", - "istanbul-lib-report": "^3.0.0", - "istanbul-lib-source-maps": "^4.0.0", - "istanbul-reports": "^3.0.2", - "jest-haste-map": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "node-notifier": "^8.0.0", - "slash": "^3.0.0", - "source-map": "^0.6.0", - "string-length": "^4.0.1", - "terminal-link": "^2.0.0", - "v8-to-istanbul": "^7.0.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "license": "MIT", + "engines": { + "node": ">=18" } }, - "@jest/source-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-26.6.2.tgz", - "integrity": "sha512-YwYcCwAnNmOVsZ8mr3GfnzdXDAl4LaenZP5z+G0c8bzC9/dugL8zRmxZzdoTl4IaS3CryS1uWnROLPFmb6lVvA==", + "node_modules/@csstools/media-query-list-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-3.0.1.tgz", + "integrity": "sha512-HNo8gGD02kHmcbX6PvCoUuOQvn4szyB9ca63vZHKX5A81QytgDG4oxG4IaEfHTlEZSZ6MjPEMWIVU+zF2PZcgw==", "dev": true, - "requires": { - "callsites": "^3.0.0", - "graceful-fs": "^4.2.4", - "source-map": "^0.6.0" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1" } }, - "@jest/test-result": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-26.6.2.tgz", - "integrity": "sha512-5O7H5c/7YlojphYNrK02LlDIV2GNPYisKwHm2QTKjNZeEzezCbwYs9swJySv2UfPMyZ0VdsmMv7jIlD/IKYQpQ==", + "node_modules/@csstools/selector-specificity": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", + "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/istanbul-lib-coverage": "^2.0.0", - "collect-v8-coverage": "^1.0.0" + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" + } + ], + "license": "MIT-0", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "postcss-selector-parser": "^7.0.0" } }, - "@jest/test-sequencer": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-26.6.3.tgz", - "integrity": "sha512-YHlVIjP5nfEyjlrSr8t/YdNfU/1XEt7c5b4OxcXCjyRhjzLYu/rO69/WHPuYcbCWkz8kAeZVZp2N2+IOLLEPGw==", + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", "dev": true, - "requires": { - "@jest/test-result": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3" + "engines": { + "node": ">=10.0.0" } }, - "@jest/transform": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-26.6.2.tgz", - "integrity": "sha512-E9JjhUgNzvuQ+vVAL21vlyfy12gP0GhazGgJC4h6qUt1jSdUXGWJ1wfu/X7Sd8etSgxV4ovT1pb9v5D6QW4XgA==", + "node_modules/@dual-bundle/import-meta-resolve": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@dual-bundle/import-meta-resolve/-/import-meta-resolve-4.2.1.tgz", + "integrity": "sha512-id+7YRUgoUX6CgV0DtuhirQWodeeA7Lf4i2x71JS/vtA5pRb/hIGWlw+G6MeXvsM+MXrz0VAydTGElX1rAfgPg==", "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/types": "^26.6.2", - "babel-plugin-istanbul": "^6.0.0", - "chalk": "^4.0.0", - "convert-source-map": "^1.4.0", - "fast-json-stable-stringify": "^2.0.0", - "graceful-fs": "^4.2.4", - "jest-haste-map": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-util": "^26.6.2", - "micromatch": "^4.0.2", - "pirates": "^4.0.1", - "slash": "^3.0.0", - "source-map": "^0.6.1", - "write-file-atomic": "^3.0.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/JounQin" } }, - "@jest/types": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-26.6.2.tgz", - "integrity": "sha512-fC6QCp7Sc5sX6g8Tvbmj4XUTbyrik0akgRy03yjXbQaBWWNWGE7SGtJk98m0N8nzegD/7SggrUlivxo5ax4KWQ==", + "node_modules/@emnapi/core": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.0.tgz", + "integrity": "sha512-0DQ98G9ZQZOxfUcQn1waV2yS8aWdZ6kJMbYCJB3oUBecjWYO1fqJ+a1DRfPF3O5JEkwqwP1A9QEN/9mYm2Yd0w==", "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0" + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" } }, - "@juggle/resize-observer": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/@juggle/resize-observer/-/resize-observer-3.3.1.tgz", - "integrity": "sha512-zMM9Ds+SawiUkakS7y94Ymqx+S0ORzpG3frZirN3l+UlXUmSUR7hF4wxCVqW+ei94JzV5kt0uXBcoOEAuiydrw==" - }, - "@mrmlnc/readdir-enhanced": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", - "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "node_modules/@emnapi/runtime": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.0.tgz", + "integrity": "sha512-QN75eB0IH2ywSpRpNddCRfQIhmJYBCJ1x5Lb3IscKAL8bMnVAKnRg8dCoXbHzVLLH7P38N2Z3mtulB7W0J0FKw==", "dev": true, - "requires": { - "call-me-maybe": "^1.0.1", - "glob-to-regexp": "^0.3.0" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", "dev": true, - "requires": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "dev": true - }, - "@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@es-joy/jsdoccomment": { + "version": "0.41.0", + "resolved": "https://registry.npmjs.org/@es-joy/jsdoccomment/-/jsdoccomment-0.41.0.tgz", + "integrity": "sha512-aKUhyn1QI5Ksbqcr3fFJj16p99QdjUxXAEuFst1Z47DRyoiMwivIH9MV/ARcJOCXVjPfjITciej8ZD2O/6qUmw==", "dev": true, - "requires": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "license": "MIT", + "dependencies": { + "comment-parser": "1.4.1", + "esquery": "^1.5.0", + "jsdoc-type-pratt-parser": "~4.0.0" + }, + "engines": { + "node": ">=16" } }, - "@npmcli/fs": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-1.0.0.tgz", - "integrity": "sha512-8ltnOpRR/oJbOp8vaGUnipOi3bqkcW+sLHFlyXIr08OGHmVJLB1Hn7QtGXbYcpVtH1gAYZTlmDXtE4YV0+AMMQ==", + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, - "requires": { - "@gar/promisify": "^1.0.1", - "semver": "^7.3.5" - }, + "license": "MIT", "dependencies": { - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, - "@npmcli/move-file": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@npmcli/move-file/-/move-file-1.1.2.tgz", - "integrity": "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg==", + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", "dev": true, - "requires": { - "mkdirp": "^1.0.4", - "rimraf": "^3.0.2" + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" }, - "dependencies": { - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "funding": { + "url": "https://opencollective.com/eslint" } }, - "@polka/url": { - "version": "1.0.0-next.20", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.20.tgz", - "integrity": "sha512-88p7+M0QGxKpmnkfXjS4V26AnoC/eiqZutE8GLdaI5X12NY75bXSdTY9NkmYb2Xyk1O+MmkuO6Frmsj84V6I8Q==", - "dev": true - }, - "@popperjs/core": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/@popperjs/core/-/core-2.9.3.tgz", - "integrity": "sha512-xDu17cEfh7Kid/d95kB6tZsLOmSWKCZKtprnhVepjsSaCij+lM3mItSJDuuHDMbCWTh8Ejmebwb+KONcCJ0eXQ==" - }, - "@sideway/address": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.1.tgz", - "integrity": "sha512-+I5aaQr3m0OAmMr7RQ3fR9zx55sejEYR2BFJaxL+zT3VM2611X0SHvPWIbAUBZVTn/YzYKbV8gJ2oT/QELknfQ==", + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - }, - "dependencies": { - "@hapi/hoek": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", - "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==", - "dev": true - } + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" } }, - "@sideway/formula": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.0.tgz", - "integrity": "sha512-vHe7wZ4NOXVfkoRb8T5otiENVlT7a3IAiw7H5M2+GO+9CDgcVUUsX1zalAztCmwyOr2RUTGJdgB+ZvSVqmdHmg==", - "dev": true + "node_modules/@eslint/eslintrc": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-2.1.4.tgz", + "integrity": "sha512-269Z39MS6wVJtsoUl10L60WdkhJVdPG24Q4eZTH3nnF6lpvSShEK3wQjDX9JRWAUPvPh7COouPpU9IrqaZFvtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.12.4", + "debug": "^4.3.2", + "espree": "^9.6.0", + "globals": "^13.19.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.0", + "minimatch": "^3.1.2", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "dev": true + "node_modules/@eslint/eslintrc/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" }, - "@sindresorhus/is": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", - "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", + "node_modules/@eslint/eslintrc/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "@sinonjs/commons": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.1.tgz", - "integrity": "sha512-892K+kWUUi3cl+LlqEWIDrhvLgdL79tECi8JZUyq6IviKy/DNhuzCRlbHUjxK89f4ypPMMaFnFuR9Ie6DoIMsw==", + "node_modules/@eslint/eslintrc/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "requires": { - "type-detect": "4.0.8" + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "@sinonjs/fake-timers": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-6.0.1.tgz", - "integrity": "sha512-MZPUxrmFubI36XS1DI3qmI0YdN1gks62JtFZvxR67ljjSNCeK6U08Zx4msEWOXuofgqUt6zPHSi1H9fbjR/NRA==", + "node_modules/@eslint/js": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-8.57.1.tgz", + "integrity": "sha512-d9zaMRSTIKDLhctzH12MtXvJKSSUhaHcjV+2Z+GK+EEY7XKpP5yR4x+N3TAcHTcu963nIr+TMcCb4DBCYX1z6Q==", "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, - "@sinonjs/samsam": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-6.0.2.tgz", - "integrity": "sha512-jxPRPp9n93ci7b8hMfJOFDPRLFYadN6FSpeROFTR4UNF4i5b+EK6m4QXPO46BDhFgRy1JuS87zAnFOzCUwMJcQ==", + "node_modules/@financial-times/polyfill-useragent-normaliser": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@financial-times/polyfill-useragent-normaliser/-/polyfill-useragent-normaliser-2.0.1.tgz", + "integrity": "sha512-I6zZJgy3cwM9ojo9NVNj5kNTkt96KIKhJQIPQ7vjROaDCpy+sTRQA2HLCjBFKgo0AzsqdMB43Tx0Szt/mf+00g==", + "dependencies": { + "@financial-times/useragent_parser": "^1.6.1", + "semver": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@financial-times/useragent_parser": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@financial-times/useragent_parser/-/useragent_parser-1.6.3.tgz", + "integrity": "sha512-TlQiXt/vS5ZwY0V3salvlyQzIzMGZEyw9inmJA25A8heL2kBVENbToiEc64R6ETNf5YHa2lwnc2I7iNHP9SqeQ==" + }, + "node_modules/@formatjs/ecma402-abstract": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/@formatjs/ecma402-abstract/-/ecma402-abstract-2.3.2.tgz", + "integrity": "sha512-6sE5nyvDloULiyOMbOTJEEgWL32w+VHkZQs8S02Lnn8Y/O5aQhjOEXwWzvR7SsBE/exxlSpY2EsWZgqHbtLatg==", "dev": true, - "requires": { - "@sinonjs/commons": "^1.6.0", - "lodash.get": "^4.4.2", - "type-detect": "^4.0.8" + "dependencies": { + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/intl-localematcher": "0.5.10", + "decimal.js": "10", + "tslib": "2" } }, - "@sinonjs/text-encoding": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.1.tgz", - "integrity": "sha512-+iTbntw2IZPb/anVDbypzfQa+ay64MW0Zo8aJ8gZPWMMK6/OubMVb6lUPMagqjOPnmtauXnFCACVl3O7ogjeqQ==", - "dev": true + "node_modules/@formatjs/fast-memoize": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@formatjs/fast-memoize/-/fast-memoize-2.2.6.tgz", + "integrity": "sha512-luIXeE2LJbQnnzotY1f2U2m7xuQNj2DA8Vq4ce1BY9ebRZaoPB1+8eZ6nXpLzsxuW5spQxr7LdCg+CApZwkqkw==", + "dev": true, + "dependencies": { + "tslib": "2" + } }, - "@stylelint/postcss-css-in-js": { - "version": "0.37.2", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-css-in-js/-/postcss-css-in-js-0.37.2.tgz", - "integrity": "sha512-nEhsFoJurt8oUmieT8qy4nk81WRHmJynmVwn/Vts08PL9fhgIsMhk1GId5yAN643OzqEEb5S/6At2TZW7pqPDA==", + "node_modules/@formatjs/icu-messageformat-parser": { + "version": "2.11.0", + "resolved": "https://registry.npmjs.org/@formatjs/icu-messageformat-parser/-/icu-messageformat-parser-2.11.0.tgz", + "integrity": "sha512-Hp81uTjjdTk3FLh/dggU5NK7EIsVWc5/ZDWrIldmf2rBuPejuZ13CZ/wpVE2SToyi4EiroPTQ1XJcJuZFIxTtw==", "dev": true, - "requires": { - "@babel/core": ">=7.9.0" + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/icu-skeleton-parser": "1.8.12", + "tslib": "2" } }, - "@stylelint/postcss-markdown": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/@stylelint/postcss-markdown/-/postcss-markdown-0.36.2.tgz", - "integrity": "sha512-2kGbqUVJUGE8dM+bMzXG/PYUWKkjLIkRLWNh39OaADkiabDRdw8ATFCgbMz5xdIcvwspPAluSL7uY+ZiTWdWmQ==", + "node_modules/@formatjs/icu-skeleton-parser": { + "version": "1.8.12", + "resolved": "https://registry.npmjs.org/@formatjs/icu-skeleton-parser/-/icu-skeleton-parser-1.8.12.tgz", + "integrity": "sha512-QRAY2jC1BomFQHYDMcZtClqHR55EEnB96V7Xbk/UiBodsuFc5kujybzt87+qj1KqmJozFhk6n4KiT1HKwAkcfg==", "dev": true, - "requires": { - "remark": "^13.0.0", - "unist-util-find-all-after": "^3.0.2" + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "tslib": "2" } }, - "@svgr/babel-plugin-add-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-ZFf2gs/8/6B8PnSofI0inYXr2SDNTDScPXhN7k5EqD4aZ3gi6u+rbmZHVB8IM3wDyx8ntKACZbtXSm7oZGRqVg==", - "dev": true + "node_modules/@formatjs/intl-localematcher": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/@formatjs/intl-localematcher/-/intl-localematcher-0.5.10.tgz", + "integrity": "sha512-af3qATX+m4Rnd9+wHcjJ4w2ijq+rAVP3CCinJQvFv1kgSu1W6jypUmvleJxcewdxmutM8dmIRZFxO/IQBZmP2Q==", + "dev": true, + "dependencies": { + "tslib": "2" + } }, - "@svgr/babel-plugin-remove-jsx-attribute": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-5.4.0.tgz", - "integrity": "sha512-yaS4o2PgUtwLFGTKbsiAy6D0o3ugcUhWK0Z45umJ66EPWunAz9fuFw2gJuje6wqQvQWOTJvIahUwndOXb7QCPg==", - "dev": true + "node_modules/@hapi/address": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/@hapi/address/-/address-5.1.1.tgz", + "integrity": "sha512-A+po2d/dVoY7cYajycYI43ZbYMXukuopIsqCjh5QzsBCipDtdofHntljDlpccMjIfTy6UOkg+5KPriwYch2bXA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + }, + "engines": { + "node": ">=14.0.0" + } }, - "@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-5.0.1.tgz", - "integrity": "sha512-LA72+88A11ND/yFIMzyuLRSMJ+tRKeYKeQ+mR3DcAZ5I4h5CPWN9AHyUzJbWSYp/u2u0xhmgOe0+E41+GjEueA==", - "dev": true + "node_modules/@hapi/formula": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@hapi/formula/-/formula-3.0.2.tgz", + "integrity": "sha512-hY5YPNXzw1He7s0iqkRQi+uMGh383CGdyyIGYtB+W5N3KHPXoqychklvHhKCC9M3Xtv0OCs/IHw+r4dcHtBYWw==", + "dev": true, + "license": "BSD-3-Clause" }, - "@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-5.0.1.tgz", - "integrity": "sha512-PoiE6ZD2Eiy5mK+fjHqwGOS+IXX0wq/YDtNyIgOrc6ejFnxN4b13pRpiIPbtPwHEc+NT2KCjteAcq33/F1Y9KQ==", - "dev": true + "node_modules/@hapi/hoek": { + "version": "11.0.7", + "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-11.0.7.tgz", + "integrity": "sha512-HV5undWkKzcB4RZUusqOpcgxOaq6VOAH7zhhIr2g3G8NF/MlFO75SjOr2NfuSx0Mh40+1FqCkagKLJRykUWoFQ==", + "dev": true, + "license": "BSD-3-Clause" }, - "@svgr/babel-plugin-svg-dynamic-title": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-5.4.0.tgz", - "integrity": "sha512-zSOZH8PdZOpuG1ZVx/cLVePB2ibo3WPpqo7gFIjLV9a0QsuQAzJiwwqmuEdTaW2pegyBE17Uu15mOgOcgabQZg==", - "dev": true + "node_modules/@hapi/pinpoint": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@hapi/pinpoint/-/pinpoint-2.0.1.tgz", + "integrity": "sha512-EKQmr16tM8s16vTT3cA5L0kZZcTMU5DUOZTuvpnY738m+jyP3JIUj+Mm1xc1rsLkGBQ/gVnfKYPwOmPg1tUR4Q==", + "dev": true, + "license": "BSD-3-Clause" }, - "@svgr/babel-plugin-svg-em-dimensions": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-5.4.0.tgz", - "integrity": "sha512-cPzDbDA5oT/sPXDCUYoVXEmm3VIoAWAPT6mSPTJNbQaBNUuEKVKyGH93oDY4e42PYHRW67N5alJx/eEol20abw==", - "dev": true + "node_modules/@hapi/tlds": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@hapi/tlds/-/tlds-1.1.4.tgz", + "integrity": "sha512-Fq+20dxsxLaUn5jSSWrdtSRcIUba2JquuorF9UW1wIJS5cSUwxIsO2GIhaWynPRflvxSzFN+gxKte2HEW1OuoA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=14.0.0" + } }, - "@svgr/babel-plugin-transform-react-native-svg": { - "version": "5.4.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-5.4.0.tgz", - "integrity": "sha512-3eYP/SaopZ41GHwXma7Rmxcv9uRslRDTY1estspeB1w1ueZWd/tPlMfEOoccYpEMZU3jD4OU7YitnXcF5hLW2Q==", - "dev": true + "node_modules/@hapi/topo": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-6.0.2.tgz", + "integrity": "sha512-KR3rD5inZbGMrHmgPxsJ9dbi6zEK+C3ZwUwTa+eMwWLz7oijWUTWD2pMSNNYJAU6Qq+65NkxXjqHr/7LM2Xkqg==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/hoek": "^11.0.2" + } }, - "@svgr/babel-plugin-transform-svg-component": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-5.5.0.tgz", - "integrity": "sha512-q4jSH1UUvbrsOtlo/tKcgSeiCHRSBdXoIoqX1pgcKK/aU3JD27wmMKwGtpB8qRYUYoyXvfGxUVKchLuR5pB3rQ==", - "dev": true + "node_modules/@humanwhocodes/config-array": { + "version": "0.13.0", + "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", + "integrity": "sha512-DZLEEqFWQFiyK6h5YIeynKx7JlvCYWL0cImfSRXZ9l4Sg2efkFGTuFf6vzXjK1cq6IYkU+Eg/JizXw+TD2vRNw==", + "deprecated": "Use @eslint/config-array instead", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanwhocodes/object-schema": "^2.0.3", + "debug": "^4.3.1", + "minimatch": "^3.0.5" + }, + "engines": { + "node": ">=10.10.0" + } }, - "@svgr/babel-preset": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-5.5.0.tgz", - "integrity": "sha512-4FiXBjvQ+z2j7yASeGPEi8VD/5rrGQk4Xrq3EdJmoZgz/tpqChpo5hgXDvmEauwtvOc52q8ghhZK4Oy7qph4ig==", + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", "dev": true, - "requires": { - "@svgr/babel-plugin-add-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-attribute": "^5.4.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "^5.0.1", - "@svgr/babel-plugin-replace-jsx-attribute-value": "^5.0.1", - "@svgr/babel-plugin-svg-dynamic-title": "^5.4.0", - "@svgr/babel-plugin-svg-em-dimensions": "^5.4.0", - "@svgr/babel-plugin-transform-react-native-svg": "^5.4.0", - "@svgr/babel-plugin-transform-svg-component": "^5.5.0" + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" } }, - "@svgr/core": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-5.5.0.tgz", - "integrity": "sha512-q52VOcsJPvV3jO1wkPtzTuKlvX7Y3xIcWRpCMtBF3MrteZJtBfQw/+u0B1BHy5ColpQc1/YVTrPEtSYIMNZlrQ==", + "node_modules/@humanwhocodes/object-schema": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/object-schema/-/object-schema-2.0.3.tgz", + "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", + "deprecated": "Use @eslint/object-schema instead", "dev": true, - "requires": { - "@svgr/plugin-jsx": "^5.5.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^7.0.0" + "license": "BSD-3-Clause" + }, + "node_modules/@istanbuljs/load-nyc-config": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@istanbuljs/load-nyc-config/-/load-nyc-config-1.1.0.tgz", + "integrity": "sha512-VjeHSlIzpv/NyD3N0YuHfXOPDIixcA1q2ZV98wsMqcYlPmv2n3Yb2lYP9XMElnaFVXg5A7YLTeLu6V84uQDjmQ==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "find-up": "^4.1.0", + "get-package-type": "^0.1.0", + "js-yaml": "^3.13.1", + "resolve-from": "^5.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@istanbuljs/load-nyc-config/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, "dependencies": { - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - } + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@svgr/hast-util-to-babel-ast": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-5.5.0.tgz", - "integrity": "sha512-cAaR/CAiZRB8GP32N+1jocovUtvlj0+e65TB50/6Lcime+EA49m/8l+P2ko+XPJ4dw3xaPS3jOL4F2X4KWxoeQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", "dev": true, - "requires": { - "@babel/types": "^7.12.6" + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" } }, - "@svgr/plugin-jsx": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-5.5.0.tgz", - "integrity": "sha512-V/wVh33j12hGh05IDg8GpIUXbjAPnTdPTKuP4VNLggnwaHMPNQNae2pRnyTAILWCQdz5GyMqtO488g7CKM8CBA==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@svgr/babel-preset": "^5.5.0", - "@svgr/hast-util-to-babel-ast": "^5.5.0", - "svg-parser": "^2.0.2" + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "@svgr/plugin-svgo": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-5.5.0.tgz", - "integrity": "sha512-r5swKk46GuQl4RrVejVwpeeJaydoxkdwkM1mBKOgJLBUJPGaLci6ylg/IjhrRsREKDkr4kbMWdgOtbXEh0fyLQ==", + "node_modules/@istanbuljs/load-nyc-config/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "deepmerge": "^4.2.2", - "svgo": "^1.2.2" - }, - "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - } + "engines": { + "node": ">=8" } }, - "@svgr/webpack": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-5.5.0.tgz", - "integrity": "sha512-DOBOK255wfQxguUta2INKkzPj6AIS6iafZYiYmHn6W3pHlycSRRlvWKCfLDG10fXfLWqE3DJHgRUOyJYmARa7g==", + "node_modules/@istanbuljs/schema": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@istanbuljs/schema/-/schema-0.1.3.tgz", + "integrity": "sha512-ZXRY4jNvVgSVQ8DL3LTcakaAtXwTVUxE81hslsyD2AtoXW/wVob10HkOJ1X/pAlcI7D+2YoZKg5do8G/w6RYgA==", "dev": true, - "requires": { - "@babel/core": "^7.12.3", - "@babel/plugin-transform-react-constant-elements": "^7.12.1", - "@babel/preset-env": "^7.12.1", - "@babel/preset-react": "^7.12.5", - "@svgr/core": "^5.5.0", - "@svgr/plugin-jsx": "^5.5.0", - "@svgr/plugin-svgo": "^5.5.0", - "loader-utils": "^2.0.0" - }, - "dependencies": { - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - } + "engines": { + "node": ">=8" } }, - "@tannin/compile": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/compile/-/compile-1.1.0.tgz", - "integrity": "sha512-n8m9eNDfoNZoxdvWiTfW/hSPhehzLJ3zW7f8E7oT6mCROoMNWCB4TYtv041+2FMAxweiE0j7i1jubQU4MEC/Gg==", - "requires": { - "@tannin/evaluate": "^1.2.0", - "@tannin/postfix": "^1.1.0" + "node_modules/@jest/console": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/console/-/console-29.7.0.tgz", + "integrity": "sha512-5Ni4CU7XHQi32IJ398EEP4RrB8eV09sXP2ROqD4bksHrnTree52PsxvX8tpL8LvTZ3pFzXyPbNQReSN41CAhOg==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@tannin/evaluate": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@tannin/evaluate/-/evaluate-1.2.0.tgz", - "integrity": "sha512-3ioXvNowbO/wSrxsDG5DKIMxC81P0QrQTYai8zFNY+umuoHWRPbQ/TuuDEOju9E+jQDXmj6yI5GyejNuh8I+eg==" + "node_modules/@jest/console/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "@tannin/plural-forms": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/plural-forms/-/plural-forms-1.1.0.tgz", - "integrity": "sha512-xl9R2mDZO/qiHam1AgMnAES6IKIg7OBhcXqy6eDsRCdXuxAFPcjrej9HMjyCLE0DJ/8cHf0i5OQTstuBRhpbHw==", - "requires": { - "@tannin/compile": "^1.1.0" + "node_modules/@jest/console/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "@tannin/postfix": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@tannin/postfix/-/postfix-1.1.0.tgz", - "integrity": "sha512-oocsqY7g0cR+Gur5jRQLSrX2OtpMLMse1I10JQBm8CdGMrDkh1Mg2gjsiquMHRtBs4Qwu5wgEp5GgIYHk4SNPw==" + "node_modules/@jest/console/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "@tootallnate/once": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", - "integrity": "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw==", + "node_modules/@jest/console/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "@types/babel__core": { - "version": "7.1.15", - "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.1.15.tgz", - "integrity": "sha512-bxlMKPDbY8x5h6HBwVzEOk2C8fb6SLfYQ5Jw3uBYuYF1lfWk/kbLd81la82vrIkBb0l+JdmrZaDikPrNxpS/Ew==", + "node_modules/@jest/console/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0", - "@types/babel__generator": "*", - "@types/babel__template": "*", - "@types/babel__traverse": "*" + "engines": { + "node": ">=8" } }, - "@types/babel__generator": { - "version": "7.6.3", - "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.3.tgz", - "integrity": "sha512-/GWCmzJWqV7diQW54smJZzWbSFf4QYtF71WCKhcx6Ru/tFyQIY2eiiITcCAeuPbNSvT9YCGkVMqqvSk2Z0mXiA==", + "node_modules/@jest/console/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "@babel/types": "^7.0.0" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@types/babel__template": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.1.tgz", - "integrity": "sha512-azBFKemX6kMg5Io+/rdGT0dkGreboUVR0Cdm3fz9QJWpaQGJRQXl7C+6hOTCZcMll7KFyEQpgbYI2lHdsS4U7g==", + "node_modules/@jest/core": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/core/-/core-29.7.0.tgz", + "integrity": "sha512-n7aeXWKMnGtDA48y8TLWJPJmLmmZ642Ceo78cYWEpiD7FzDgmNDV/GCVRorPABdXLJZ/9wzzgZAlHjXjxDHGsg==", "dev": true, - "requires": { - "@babel/parser": "^7.1.0", - "@babel/types": "^7.0.0" + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/reporters": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-changed-files": "^29.7.0", + "jest-config": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-resolve-dependencies": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "jest-watcher": "^29.7.0", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "@types/babel__traverse": { - "version": "7.14.2", - "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.14.2.tgz", - "integrity": "sha512-K2waXdXBi2302XUdcHcR1jCeU0LL4TD9HRs/gk0N2Xvrht+G/BfJa4QObBQZfhMdxiCpV3COl5Nfq4uKTeTnJA==", + "node_modules/@jest/core/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "@babel/types": "^7.3.0" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@types/cheerio": { - "version": "0.22.30", - "resolved": "https://registry.npmjs.org/@types/cheerio/-/cheerio-0.22.30.tgz", - "integrity": "sha512-t7ZVArWZlq3dFa9Yt33qFBQIK4CQd1Q3UJp0V+UhP6vgLWLM6Qug7vZuRSGXg45zXeB1Fm5X2vmBkEX58LV2Tw==", + "node_modules/@jest/core/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "@types/component-emitter": { - "version": "1.2.10", - "resolved": "https://registry.npmjs.org/@types/component-emitter/-/component-emitter-1.2.10.tgz", - "integrity": "sha512-bsjleuRKWmGqajMerkzox19aGbscQX5rmmvvXl3wlIp5gMG1HgkiwPxsN5p070fBDKTNSPgojVbuY1+HWMbFhg==" - }, - "@types/cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-y7mImlc/rNkvCRmg8gC3/lj87S7pTUIJ6QGjwHR9WQJcFs+ZMTOaoPrkdFA/YdbuqVEmEbb5RdhVxMkAcgOnpg==" - }, - "@types/cors": { - "version": "2.8.10", - "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.10.tgz", - "integrity": "sha512-C7srjHiVG3Ey1nR6d511dtDkCEjxuN9W1HWAEjGq8kpcwmNM6JJkpC0xvabM7BXTG2wDq8Eu33iH9aQKa7IvLQ==" - }, - "@types/glob": { - "version": "7.1.3", - "resolved": "https://registry.npmjs.org/@types/glob/-/glob-7.1.3.tgz", - "integrity": "sha512-SEYeGAIQIQX8NN6LDKprLjbrd5dARM5EXsd8GI/A5l0apYI1fGMWgPHSe4ZKL4eozlAyI+doUE9XbYS4xCkQ1w==", + "node_modules/@jest/core/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { - "@types/minimatch": "*", - "@types/node": "*" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "@types/graceful-fs": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.5.tgz", - "integrity": "sha512-anKkLmZZ+xm4p8JWBf4hElkM4XR+EZeA2M9BAkkTldmcyDY4mbdIJnRghDJH3Ov5ooY7/UAoENtmdMSkaAd7Cw==", + "node_modules/@jest/core/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "@types/node": "*" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "@types/istanbul-lib-coverage": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.3.tgz", - "integrity": "sha512-sz7iLqvVUg1gIedBOvlkxPlc8/uVzyS5OwGz1cKjXzkl3FpL3al0crU8YGU1WoHkxn0Wxbw5tyi6hvzJKNzFsw==", + "node_modules/@jest/core/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "@types/istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", + "node_modules/@jest/core/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "requires": { - "@types/istanbul-lib-coverage": "*" + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@types/istanbul-reports": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.1.tgz", - "integrity": "sha512-c3mAZEuK0lvBp8tmuL74XRKn1+y2dcwOUpH7x4WrF6gk1GIgiluDRgMYQtw2OFcBvAJWlt6ASU3tSqxp0Uu0Aw==", + "node_modules/@jest/core/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "@types/istanbul-lib-report": "*" + "engines": { + "node": ">=8" } }, - "@types/json-schema": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.6.tgz", - "integrity": "sha512-3c+yGKvVP5Y9TYBEibGNR+kLtijnj7mYrXRg+WpFb2X9xm04g/DXYkfg4hmzJQosc9snFNUPkbYIhu+KAm6jJw==", - "dev": true + "node_modules/@jest/core/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "@types/json5": { - "version": "0.0.29", - "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", - "integrity": "sha1-7ihweulOEdK4J7y+UnC86n8+ce4=", - "dev": true + "node_modules/@jest/core/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } }, - "@types/mdast": { - "version": "3.0.10", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-3.0.10.tgz", - "integrity": "sha512-W864tg/Osz1+9f4lrGTZpCSO5/z4608eUp19tbozkq2HJK6i3z1kT0H9tlADXuYIb1YYOBByU4Jsqkk75q48qA==", + "node_modules/@jest/core/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "@types/unist": "*" + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@types/minimatch": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/minimatch/-/minimatch-3.0.3.tgz", - "integrity": "sha512-tHq6qdbT9U1IRSGf14CL0pUlULksvY9OZ+5eEgl1N7t+OA3tGvNpxJCzuKQlsNgCVwbAs670L1vcVQi8j9HjnA==", - "dev": true + "node_modules/@jest/core/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "@types/minimist": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", - "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", - "dev": true + "node_modules/@jest/core/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } }, - "@types/node": { - "version": "14.14.20", - "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", - "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==" + "node_modules/@jest/environment": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-29.7.0.tgz", + "integrity": "sha512-aQIfHDq33ExsN4jP1NWGXhxgQ/wixs60gDiKO+XVMd8Mn0NWPWgc34ZQDTb2jKaUWQ7MuwoitXAsN2XVXNMpAw==", + "dev": true, + "dependencies": { + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "@types/normalize-package-data": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", - "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment-jsdom-abstract/-/environment-jsdom-abstract-30.3.0.tgz", + "integrity": "sha512-0hNFs5N6We3DMCwobzI0ydhkY10sT1tZSC0AAiy+0g2Dt/qEWgrcV5BrMxPczhe41cxW4qm6X+jqZaUdpZIajA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/jsdom": "^21.1.7", + "@types/node": "*", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } }, - "@types/parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==" + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "@types/prettier": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/@types/prettier/-/prettier-2.3.2.tgz", - "integrity": "sha512-eI5Yrz3Qv4KPUa/nSIAi0h+qX0XyewOliug5F2QAtuRg6Kjg6jfmxe1GIwoIRhZspD1A0RP8ANrPwvEXXtRFog==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "@types/prop-types": { - "version": "15.7.4", - "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.4.tgz", - "integrity": "sha512-rZ5drC/jWjrArrS8BR6SIr4cWpW09RNTYt9AMZo3Jwwif+iacXAqgVjm0B0Bv/S1jhDXKHqRVNCbACkJ89RAnQ==" + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "@types/q": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", - "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "@types/react": { - "version": "16.14.14", - "resolved": "https://registry.npmjs.org/@types/react/-/react-16.14.14.tgz", - "integrity": "sha512-uwIWDYW8LznHzEMJl7ag9St1RsK0gw/xaFZ5+uI1ZM1HndwUgmPH3/wQkSb87GkOVg7shUxnpNW8DcN0AzvG5Q==", - "requires": { - "@types/prop-types": "*", - "@types/scheduler": "*", - "csstype": "^3.0.2" + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jest/environment-jsdom-abstract/node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" } }, - "@types/react-dom": { - "version": "16.9.14", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.14.tgz", - "integrity": "sha512-FIX2AVmPTGP30OUJ+0vadeIFJJ07Mh1m+U0rxfgyW34p3rTlXI+nlenvAxNn4BP36YyI9IJ/+UJ7Wu22N1pI7A==", - "requires": { - "@types/react": "^16" + "node_modules/@jest/environment-jsdom-abstract/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@types/scheduler": { - "version": "0.16.2", - "resolved": "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.2.tgz", - "integrity": "sha512-hppQEBDmlwhFAXKJX2KnWLYu5yMfi91yazPb2l+lbJiwW+wdo1gNeRA+3RgNSO39WYX2euey41KEwnqesU2Jew==" + "node_modules/@jest/environment-jsdom-abstract/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "@types/source-list-map": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/@types/source-list-map/-/source-list-map-0.1.2.tgz", - "integrity": "sha512-K5K+yml8LTo9bWJI/rECfIPrGgxdpeNbj+d53lwN4QjW1MCwlkhUms+gtdzigTeUyBr09+u8BwOIY3MXvHdcsA==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "@types/stack-utils": { + "node_modules/@jest/environment-jsdom-abstract/node_modules/color-convert": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.1.tgz", - "integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==", - "dev": true + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } }, - "@types/tapable": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/tapable/-/tapable-1.0.8.tgz", - "integrity": "sha512-ipixuVrh2OdNmauvtT51o3d8z12p6LtFW9in7U79der/kwejjdNchQC5UMn5u/KxNoM7VHHOs/l8KS8uHxhODQ==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, - "@types/uglify-js": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/@types/uglify-js/-/uglify-js-3.13.1.tgz", - "integrity": "sha512-O3MmRAk6ZuAKa9CHgg0Pr0+lUOqoMLpc9AS4R8ano2auvsg7IE8syF3Xh/NPr26TWklxYcqoEEFdzLLs1fV9PQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "source-map": "^0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=8" } }, - "@types/unist": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.6.tgz", - "integrity": "sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ==", - "dev": true + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } }, - "@types/webpack": { - "version": "4.41.30", - "resolved": "https://registry.npmjs.org/@types/webpack/-/webpack-4.41.30.tgz", - "integrity": "sha512-GUHyY+pfuQ6haAfzu4S14F+R5iGRwN6b2FRNJY7U0NilmFAqbsOfK6j1HwuLBAqwRIT+pVdNDJGJ6e8rpp0KHA==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", "@types/node": "*", - "@types/tapable": "^1", - "@types/uglify-js": "*", - "@types/webpack-sources": "*", - "anymatch": "^3.0.0", - "source-map": "^0.6.0" + "jest-util": "30.3.0" }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "@types/webpack-sources": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@types/webpack-sources/-/webpack-sources-3.2.0.tgz", - "integrity": "sha512-Ft7YH3lEVRQ6ls8k4Ff1oB4jN6oy/XmU6tQISKdhfh+1mR+viZFphS6WL0IrtDOzvefmJg5a0s7ZQoRXwqTEFg==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", "@types/node": "*", - "@types/source-list-map": "*", - "source-map": "^0.7.3" + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "@types/yargs": { - "version": "15.0.14", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-15.0.14.tgz", - "integrity": "sha512-yEJzHoxf6SyQGhBhIYGXQDSCkJjB6HohDShto7m8vaKg9Yp0Yn8+71J9eakh2bnPg6BfsH9PRMhiRTZnd4eXGQ==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", "dev": true, - "requires": { - "@types/yargs-parser": "*" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "@types/yargs-parser": { - "version": "20.2.1", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-20.2.1.tgz", - "integrity": "sha512-7tFImggNeNBVMsn0vLrpn1H1uPrUBdnARPTpZoitY37ZrdJREzf7I16tMrlK3hen349gr1NYh8CmZQa7CTG6Aw==", - "dev": true - }, - "@types/yauzl": { - "version": "2.9.1", - "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", - "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", "dev": true, - "optional": true, - "requires": { - "@types/node": "*" + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "@typescript-eslint/eslint-plugin": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-4.30.0.tgz", - "integrity": "sha512-NgAnqk55RQ/SD+tZFD9aPwNSeHmDHHe5rtUyhIq0ZeCWZEvo4DK9rYz7v9HDuQZFvn320Ot+AikaCKMFKLlD0g==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "4.30.0", - "@typescript-eslint/scope-manager": "4.30.0", - "debug": "^4.3.1", - "functional-red-black-tree": "^1.0.1", - "regexpp": "^3.1.0", - "semver": "^7.3.5", - "tsutils": "^3.21.0" + "license": "MIT", + "engines": { + "node": ">=10" }, - "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@typescript-eslint/experimental-utils": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/experimental-utils/-/experimental-utils-4.30.0.tgz", - "integrity": "sha512-K8RNIX9GnBsv5v4TjtwkKtqMSzYpjqAQg/oSphtxf3xxdt6T0owqnpojztjjTcatSteH3hLj3t/kklKx87NPqw==", + "node_modules/@jest/environment-jsdom-abstract/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "@types/json-schema": "^7.0.7", - "@typescript-eslint/scope-manager": "4.30.0", - "@typescript-eslint/types": "4.30.0", - "@typescript-eslint/typescript-estree": "4.30.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@typescript-eslint/parser": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-4.30.0.tgz", - "integrity": "sha512-HJ0XuluSZSxeboLU7Q2VQ6eLlCwXPBOGnA7CqgBnz2Db3JRQYyBDJgQnop6TZ+rsbSx5gEdWhw4rE4mDa1FnZg==", + "node_modules/@jest/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-8uMeAMycttpva3P1lBHB8VciS9V0XAr3GymPpipdyQXbBcuhkLQOSe8E/p92RyAdToS6ZD1tFkX+CkhoECE0dQ==", "dev": true, - "requires": { - "@typescript-eslint/scope-manager": "4.30.0", - "@typescript-eslint/types": "4.30.0", - "@typescript-eslint/typescript-estree": "4.30.0", - "debug": "^4.3.1" - }, "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - } + "expect": "^29.7.0", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@typescript-eslint/scope-manager": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-4.30.0.tgz", - "integrity": "sha512-VJ/jAXovxNh7rIXCQbYhkyV2Y3Ac/0cVHP/FruTJSAUUm4Oacmn/nkN5zfWmWFEanN4ggP0vJSHOeajtHq3f8A==", + "node_modules/@jest/expect-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.7.0.tgz", + "integrity": "sha512-GlsNBWiFQFCVi9QVSx7f5AgMeLxe9YCCs5PuP2O2LdjDAA8Jh9eX7lA1Jq/xdXw3Wb3hyvlFNfZIfcRetSzYcA==", "dev": true, - "requires": { - "@typescript-eslint/types": "4.30.0", - "@typescript-eslint/visitor-keys": "4.30.0" + "dependencies": { + "jest-get-type": "^29.6.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@typescript-eslint/types": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-4.30.0.tgz", - "integrity": "sha512-YKldqbNU9K4WpTNwBqtAerQKLLW/X2A/j4yw92e3ZJYLx+BpKLeheyzoPfzIXHfM8BXfoleTdiYwpsvVPvHrDw==", - "dev": true - }, - "@typescript-eslint/typescript-estree": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-4.30.0.tgz", - "integrity": "sha512-6WN7UFYvykr/U0Qgy4kz48iGPWILvYL34xXJxvDQeiRE018B7POspNRVtAZscWntEPZpFCx4hcz/XBT+erenfg==", + "node_modules/@jest/fake-timers": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-29.7.0.tgz", + "integrity": "sha512-q4DH1Ha4TTFPdxLsqDXK1d3+ioSL7yL5oCMJZgDYm6i+6CygW5E5xVr/D1HdsGxjt1ZWSfUAs9OxSB/BNelWrQ==", "dev": true, - "requires": { - "@typescript-eslint/types": "4.30.0", - "@typescript-eslint/visitor-keys": "4.30.0", - "debug": "^4.3.1", - "globby": "^11.0.3", - "is-glob": "^4.0.1", - "semver": "^7.3.5", - "tsutils": "^3.21.0" - }, "dependencies": { - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } + "@jest/types": "^29.6.3", + "@sinonjs/fake-timers": "^10.0.2", + "@types/node": "*", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@typescript-eslint/visitor-keys": { - "version": "4.30.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-4.30.0.tgz", - "integrity": "sha512-pNaaxDt/Ol/+JZwzP7MqWc8PJQTUhZwoee/PVlQ+iYoYhagccvoHnC9e4l+C/krQYYkENxznhVSDwClIbZVxRw==", + "node_modules/@jest/globals": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/globals/-/globals-29.7.0.tgz", + "integrity": "sha512-mpiz3dutLbkW2MNFubUGUEVLkTGiqW6yLVTA+JbP6fI6J5iL9Y0Nlg8k95pcF8ctKwCS7WVxteBs29hhfAotzQ==", "dev": true, - "requires": { - "@typescript-eslint/types": "4.30.0", - "eslint-visitor-keys": "^2.0.0" + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/types": "^29.6.3", + "jest-mock": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@webassemblyjs/ast": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.9.0.tgz", - "integrity": "sha512-C6wW5L+b7ogSDVqymbkkvuW9kruN//YisMED04xzeBBqjHa2FYnmvOlS6Xj68xWQRgWvI9cIglsjFowH/RJyEA==", + "node_modules/@jest/pattern": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/@jest/pattern/-/pattern-30.0.1.tgz", + "integrity": "sha512-gWp7NfQW27LaBQz3TITS8L7ZCQ0TLvtmI//4OwlQRx4rnWxcPNIYjxZpDcN4+UlGxgm3jS5QPz8IPTCkb59wZA==", "dev": true, - "requires": { - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0" + "license": "MIT", + "dependencies": { + "@types/node": "*", + "jest-regex-util": "30.0.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "@webassemblyjs/floating-point-hex-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.9.0.tgz", - "integrity": "sha512-TG5qcFsS8QB4g4MhrxK5TqfdNe7Ey/7YL/xN+36rRjl/BlGE/NcBvJcqsRgCP6Z92mRE+7N50pRIi8SmKUbcQA==", - "dev": true - }, - "@webassemblyjs/helper-api-error": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.9.0.tgz", - "integrity": "sha512-NcMLjoFMXpsASZFxJ5h2HZRcEhDkvnNFOAKneP5RbKRzaWJN36NC4jqQHKwStIhGXu5mUWlUUk7ygdtrO8lbmw==", - "dev": true - }, - "@webassemblyjs/helper-buffer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.9.0.tgz", - "integrity": "sha512-qZol43oqhq6yBPx7YM3m9Bv7WMV9Eevj6kMi6InKOuZxhw+q9hOkvq5e/PpKSiLfyetpaBnogSbNCfBwyB00CA==", - "dev": true - }, - "@webassemblyjs/helper-code-frame": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-code-frame/-/helper-code-frame-1.9.0.tgz", - "integrity": "sha512-ERCYdJBkD9Vu4vtjUYe8LZruWuNIToYq/ME22igL+2vj2dQ2OOujIZr3MEFvfEaqKoVqpsFKAGsRdBSBjrIvZA==", + "node_modules/@jest/pattern/node_modules/jest-regex-util": { + "version": "30.0.1", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-30.0.1.tgz", + "integrity": "sha512-jHEQgBXAgc+Gh4g0p3bCevgRCVRkB4VB70zhoAE48gxeSr1hfUOsM/C2WoJgVL7Eyg//hudYENbm3Ne+/dRVVA==", "dev": true, - "requires": { - "@webassemblyjs/wast-printer": "1.9.0" + "license": "MIT", + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" } }, - "@webassemblyjs/helper-fsm": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-fsm/-/helper-fsm-1.9.0.tgz", - "integrity": "sha512-OPRowhGbshCb5PxJ8LocpdX9Kl0uB4XsAjl6jH/dWKlk/mzsANvhwbiULsaiqT5GZGT9qinTICdj6PLuM5gslw==", - "dev": true - }, - "@webassemblyjs/helper-module-context": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-module-context/-/helper-module-context-1.9.0.tgz", - "integrity": "sha512-MJCW8iGC08tMk2enck1aPW+BE5Cw8/7ph/VGZxwyvGbJwjktKkDK7vy7gAmMDx88D7mhDTCNKAW5tED+gZ0W8g==", + "node_modules/@jest/reporters": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/reporters/-/reporters-29.7.0.tgz", + "integrity": "sha512-DApq0KJbJOEzAFYjHADNNxAE3KbhxQB1y5Kplb5Waqw6zVbuWatSnMjE5gs8FUgEPmNsnZA3NCWl9NG0ia04Pg==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0" + "dependencies": { + "@bcoe/v8-coverage": "^0.2.3", + "@jest/console": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "@types/node": "*", + "chalk": "^4.0.0", + "collect-v8-coverage": "^1.0.0", + "exit": "^0.1.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "istanbul-lib-coverage": "^3.0.0", + "istanbul-lib-instrument": "^6.0.0", + "istanbul-lib-report": "^3.0.0", + "istanbul-lib-source-maps": "^4.0.0", + "istanbul-reports": "^3.1.3", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "slash": "^3.0.0", + "string-length": "^4.0.1", + "strip-ansi": "^6.0.0", + "v8-to-istanbul": "^9.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } } }, - "@webassemblyjs/helper-wasm-bytecode": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.9.0.tgz", - "integrity": "sha512-R7FStIzyNcd7xKxCZH5lE0Bqy+hGTwS3LJjuv1ZVxd9O7eHCedSdrId/hMOd20I+v8wDXEn+bjfKDLzTepoaUw==", - "dev": true - }, - "@webassemblyjs/helper-wasm-section": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.9.0.tgz", - "integrity": "sha512-XnMB8l3ek4tvrKUUku+IVaXNHz2YsJyOOmz+MMkZvh8h1uSJpSen6vYnw3IoQ7WwEuAhL8Efjms1ZWjqh2agvw==", + "node_modules/@jest/reporters/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@webassemblyjs/ieee754": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.9.0.tgz", - "integrity": "sha512-dcX8JuYU/gvymzIHc9DgxTzUUTLexWwt8uCTWP3otys596io0L5aW02Gb1RjYpx2+0Jus1h4ZFqjla7umFniTg==", + "node_modules/@jest/reporters/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { - "@xtuc/ieee754": "^1.2.0" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "@webassemblyjs/leb128": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.9.0.tgz", - "integrity": "sha512-ENVzM5VwV1ojs9jam6vPys97B/S65YQtv/aanqnU7D8aSoHFX8GyhGg0CMfyKNIHBuAVjy3tlzd5QMMINa7wpw==", + "node_modules/@jest/reporters/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "@xtuc/long": "4.2.2" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "@webassemblyjs/utf8": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.9.0.tgz", - "integrity": "sha512-GZbQlWtopBTP0u7cHrEx+73yZKrQoBMpwkGEIqlacljhXCkVM1kMQge/Mf+csMJAjEdSwhOyLAS0AoR3AG5P8w==", + "node_modules/@jest/reporters/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "@webassemblyjs/wasm-edit": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.9.0.tgz", - "integrity": "sha512-FgHzBm80uwz5M8WKnMTn6j/sVbqilPdQXTWraSjBwFXSYGirpkSWE2R9Qvz9tNiTKQvoKILpCuTjBKzOIm0nxw==", + "node_modules/@jest/reporters/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/helper-wasm-section": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-opt": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "@webassemblyjs/wast-printer": "1.9.0" + "engines": { + "node": ">=8" } }, - "@webassemblyjs/wasm-gen": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.9.0.tgz", - "integrity": "sha512-cPE3o44YzOOHvlsb4+E9qSqjc9Qf9Na1OO/BHFy4OI91XDE14MjFN4lTMezzaIWdPqHnsTodGGNP+iRSYfGkjA==", + "node_modules/@jest/reporters/node_modules/istanbul-lib-instrument": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", + "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" + "license": "BSD-3-Clause", + "dependencies": { + "@babel/core": "^7.23.9", + "@babel/parser": "^7.23.9", + "@istanbuljs/schema": "^0.1.3", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">=10" } }, - "@webassemblyjs/wasm-opt": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.9.0.tgz", - "integrity": "sha512-Qkjgm6Anhm+OMbIL0iokO7meajkzQD71ioelnfPEj6r4eOFuqm4YC3VBPqXjFyyNwowzbMD+hizmprP/Fwkl2A==", + "node_modules/@jest/reporters/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-buffer": "1.9.0", - "@webassemblyjs/wasm-gen": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0" + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@webassemblyjs/wasm-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.9.0.tgz", - "integrity": "sha512-9+wkMowR2AmdSWQzsPEjFU7njh8HTO5MqO8vjwEHuM+AMHioNqSBONRdr0NQQ3dVQrzp0s8lTcYqzUdb7YgELA==", + "node_modules/@jest/reporters/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-wasm-bytecode": "1.9.0", - "@webassemblyjs/ieee754": "1.9.0", - "@webassemblyjs/leb128": "1.9.0", - "@webassemblyjs/utf8": "1.9.0" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" } }, - "@webassemblyjs/wast-parser": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-parser/-/wast-parser-1.9.0.tgz", - "integrity": "sha512-qsqSAP3QQ3LyZjNC/0jBJ/ToSxfYJ8kYyuiGvtn/8MK89VrNEfwj7BPQzJVHi0jGTRK2dGdJ5PRqhtjzoww+bw==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/floating-point-hex-parser": "1.9.0", - "@webassemblyjs/helper-api-error": "1.9.0", - "@webassemblyjs/helper-code-frame": "1.9.0", - "@webassemblyjs/helper-fsm": "1.9.0", - "@xtuc/long": "4.2.2" + "node_modules/@jest/reporters/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@webassemblyjs/wast-printer": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.9.0.tgz", - "integrity": "sha512-2J0nE95rHXHyQ24cWjMKJ1tqB/ds8z/cyeOZxJhcb+rW+SQASVjuznUSmdz5GpVJTzU8JkhYut0D3siFDD6wsA==", + "node_modules/@jest/reporters/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/wast-parser": "1.9.0", - "@xtuc/long": "4.2.2" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@webcomponents/template": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/@webcomponents/template/-/template-1.4.4.tgz", - "integrity": "sha512-QqCmmywIKJTilkl6UIPLxEBBuqhDaOBpvQyKOnUEwl9lJuVHBrVlhMIhhnp9VSZJ6xEUnp+PiX8DST1k0q/v4Q==" - }, - "@wojtekmaj/enzyme-adapter-react-17": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-react-17/-/enzyme-adapter-react-17-0.6.3.tgz", - "integrity": "sha512-Kp1ZJxtHkKEnUksaWrcMABNTOgL4wOt8VI6k2xOek2aH9PtZcWRXJNUEgnKrdJrqg5UqIjRslbVF9uUqwQJtFg==", - "dev": true, - "requires": { - "@wojtekmaj/enzyme-adapter-utils": "^0.1.1", - "enzyme-shallow-equal": "^1.0.0", - "has": "^1.0.0", - "object.assign": "^4.1.0", - "object.values": "^1.1.0", - "prop-types": "^15.7.0", - "react-is": "^17.0.2", - "react-test-renderer": "^17.0.0" - }, + "node_modules/@jest/schemas": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", + "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", + "dev": true, "dependencies": { - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - } + "@sinclair/typebox": "^0.27.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wojtekmaj/enzyme-adapter-utils": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/@wojtekmaj/enzyme-adapter-utils/-/enzyme-adapter-utils-0.1.1.tgz", - "integrity": "sha512-bNPWtN/d8huKOkC6j1E3EkSamnRrHHT7YuR6f9JppAQqtoAm3v4/vERe4J14jQKmHLCyEBHXrlgb7H6l817hVg==", + "node_modules/@jest/source-map": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/source-map/-/source-map-29.6.3.tgz", + "integrity": "sha512-MHjT95QuipcPrpLM+8JMSzFx6eHp5Bm+4XeFDJlwsvVBjmKNiIAvasGK2fxz2WbGRlnvqehFbh07MMa7n3YJnw==", "dev": true, - "requires": { - "function.prototype.name": "^1.1.0", - "has": "^1.0.0", - "object.assign": "^4.1.0", - "object.fromentries": "^2.0.0", - "prop-types": "^15.7.0" + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.18", + "callsites": "^3.0.0", + "graceful-fs": "^4.2.9" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wordpress/a11y": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/a11y/-/a11y-3.1.2.tgz", - "integrity": "sha512-VUrJN4UcDdYLYVOjArMbSsHNrIPmoAfrSsJi09xpRFBJXUq67sLLPh5+yy01uptbV1aOqcIbRagyj0Zaq8I70g==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/dom-ready": "^3.1.2", - "@wordpress/i18n": "^4.1.2" + "node_modules/@jest/test-result": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-result/-/test-result-29.7.0.tgz", + "integrity": "sha512-Fdx+tv6x1zlkJPcWXmMDAG2HBnaR9XPSd5aDWQVsfrZmLVT3lU1cwyxLgRmXR9yrq4NBoEm9BMsfgFzTQAbJYA==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "collect-v8-coverage": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wordpress/annotations": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/annotations/-/annotations-2.1.6.tgz", - "integrity": "sha512-z79UvFGm8GiwJZWEIgE9mzIEzoGVRPo31qwjnotFq3uHjAhKsRZk3ppmaLRNZUbJfobNOvDEyD8VT8V033Gp2A==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/data": "^5.1.6", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/rich-text": "^4.1.6", - "lodash": "^4.17.21", - "rememo": "^3.0.0", - "uuid": "^8.3.0" + "node_modules/@jest/test-sequencer": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/test-sequencer/-/test-sequencer-29.7.0.tgz", + "integrity": "sha512-GQwJ5WZVrKnOJuiYiAF52UNUJXgTZx1NHjFSEB0qEMmSZKAkdMoIzw/Cj6x6NF4AvV23AUqDpFzQkN/eYCYTxw==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wordpress/api-fetch": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-5.1.2.tgz", - "integrity": "sha512-qdYENoku3IHfq9tHQismlOnCXerps3Vf9oHjoicUT7ukUBLawEfJWM8LeMJmuFoMRh4mc5ceuhUmTnmEfbcwIQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/i18n": "^4.1.2", - "@wordpress/url": "^3.1.2" + "node_modules/@jest/transform": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.7.0.tgz", + "integrity": "sha512-ok/BTPFzFKVMwO5eOHRrvnBVHdRy9IrsrW1GpMaQ9MCnilNLXQKmAX8s1YXDFaai9xJpac2ySzV0YeRRECr2Vw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/types": "^29.6.3", + "@jridgewell/trace-mapping": "^0.3.18", + "babel-plugin-istanbul": "^6.1.1", + "chalk": "^4.0.0", + "convert-source-map": "^2.0.0", + "fast-json-stable-stringify": "^2.1.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "micromatch": "^4.0.4", + "pirates": "^4.0.4", + "slash": "^3.0.0", + "write-file-atomic": "^4.0.2" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wordpress/autop": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/autop/-/autop-3.1.2.tgz", - "integrity": "sha512-EbQ/lJwK20GjMXa0pszW8RVdIWghrQcjM1GE/IjPhSWuReD8h0R+3LpB+noZpuy4ccqiz3/Enksq9LBlnFUMvg==", - "requires": { - "@babel/runtime": "^7.13.10" + "node_modules/@jest/transform/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@wordpress/babel-plugin-import-jsx-pragma": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/babel-plugin-import-jsx-pragma/-/babel-plugin-import-jsx-pragma-3.1.0.tgz", - "integrity": "sha512-518mL3goaSeXtJCQcPK9OYHUUiA0sjXuoGWHBwRalkyTIQZZy5ZZzlwrlSc9ESZcOw9BZ+Uo8CJRjV2OWnx+Zw==", - "dev": true - }, - "@wordpress/babel-preset-default": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-6.2.1.tgz", - "integrity": "sha512-io2HbPtmLdfu3WdZg6X6T1oRVCrOTVrTouqSeUxyAZ954fNh8xyB7Q8rfHfxUZHPgGypnbzZOKBiCpUlnBRs7A==", - "dev": true, - "requires": { - "@babel/core": "^7.13.10", - "@babel/plugin-transform-react-jsx": "^7.12.7", - "@babel/plugin-transform-runtime": "^7.13.10", - "@babel/preset-env": "^7.13.10", - "@babel/preset-typescript": "^7.13.0", - "@babel/runtime": "^7.13.10", - "@wordpress/babel-plugin-import-jsx-pragma": "^3.0.5", - "@wordpress/browserslist-config": "^4.0.1", - "@wordpress/element": "^3.1.2", - "@wordpress/warning": "^2.1.2", - "browserslist": "^4.16.6", - "core-js": "^3.12.1" - } - }, - "@wordpress/base-styles": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-3.6.0.tgz", - "integrity": "sha512-6/vXAmc9FSX7Y17UjKgUJoVU++Pv1U1G8uMx7iClRUaLetc7/jj2DD9PTyX/cdJjHr32e3yXuLVT9wfEbo6SEg==", - "dev": true - }, - "@wordpress/blob": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/blob/-/blob-3.1.2.tgz", - "integrity": "sha512-xtuw2+fPok3Ep+fKJlNuatMzag19FUzLziDPa5XLtfncVo+WKg/Tnz+gj138EwYtrjVKnxGlGolps1zqFzH4Bg==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@wordpress/block-directory": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/@wordpress/block-directory/-/block-directory-2.1.21.tgz", - "integrity": "sha512-CCfQDyY7LpWkPxvmA7Dv9KAeSK1WiYIAZKt8FZNXJhnzZHuADsUcZy/7otopcMGHqCYycHcg+ygkpfzNyve5lA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/edit-post": "^4.1.21", - "@wordpress/editor": "^10.1.17", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/notices": "^3.1.6", - "@wordpress/plugins": "^3.1.6", - "@wordpress/url": "^3.1.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/block-editor": { - "version": "6.1.14", - "resolved": "https://registry.npmjs.org/@wordpress/block-editor/-/block-editor-6.1.14.tgz", - "integrity": "sha512-4qiAPqrBSOIO6fanm+jG2nTaNuPc1vfqq+KLHnWWfEZtAA06tuKeW/C8XbHT5NdCBzdSL9sytVw+94gOJVeTww==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/blob": "^3.1.2", - "@wordpress/block-serialization-default-parser": "^4.1.2", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keyboard-shortcuts": "^2.1.7", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/notices": "^3.1.6", - "@wordpress/rich-text": "^4.1.6", - "@wordpress/shortcode": "^3.1.2", - "@wordpress/token-list": "^2.1.1", - "@wordpress/url": "^3.1.2", - "@wordpress/wordcount": "^3.1.2", - "classnames": "^2.2.5", - "css-mediaquery": "^0.1.2", - "diff": "^4.0.2", - "dom-scroll-into-view": "^1.2.1", - "inherits": "^2.0.3", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "react-autosize-textarea": "^7.1.0", - "react-spring": "^8.0.19", - "redux-multi": "^0.1.12", - "rememo": "^3.0.0", - "tinycolor2": "^1.4.2", - "traverse": "^0.6.6" - } - }, - "@wordpress/block-library": { - "version": "3.2.19", - "resolved": "https://registry.npmjs.org/@wordpress/block-library/-/block-library-3.2.19.tgz", - "integrity": "sha512-F/Dbg9YYmoQQ9LdtanA1Nn6Bi6z7a5mC9gUankzbl6657Aqw7EcBwvbKfuG3n1TOjbbJZpA2wCCYTge6XNXLWA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/autop": "^3.1.2", - "@wordpress/blob": "^3.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/date": "^4.1.2", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/escape-html": "^2.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/notices": "^3.1.6", - "@wordpress/primitives": "^2.1.2", - "@wordpress/reusable-blocks": "^2.1.17", - "@wordpress/rich-text": "^4.1.6", - "@wordpress/server-side-render": "^2.1.12", - "@wordpress/url": "^3.1.2", - "@wordpress/viewport": "^3.1.6", - "classnames": "^2.2.5", - "fast-average-color": "4.3.0", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "micromodal": "^0.4.6", - "moment": "^2.22.1", - "react-easy-crop": "^3.0.0", - "tinycolor2": "^1.4.2" + "node_modules/@jest/transform/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "@wordpress/block-serialization-default-parser": { + "node_modules/@jest/transform/node_modules/chalk": { "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/block-serialization-default-parser/-/block-serialization-default-parser-4.1.2.tgz", - "integrity": "sha512-jJoXfn6hYYReEl211alNoTFA5Hdgu9LMTkTQJF2g8cGJS5UpDw1lcpJP0hmpfhwzTmUUemy5RXM7noOa2c/RUw==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@wordpress/blocks": { - "version": "9.1.8", - "resolved": "https://registry.npmjs.org/@wordpress/blocks/-/blocks-9.1.8.tgz", - "integrity": "sha512-RYemYN+q5/M0k5mESBkQbsB101p9hWSOTSlGLzEPBj7yXJp/OnyQVdc2hAr6CQgX16CxOyRRXx1CYQdiOtXGYg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/autop": "^3.1.2", - "@wordpress/blob": "^3.1.2", - "@wordpress/block-serialization-default-parser": "^4.1.2", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/shortcode": "^3.1.2", - "hpq": "^1.3.0", - "lodash": "^4.17.21", - "rememo": "^3.0.0", - "showdown": "^1.9.1", - "simple-html-tokenizer": "^0.5.7", - "tinycolor2": "^1.4.2", - "uuid": "^8.3.0" + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "@wordpress/browserslist-config": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-4.1.0.tgz", - "integrity": "sha512-RSJhgY2xmz6yAdDNhz/NvAO6JS+91vv9cVL7VDG2CftbyjTXBef05vWt3FzZhfeF0xUrYdpZL1PVpxmJiKvbEg==", + "node_modules/@jest/transform/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/@jest/transform/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "@wordpress/components": { - "version": "14.1.11", - "resolved": "https://registry.npmjs.org/@wordpress/components/-/components-14.1.11.tgz", - "integrity": "sha512-umsDXCtH0eqOaAVXq/N3f5QO4u9ovo2F+RMg7c566Q+StOgCVw0jRnKuyKQtBb1CEv4IIYIDWp/pL3cId8Vz2w==", - "requires": { - "@babel/runtime": "^7.13.10", - "@emotion/cache": "^10.0.27", - "@emotion/core": "^10.1.1", - "@emotion/css": "^10.0.22", - "@emotion/styled": "^10.0.23", - "@wordpress/a11y": "^3.1.2", - "@wordpress/compose": "^4.1.6", - "@wordpress/date": "^4.1.2", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/primitives": "^2.1.2", - "@wordpress/rich-text": "^4.1.6", - "@wordpress/warning": "^2.1.2", - "classnames": "^2.2.5", - "dom-scroll-into-view": "^1.2.1", - "downshift": "^6.0.15", - "emotion": "^10.0.23", - "gradient-parser": "^0.1.5", - "highlight-words-core": "^1.2.2", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "moment": "^2.22.1", - "re-resizable": "^6.4.0", - "react-dates": "^17.1.1", - "react-resize-aware": "^3.1.0", - "react-spring": "^8.0.20", - "react-use-gesture": "^9.0.0", - "reakit": "^1.3.5", - "rememo": "^3.0.0", - "tinycolor2": "^1.4.2", - "uuid": "^8.3.0" - } - }, - "@wordpress/compose": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/compose/-/compose-4.1.6.tgz", - "integrity": "sha512-rzSHBvMMwkR70+qOmSGnZpOEa2fnseGbylR3DEN3IAlscxdAn08Ejyz+bZSQ8GgQ6Uq2OkwvYjHjPUD2llpQ/g==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/priority-queue": "^2.1.2", - "clipboard": "^2.0.1", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "mousetrap": "^1.6.5", - "react-resize-aware": "^3.1.0", - "use-memo-one": "^1.1.1" - } - }, - "@wordpress/core-data": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/@wordpress/core-data/-/core-data-3.1.12.tgz", - "integrity": "sha512-GUvuWc5SQi0eBKi6gaWDz3Xg8eQyAM4YmYPpWNhWP8oNeAwsLL/RM2kXv6O4ssnGGlS/W3zJtLFTy7yDo0c74Q==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/blocks": "^9.1.8", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/url": "^3.1.2", - "equivalent-key-map": "^0.2.2", - "lodash": "^4.17.21", - "rememo": "^3.0.0", - "uuid": "^8.3.0" - } + "node_modules/@jest/transform/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true }, - "@wordpress/custom-templated-path-webpack-plugin": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@wordpress/custom-templated-path-webpack-plugin/-/custom-templated-path-webpack-plugin-2.0.5.tgz", - "integrity": "sha512-8tEcTEkr4tB55UFo0oalmpj54jf3sfTNJEqqnu0VpfB3Zk2lh3nRYmo+/vL1ks5uP47k3+iXYiPEFy9rJ4lGLw==", + "node_modules/@jest/transform/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "@wordpress/customize-widgets": { - "version": "1.0.20", - "resolved": "https://registry.npmjs.org/@wordpress/customize-widgets/-/customize-widgets-1.0.20.tgz", - "integrity": "sha512-2zHmZ3IcnkXMD+n1FPRjZTvfTkxt1onxe4oqfQjpobdYjPTSMl3HKaYuHB1lBaVN7qw4sOk/3WgB9cH1kqWpqg==", - "requires": { - "@babel/runtime": "^7.11.2", - "@wordpress/a11y": "^3.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/block-library": "^3.2.19", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keyboard-shortcuts": "^2.1.7", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/media-utils": "^2.1.2", - "@wordpress/widgets": "^1.1.19", - "classnames": "^2.2.6", - "lodash": "^4.17.21" - } - }, - "@wordpress/data": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/data/-/data-5.1.6.tgz", - "integrity": "sha512-KKcAcYh6XuRXkQvIQOv1/ci5Tj1kXEE2gwNzujN4SytJZmQTNP2EEE8u9bBnhEPoS7bORzlqeNQuMW0Rz74qgg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/compose": "^4.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/priority-queue": "^2.1.2", - "@wordpress/redux-routine": "^4.1.2", - "equivalent-key-map": "^0.2.2", - "is-promise": "^4.0.0", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "redux": "^4.1.0", - "turbo-combine-reducers": "^1.0.2", - "use-memo-one": "^1.1.1" + "node_modules/@jest/transform/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "@wordpress/data-controls": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/data-controls/-/data-controls-2.1.6.tgz", - "integrity": "sha512-AON1ZQLGhVQsJQIeW+4D1aB31TBJbDIQUMwnimiMkJ2n0M9V1Q0hvW4P7URC1k2aouNstnF2/TJ4xFPPRINf4Q==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/data": "^5.1.6", - "@wordpress/deprecated": "^3.1.2" + "node_modules/@jest/transform/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" } }, - "@wordpress/date": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/date/-/date-4.1.2.tgz", - "integrity": "sha512-t1cM1eLCQkLJeypcnFQIBJW0+aS8ZUvZLNSxlMH0jVybF9+bjVIbKY+aRkGwP4OMxxrdQkvF2qFj349bn3nz3Q==", - "requires": { - "@babel/runtime": "^7.13.10", - "moment": "^2.22.1", - "moment-timezone": "^0.5.31" + "node_modules/@jest/transform/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "@wordpress/dependency-extraction-webpack-plugin": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-3.1.4.tgz", - "integrity": "sha512-SoFdhgt75symEJz57QwzDrcZzuSZ9Fxxr1adplSHHYfvRXBm/vDM0x6jeb2pHtVWH0Ltax4Z/yelRgv982nNYA==", + "node_modules/@jest/transform/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "json2php": "^0.0.4", - "webpack-sources": "^2.2.0" - }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "webpack-sources": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz", - "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@wordpress/deprecated": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/deprecated/-/deprecated-3.1.2.tgz", - "integrity": "sha512-kURIhVWssN6lv4YZNMqeFU8ZHk1Dh3SNbHHDs/Ah4Qcql9uHJisF89lLIQBCs063wgkeXfFxdvM/BXD4kfK6fw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/hooks": "^3.1.1" + "node_modules/@jest/transform/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "@wordpress/dom": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/@wordpress/dom/-/dom-3.1.5.tgz", - "integrity": "sha512-EDX1BXtIEHw3LjAunASJO9lnbU2KU1gi4UBrjaa+Cem4u6npmfYHh6XkXO2ryA21yjx7rCWPbD0id6E7qlJ4Tw==", - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21" + "node_modules/@jest/types": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", + "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "@types/istanbul-lib-coverage": "^2.0.0", + "@types/istanbul-reports": "^3.0.0", + "@types/node": "*", + "@types/yargs": "^17.0.8", + "chalk": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "@wordpress/dom-ready": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/dom-ready/-/dom-ready-3.1.2.tgz", - "integrity": "sha512-g2zmgC/+6JYgWYzUMByRmjRAXs75q57wnNdYUTXmRFrtLs2+PNa1bUuYWZop15rTZbxuvP6ScVnPxZ5tyQLRUw==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@wordpress/e2e-test-utils": { - "version": "5.4.3", - "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils/-/e2e-test-utils-5.4.3.tgz", - "integrity": "sha512-DDLIO/QZipMLpqfiYB570nVtxOGlC9ByZkyd/+dqjwViBc7ST9pWMQzQPahlGjkArU2EMB96fvPumPnjrReZOA==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.2.2", - "@wordpress/keycodes": "^3.2.2", - "@wordpress/url": "^3.2.2", - "form-data": "^4.0.0", - "lodash": "^4.17.21", - "node-fetch": "^2.6.0" - }, - "dependencies": { - "@wordpress/api-fetch": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@wordpress/api-fetch/-/api-fetch-5.2.2.tgz", - "integrity": "sha512-WwJHOe6qiI4Oa1BSSo+Fpietdtm/0UgaN5A9k/TlEkARqIE+Fh56sfbC3JbjJDfQxz9TsAxMm+WWO5aNapantQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/i18n": "^4.2.2", - "@wordpress/url": "^3.2.2" - } - }, - "@wordpress/hooks": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.2.0.tgz", - "integrity": "sha512-nVR6V9kPxl8+aYQzQJdoDt+aKBKHHD0zplcYZbu2MHxjmHMvppAeL9mjzVhQZj/3n10NR2Ftk94mHQzHWfhCCg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@wordpress/i18n": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.2.2.tgz", - "integrity": "sha512-6PrfTDpeW5dfWyuqUx4Z5ApKFbh45CAbCs/G3PuZLlKJlXs/8p2Oq6Zxs0gLZk1QfHkw0t5qMx61lDlxWQhuPw==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/hooks": "^3.2.0", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" - } - }, - "@wordpress/keycodes": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.2.2.tgz", - "integrity": "sha512-z4B4vby+iGciJ9gvUBIozsseDkdQXDNuWm5szMnG5g1Nn7UGDWmfCNc9IHNs3alXySmAFev6d0T/o/zgm9BBvQ==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/i18n": "^4.2.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/url": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.2.2.tgz", - "integrity": "sha512-TYWE7V9F8nj0ZkCJy1eFD0crdDTS7iB3cVNW2yIDOn1RTWJJtzINXQFMASokVsjuh+NetAIOu8ru2mIfoRMG8Q==", - "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21", - "react-native-url-polyfill": "^1.1.2" - } - }, - "form-data": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", - "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - } + "node_modules/@jest/types/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "@wordpress/edit-post": { - "version": "4.1.21", - "resolved": "https://registry.npmjs.org/@wordpress/edit-post/-/edit-post-4.1.21.tgz", - "integrity": "sha512-tthVG2vA/cEkM5Ksb+AWlfYdRQRh3TE45C0NPMTjRTQaETJjZGZp1VCVl6f+EBMIiW90mzgOfP6t0bvDHBtcFg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/block-library": "^3.2.19", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/editor": "^10.1.17", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/interface": "^3.1.12", - "@wordpress/keyboard-shortcuts": "^2.1.7", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/media-utils": "^2.1.2", - "@wordpress/notices": "^3.1.6", - "@wordpress/plugins": "^3.1.6", - "@wordpress/primitives": "^2.1.2", - "@wordpress/url": "^3.1.2", - "@wordpress/viewport": "^3.1.6", - "@wordpress/warning": "^2.1.2", - "classnames": "^2.2.5", - "framer-motion": "^4.1.3", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "rememo": "^3.0.0", - "uuid": "8.3.0" - }, + "node_modules/@jest/types/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, "dependencies": { - "uuid": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.0.tgz", - "integrity": "sha512-fX6Z5o4m6XsXBdli9g7DtWgAx+osMsRRZFKma1mIUsLCz6vRvv+pz5VNbyu9UEDzpMWulZfvpgb/cmDXVulYFQ==" - } + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "@wordpress/edit-widgets": { - "version": "2.1.21", - "resolved": "https://registry.npmjs.org/@wordpress/edit-widgets/-/edit-widgets-2.1.21.tgz", - "integrity": "sha512-dkINHh3yPJnCtqXfcKX0+Wf85ztGJLEnt35pUdcif6fkxDvBciYX6maezo+m+r6TgI0J9wUO2t9+G6kX6bda0Q==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/block-library": "^3.2.19", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/interface": "^3.1.12", - "@wordpress/keyboard-shortcuts": "^2.1.7", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/media-utils": "^2.1.2", - "@wordpress/notices": "^3.1.6", - "@wordpress/plugins": "^3.1.6", - "@wordpress/reusable-blocks": "^2.1.17", - "@wordpress/server-side-render": "^2.1.12", - "@wordpress/url": "^3.1.2", - "@wordpress/widgets": "^1.1.19", - "classnames": "^2.2.5", - "lodash": "^4.17.21", - "rememo": "^3.0.0", - "uuid": "^8.3.0" - } - }, - "@wordpress/editor": { - "version": "10.1.17", - "resolved": "https://registry.npmjs.org/@wordpress/editor/-/editor-10.1.17.tgz", - "integrity": "sha512-ALvuojonwMEjWN1tbQR+mZF3ggR/fdkvOKe4esxRL3U10eTfcsSw6qA2QzB8zHrjb1cZd/90gZWixAdE398f+w==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/autop": "^3.1.2", - "@wordpress/blob": "^3.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/data-controls": "^2.1.6", - "@wordpress/date": "^4.1.2", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keyboard-shortcuts": "^2.1.7", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/media-utils": "^2.1.2", - "@wordpress/notices": "^3.1.6", - "@wordpress/reusable-blocks": "^2.1.17", - "@wordpress/rich-text": "^4.1.6", - "@wordpress/server-side-render": "^2.1.12", - "@wordpress/url": "^3.1.2", - "@wordpress/wordcount": "^3.1.2", - "classnames": "^2.2.5", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "react-autosize-textarea": "^7.1.0", - "rememo": "^3.0.0" + "node_modules/@jest/types/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "@wordpress/element": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-3.1.2.tgz", - "integrity": "sha512-6ZDhwXkkohcocZSXp+IE4Xn774+PNHJTNsEOLJRAeDo/clIvly5HoczX03z1GLgvEB3VYik8qHaqrouNIcjZvA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@types/react": "^16.9.0", - "@types/react-dom": "^16.9.0", - "@wordpress/escape-html": "^2.1.2", - "lodash": "^4.17.21", - "react": "^16.13.1", - "react-dom": "^16.13.1" + "node_modules/@jest/types/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/@jest/types/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "@wordpress/escape-html": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-2.1.2.tgz", - "integrity": "sha512-nEKvwzjMkuyV5MBnD5Um/McydurQ65mLjV1NG8bVlljZlP6/263qdZ5otH2uwr6cf2Lz1meupmi9sV3ef4TalQ==", - "requires": { - "@babel/runtime": "^7.13.10" - } - }, - "@wordpress/eslint-plugin": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-9.1.1.tgz", - "integrity": "sha512-GduOwoTTHh132RHTfLU82ujOmzuLpkA9izq+gzKtzhoDGzcpORXIht7tnbB8fzima4PYIiePz2LmuMCSuP18RA==", - "dev": true, - "requires": { - "@typescript-eslint/eslint-plugin": "^4.15.0", - "@typescript-eslint/parser": "^4.15.0", - "@wordpress/prettier-config": "^1.1.0", - "babel-eslint": "^10.1.0", - "cosmiconfig": "^7.0.0", - "eslint-config-prettier": "^7.1.0", - "eslint-plugin-import": "^2.23.4", - "eslint-plugin-jest": "^24.1.3", - "eslint-plugin-jsdoc": "^34.1.0", - "eslint-plugin-jsx-a11y": "^6.4.1", - "eslint-plugin-prettier": "^3.3.0", - "eslint-plugin-react": "^7.22.0", - "eslint-plugin-react-hooks": "^4.2.0", - "globals": "^12.0.0", - "prettier": "npm:wp-prettier@2.2.1-beta-1", - "requireindex": "^1.2.0" - }, + "node_modules/@jest/types/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, "dependencies": { - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "globals": { - "version": "12.4.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-12.4.0.tgz", - "integrity": "sha512-BWICuzzDvDoH54NHKCseDanAhE3CeDorgDL5MT6LMXXj2WCnd9UC2szdk4AWLfjdgNBCXLUanXYcpBBKOSWGwg==", - "dev": true, - "requires": { - "type-fest": "^0.8.1" - } - }, - "prettier": { - "version": "npm:wp-prettier@2.2.1-beta-1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.2.1-beta-1.tgz", - "integrity": "sha512-+JHkqs9LC/JPp51yy1hzs3lQ7qeuWCwOcSzpQNeeY/G7oSpnF61vxt7hRh87zNRTr6ob2ndy0W8rVzhgrcA+Gw==", - "dev": true - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "@wordpress/format-library": { - "version": "2.1.14", - "resolved": "https://registry.npmjs.org/@wordpress/format-library/-/format-library-2.1.14.tgz", - "integrity": "sha512-INmmTyJX0FDBkG8PTJqvIBi+yTfYHpdpkWqP/JswG7kslrG++elm4jyrA2EsV1anZyxDfsSrP3aSCy+arYkTgA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/html-entities": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/keycodes": "^3.1.2", - "@wordpress/rich-text": "^4.1.6", - "@wordpress/url": "^3.1.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/hooks": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/hooks/-/hooks-3.1.1.tgz", - "integrity": "sha512-9f6H9WBwu6x/MM4ZCVLGGBuMiBcyaLapmAku5IwcWaeB2PtPduwjmk2NfGx35TuhBQD554DUg8WtTjIS019UAg==", - "requires": { - "@babel/runtime": "^7.13.10" + "node_modules/@jridgewell/gen-mapping": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.1.1.tgz", + "integrity": "sha512-sQXCasFk+U8lWYEe66WxRDOE9PjVz4vSM51fTu3Hw+ClTpUSQb718772vH3pyS5pShp6lvQM7SxgIDXXXmOX7w==", + "dev": true, + "dependencies": { + "@jridgewell/set-array": "^1.0.0", + "@jridgewell/sourcemap-codec": "^1.4.10" + }, + "engines": { + "node": ">=6.0.0" } }, - "@wordpress/html-entities": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/html-entities/-/html-entities-3.1.2.tgz", - "integrity": "sha512-pPbEBxUTZm9YA/ynq50UEGmefaqFIW4g07KrxDQfa+xm0jb3kFZOtxFqdX7gXNofNCn/f5v4jlpFGmmYYpJMaw==", - "requires": { - "@babel/runtime": "^7.13.10" + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.0.tgz", + "integrity": "sha512-F2msla3tad+Mfht5cJq7LSXcdudKTWCVYUgw6pLFOOHSTtZlj6SWNYAp+AhuqLmWdBO2X5hPrLcu8cVP8fy28w==", + "dev": true, + "engines": { + "node": ">=6.0.0" } }, - "@wordpress/i18n": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/i18n/-/i18n-4.1.2.tgz", - "integrity": "sha512-Bp0BnGoN2XQyhFjACh74f3fDIQx/AnBIyZsDIhSlNYedFjuJ9b4M246/YTmCDlRHyCqLyG/OuS0hEZKksKAoRQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/hooks": "^3.1.1", - "gettext-parser": "^1.3.1", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "sprintf-js": "^1.1.1", - "tannin": "^1.2.0" + "node_modules/@jridgewell/set-array": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", + "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" } }, - "@wordpress/icons": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@wordpress/icons/-/icons-4.0.3.tgz", - "integrity": "sha512-I68iAysutXkYP6JrvnPlha/DKtNhtI1u88PuroZC/RgG+R0WwmxphB/bPoj92BCYDjgrMI8P625Ivttuh1spFA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/element": "^3.1.2", - "@wordpress/primitives": "^2.1.2" - } - }, - "@wordpress/interface": { - "version": "3.1.12", - "resolved": "https://registry.npmjs.org/@wordpress/interface/-/interface-3.1.12.tgz", - "integrity": "sha512-t7ksL3IIlWB47+r8UJ9CyAZZVlEVbA7lN9aqA5Ly6+QPIPwtvATDDM1agiUjGPwGLAsFSoZyda9MDjfVNDvXmw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/plugins": "^3.1.6", - "@wordpress/viewport": "^3.1.6", - "classnames": "^2.2.5", - "lodash": "^4.17.21" - } - }, - "@wordpress/is-shallow-equal": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/is-shallow-equal/-/is-shallow-equal-4.1.1.tgz", - "integrity": "sha512-Bc782s4Kte98RKLtuDXOaUBpyJWUgN4XZJevEoFasKQTpABZUDF+Y2C0/dhnlJeYF5TDEd8TQgFfpF5csxEUNw==", - "requires": { - "@babel/runtime": "^7.13.10" + "node_modules/@jridgewell/source-map": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.6.tgz", + "integrity": "sha512-1ZJTZebgqllO79ue2bm3rIGud/bOe0pP5BjSRCRxxYkEZS8STV7zN84UBbiYu7jy+eCKSnVIUgoWWE/tt+shMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" } }, - "@wordpress/jest-console": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-4.1.0.tgz", - "integrity": "sha512-MAbEfYUH+odlYYtPNKoKnWzSZKZjSc2r2kvFJ7FR920ZdteEgSAPIOvjyv4r4UbJy3ZuKemnXHuVtcTAKca5Tw==", + "node_modules/@jridgewell/source-map/node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", "dev": true, - "requires": { - "@babel/runtime": "^7.13.10", - "jest-matcher-utils": "^26.6.2", - "lodash": "^4.17.21" + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" } }, - "@wordpress/jest-preset-default": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-7.1.0.tgz", - "integrity": "sha512-N6OwVfvNodRTgIkmBor6YOGx3FbLdvPp9ZTGHJ1uw1u+HUuPwVWN9nhcGTnuP8Ht2RWyN5VpN2Peo5+dz5gp0w==", + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true, - "requires": { - "@wojtekmaj/enzyme-adapter-react-17": "^0.6.1", - "@wordpress/jest-console": "^4.1.0", - "babel-jest": "^26.6.3", - "enzyme": "^3.11.0", - "enzyme-to-json": "^3.4.4" - } + "license": "MIT" }, - "@wordpress/keyboard-shortcuts": { - "version": "2.1.7", - "resolved": "https://registry.npmjs.org/@wordpress/keyboard-shortcuts/-/keyboard-shortcuts-2.1.7.tgz", - "integrity": "sha512-V04mCe0Uwxhe2qIkfJ4h5//KsngZA25G/3kI0Ab6vL2N8OrQZQojZ4fnCK3x/P7mvdjXKQn78+vjnp3KiMvRqg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/keycodes": "^3.1.2", - "lodash": "^4.17.21", - "rememo": "^3.0.0" + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.25", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", + "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" } }, - "@wordpress/keycodes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/keycodes/-/keycodes-3.1.2.tgz", - "integrity": "sha512-8N0JiHquULMezdWEAu/MjaW5cQ4EN8z61HkA4/sIIB8JPd2TvNEkE1SeEotzpPICcBitEoQzclQ3uOUPld0WAg==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/i18n": "^4.1.2", - "lodash": "^4.17.21" + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" } }, - "@wordpress/library-export-default-webpack-plugin": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@wordpress/library-export-default-webpack-plugin/-/library-export-default-webpack-plugin-2.0.5.tgz", - "integrity": "sha512-qqyna9btIqoQ3XhdvG3+0rEibgpt2EKCwwyZ7o6IN1TeyiR9ymgHSVwE2yOK9wKcAVyBayFCpN1Sy4MEzpwhog==", + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", "dev": true, - "requires": { - "lodash": "^4.17.21", - "webpack-sources": "^2.2.0" + "license": "MIT" + }, + "node_modules/@leichtgewicht/ip-codec": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.4.tgz", + "integrity": "sha512-Hcv+nVC0kZnQ3tD9GVu5xSMR4VVYOteQIr/hwFPVEvPdlXqgGEuRjiheChHgdM+JyqdgNcmzZOX/tnl0JOiI7A==", + "dev": true + }, + "node_modules/@lodder/grunt-postcss": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@lodder/grunt-postcss/-/grunt-postcss-3.1.1.tgz", + "integrity": "sha512-dgkDAUgjtCCCk7jsIBkDMhcL78y2reQ9YxqBpVJGa/0tX1Eus7GRWEn0QWqfFiHqqc3yrMQN+GtH8PUIZOBmDQ==", + "dev": true, + "dependencies": { + "diff": "^5.0.0", + "maxmin": "^3.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": "^10 || ^12 || >=14" }, + "peerDependencies": { + "grunt": ">=1.0.4", + "postcss": "^8.0.0" + } + }, + "node_modules/@mrmlnc/readdir-enhanced": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/@mrmlnc/readdir-enhanced/-/readdir-enhanced-2.2.1.tgz", + "integrity": "sha512-bPHp6Ji8b41szTOcaP63VlnbbO5Ny6dwAATtY6JTjh5N2OLrb5Qk/Th5cRkRQhkWCt+EJsYrNB0MiL+Gpn6e3g==", + "dev": true, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "webpack-sources": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.0.tgz", - "integrity": "sha512-WyOdtwSvOML1kbgtXbTDnEW0jkJ7hZr/bDByIwszhWd/4XX1A3XMkrbFMsuH4+/MfLlZCUzlAdg4r7jaGKEIgQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } - } + "call-me-maybe": "^1.0.1", + "glob-to-regexp": "^0.3.0" + }, + "engines": { + "node": ">=4" } }, - "@wordpress/list-reusable-blocks": { - "version": "2.1.11", - "resolved": "https://registry.npmjs.org/@wordpress/list-reusable-blocks/-/list-reusable-blocks-2.1.11.tgz", - "integrity": "sha512-/y9YbYKL0E0jGs0QJHzdAK3OHrpWec9/dt2oy/RkB6+r+SjHUMBbCyxtL67SPhqZAliXDO10tJSih0OFK0gULw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "lodash": "^4.17.21" + "node_modules/@napi-rs/wasm-runtime": { + "version": "0.2.12", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-0.2.12.tgz", + "integrity": "sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.4.3", + "@emnapi/runtime": "^1.4.3", + "@tybys/wasm-util": "^0.10.0" } }, - "@wordpress/media-utils": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/media-utils/-/media-utils-2.1.2.tgz", - "integrity": "sha512-LhZ52JdLz4Lwr1H1YB90pAHTlAowv9R2HwQXjjmKmkSfGwCOhd6xigSRnDVKEjJxT4sUsVVCJdnb4Og/SVk0Nw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/blob": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/notices": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/notices/-/notices-3.1.6.tgz", - "integrity": "sha512-I+xDHCY7n5I/mBeZvlker7BoMtRFgaHBe/XySGJv2V2D3ZxgpAGXeVv13v+ohSvhaEjFhll+RgszpZOW/8SMLA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/a11y": "^3.1.2", - "@wordpress/data": "^5.1.6", - "lodash": "^4.17.21" + "node_modules/@nicolo-ribaudo/eslint-scope-5-internals": { + "version": "5.1.1-v1", + "resolved": "https://registry.npmjs.org/@nicolo-ribaudo/eslint-scope-5-internals/-/eslint-scope-5-internals-5.1.1-v1.tgz", + "integrity": "sha512-54/JRvkLIzzDWshCWfuhadfrfZVPiElY8Fcgmg1HroEly/EDSszzhBAsarCux+D/kOslTRquNzuyGSmUSTTHGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-scope": "5.1.1" } }, - "@wordpress/npm-package-json-lint-config": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-4.1.0.tgz", - "integrity": "sha512-FjXL5GbpmI/wXXcpCf2sKosVIVuWjUuHmDbwcMzd0SClcudo9QjDRdVe35We+js8eQLPgB9hsG4Cty6cAFFxsQ==", - "dev": true + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } }, - "@wordpress/nux": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/@wordpress/nux/-/nux-4.1.11.tgz", - "integrity": "sha512-GRuQW4guMp12NjdUQPpQHJ+YoLt9lzigWO5za5sPXtWzgpYjzzTGIvBrC8hU5s5dXAlvHzZ0yk6swCEBXGqVDw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "lodash": "^4.17.21", - "rememo": "^3.0.0" + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "dev": true, + "engines": { + "node": ">= 8" } }, - "@wordpress/plugins": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/plugins/-/plugins-3.1.6.tgz", - "integrity": "sha512-I2ZE6M/IzkLDDy0YOWLqMebutuubeYCk5Y3QkSRVmQYiuTP0o6q2vO8iJjfL87L8wxB3BgkZwNxusFB1not+lw==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/compose": "^4.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/hooks": "^3.1.1", - "@wordpress/icons": "^4.0.3", - "lodash": "^4.17.21", - "memize": "^1.1.0" + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dev": true, + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" } }, - "@wordpress/postcss-plugins-preset": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-3.2.0.tgz", - "integrity": "sha512-vYzlqr92pq9cIdN6eO5/h1hyDjEIUUvRlm3Tgd822dPPr6EpkM8uJ82quObE1pPt4JfmXYhTj+gMgOUzRNLHJg==", - "dev": true, - "requires": { - "@wordpress/base-styles": "^3.6.0", - "autoprefixer": "^10.2.5" - }, - "dependencies": { - "autoprefixer": { - "version": "10.3.3", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.3.3.tgz", - "integrity": "sha512-yRzjxfnggrP/+qVHlUuZz5FZzEbkT+Yt0/Df6ScEMnbbZBLzYB2W0KLxoQCW+THm1SpOsM1ZPcTHAwuvmibIsQ==", - "dev": true, - "requires": { - "browserslist": "^4.16.8", - "caniuse-lite": "^1.0.30001252", - "colorette": "^1.3.0", - "fraction.js": "^4.1.1", - "normalize-range": "^0.1.2", - "postcss-value-parser": "^4.1.0" - } - }, - "browserslist": { - "version": "4.16.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", - "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001251", - "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.811", - "escalade": "^3.1.1", - "node-releases": "^1.1.75" - } - }, - "caniuse-lite": { - "version": "1.0.30001252", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", - "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==", - "dev": true - }, - "colorette": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", - "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.826", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.826.tgz", - "integrity": "sha512-bpLc4QU4B8PYmdO4MSu2ZBTMD8lAaEXRS43C09lB31BvYwuk9UxgBRXbY5OJBw7VuMGcg2MZG5FyTaP9u4PQnw==", - "dev": true - }, - "node-releases": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", - "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", - "dev": true - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - } + "node_modules/@parcel/watcher": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher/-/watcher-2.5.0.tgz", + "integrity": "sha512-i0GV1yJnm2n3Yq1qw6QrUrd/LI9bE8WEBOTtOkpCXHHdyN3TAGgqAK/DAT05z4fq2x04cARXt2pDmjWjL92iTQ==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "detect-libc": "^1.0.3", + "is-glob": "^4.0.3", + "micromatch": "^4.0.5", + "node-addon-api": "^7.0.0" + }, + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "@parcel/watcher-android-arm64": "2.5.0", + "@parcel/watcher-darwin-arm64": "2.5.0", + "@parcel/watcher-darwin-x64": "2.5.0", + "@parcel/watcher-freebsd-x64": "2.5.0", + "@parcel/watcher-linux-arm-glibc": "2.5.0", + "@parcel/watcher-linux-arm-musl": "2.5.0", + "@parcel/watcher-linux-arm64-glibc": "2.5.0", + "@parcel/watcher-linux-arm64-musl": "2.5.0", + "@parcel/watcher-linux-x64-glibc": "2.5.0", + "@parcel/watcher-linux-x64-musl": "2.5.0", + "@parcel/watcher-win32-arm64": "2.5.0", + "@parcel/watcher-win32-ia32": "2.5.0", + "@parcel/watcher-win32-x64": "2.5.0" + } + }, + "node_modules/@parcel/watcher-android-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-android-arm64/-/watcher-android-arm64-2.5.0.tgz", + "integrity": "sha512-qlX4eS28bUcQCdribHkg/herLe+0A9RyYC+mm2PXpncit8z5b3nSqGVzMNR3CmtAOgRutiZ02eIJJgP/b1iEFQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/prettier-config": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-1.1.0.tgz", - "integrity": "sha512-cMYc/dtuiRo9VAb+m8S2Mvv/jELvoJAtcPsq6HT6XMppXC9slZ5z0q1A4PNf3ewMvvHtodjwkl2oHbO+vaAYzg==", - "dev": true + "node_modules/@parcel/watcher-darwin-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-arm64/-/watcher-darwin-arm64-2.5.0.tgz", + "integrity": "sha512-hyZ3TANnzGfLpRA2s/4U1kbw2ZI4qGxaRJbBH2DCSREFfubMswheh8TeiC1sGZ3z2jUf3s37P0BBlrD3sjVTUw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } }, - "@wordpress/primitives": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/primitives/-/primitives-2.1.2.tgz", - "integrity": "sha512-Eof+TK+zoKIr6w4lMlwxq+HvP+nKXBoidukjP3YhNUPOdB7H27+X6/V/IJd/Zza2xKN2ExkJybssBAGqi5NS6g==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/element": "^3.1.2", - "classnames": "^2.2.5" + "node_modules/@parcel/watcher-darwin-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-darwin-x64/-/watcher-darwin-x64-2.5.0.tgz", + "integrity": "sha512-9rhlwd78saKf18fT869/poydQK8YqlU26TMiNg7AIu7eBp9adqbJZqmdFOsbZ5cnLp5XvRo9wcFmNHgHdWaGYA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/priority-queue": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/priority-queue/-/priority-queue-2.1.2.tgz", - "integrity": "sha512-qXZzmfTRH6ssSZpCbJEHnj/w9W8bjpYm8V1tLKUqBXEYkHbII4O2FIL21LGI5aFhPPWsHG7tlYz3Z4lA/MHALQ==", - "requires": { - "@babel/runtime": "^7.13.10" + "node_modules/@parcel/watcher-freebsd-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-freebsd-x64/-/watcher-freebsd-x64-2.5.0.tgz", + "integrity": "sha512-syvfhZzyM8kErg3VF0xpV8dixJ+RzbUaaGaeb7uDuz0D3FK97/mZ5AJQ3XNnDsXX7KkFNtyQyFrXZzQIcN49Tw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/redux-routine": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/redux-routine/-/redux-routine-4.1.2.tgz", - "integrity": "sha512-1wOH4zdp58VNeMCoTNRHc1wHTGL3ZWqx8kCCqeA3NhULnRBx2RkI/v6nemKCho5QRbMdIWWUYXffFsLbDOUOSg==", - "requires": { - "@babel/runtime": "^7.13.10", - "is-promise": "^4.0.0", - "lodash": "^4.17.21", - "rungen": "^0.3.2" - } - }, - "@wordpress/reusable-blocks": { - "version": "2.1.17", - "resolved": "https://registry.npmjs.org/@wordpress/reusable-blocks/-/reusable-blocks-2.1.17.tgz", - "integrity": "sha512-54tZPQppe4kmDeivxsUk5ZZhTlSY8jum+5OEcYs0jJVctEXhRVh7rU3p2hqSWWriNghqVz4i3cuJ57tqt/tIsw==", - "requires": { - "@wordpress/block-editor": "^6.1.14", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/notices": "^3.1.6", - "@wordpress/url": "^3.1.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/rich-text": { - "version": "4.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/rich-text/-/rich-text-4.1.6.tgz", - "integrity": "sha512-wF60RXrGIQ0xgAvMSNwh1dP8lM4RRMWbRS5ZKOf1ld2s7oTsezEodBwCIHhFkHeraM/ORbeFoBPFsLbGf3oXwQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/dom": "^3.1.5", - "@wordpress/element": "^3.1.2", - "@wordpress/escape-html": "^2.1.2", - "@wordpress/is-shallow-equal": "^4.1.1", - "@wordpress/keycodes": "^3.1.2", - "classnames": "^2.2.5", - "lodash": "^4.17.21", - "memize": "^1.1.0", - "rememo": "^3.0.0" - } - }, - "@wordpress/scripts": { - "version": "16.1.5", - "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-16.1.5.tgz", - "integrity": "sha512-EF63cT5UGbWEMEJBBhxvcoNAuJIooKlqAAaMq5wN1urzsJRpQOTHTV658onoKFfgAxWB78CD7svuwIqNPGQdIw==", - "dev": true, - "requires": { - "@svgr/webpack": "^5.2.0", - "@wordpress/babel-preset-default": "^6.2.1", - "@wordpress/dependency-extraction-webpack-plugin": "^3.1.4", - "@wordpress/eslint-plugin": "^9.0.6", - "@wordpress/jest-preset-default": "^7.0.5", - "@wordpress/npm-package-json-lint-config": "^4.0.5", - "@wordpress/postcss-plugins-preset": "^3.1.4", - "@wordpress/prettier-config": "^1.0.5", - "@wordpress/stylelint-config": "^19.0.5", - "babel-jest": "^26.6.3", - "babel-loader": "^8.2.2", - "chalk": "^4.0.0", - "check-node-version": "^4.1.0", - "clean-webpack-plugin": "^3.0.0", - "cross-spawn": "^5.1.0", - "css-loader": "^5.1.3", - "cwd": "^0.10.0", - "dir-glob": "^3.0.1", - "eslint": "^7.17.0", - "eslint-plugin-markdown": "^1.0.2", - "expect-puppeteer": "^4.4.0", - "file-loader": "^6.2.0", - "filenamify": "^4.2.0", - "ignore-emit-webpack-plugin": "^2.0.6", - "jest": "^26.6.3", - "jest-circus": "^26.6.3", - "jest-dev-server": "^4.4.0", - "jest-environment-node": "^26.6.2", - "markdownlint": "^0.18.0", - "markdownlint-cli": "^0.21.0", - "merge-deep": "^3.0.3", - "mini-css-extract-plugin": "^1.3.9", - "minimist": "^1.2.0", - "npm-package-json-lint": "^5.0.0", - "postcss": "^8.2.15", - "postcss-loader": "^4.2.0", - "prettier": "npm:wp-prettier@2.2.1-beta-1", - "puppeteer-core": "^9.0.0", - "read-pkg-up": "^1.0.1", - "resolve-bin": "^0.4.0", - "sass": "^1.26.11", - "sass-loader": "^10.1.1", - "source-map-loader": "^0.2.4", - "stylelint": "^13.8.0", - "terser-webpack-plugin": "^3.0.3", - "thread-loader": "^3.0.1", - "url-loader": "^4.1.1", - "webpack": "^4.46.0", - "webpack-bundle-analyzer": "^4.2.0", - "webpack-cli": "^3.3.11", - "webpack-livereload-plugin": "^2.3.0", - "webpack-sources": "^2.2.0" - }, - "dependencies": { - "cacache": { - "version": "15.3.0", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-15.3.0.tgz", - "integrity": "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ==", - "dev": true, - "requires": { - "@npmcli/fs": "^1.0.0", - "@npmcli/move-file": "^1.0.1", - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "glob": "^7.1.4", - "infer-owner": "^1.0.4", - "lru-cache": "^6.0.0", - "minipass": "^3.1.1", - "minipass-collect": "^1.0.2", - "minipass-flush": "^1.0.5", - "minipass-pipeline": "^1.2.2", - "mkdirp": "^1.0.3", - "p-map": "^4.0.0", - "promise-inflight": "^1.0.1", - "rimraf": "^3.0.2", - "ssri": "^8.0.1", - "tar": "^6.0.2", - "unique-filename": "^1.1.1" - } - }, - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "colorette": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", - "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", - "dev": true - }, - "enhanced-resolve": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.5.0.tgz", - "integrity": "sha512-Nv9m36S/vxpsI+Hc4/ZGRs0n9mXqSWGGq49zxb/cJfPAQMbUtttJAlNPS4AQzaBdw/pKskw5bMbekT/Y7W/Wlg==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } - } - }, - "filenamify": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", - "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", - "dev": true, - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.1", - "trim-repeated": "^1.0.0" - } - }, - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "p-limit": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", - "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", - "dev": true, - "requires": { - "yocto-queue": "^0.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - }, - "dependencies": { - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - } - } - }, - "p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "dev": true, - "requires": { - "aggregate-error": "^3.0.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "postcss": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", - "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", - "dev": true, - "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" - } - }, - "prettier": { - "version": "npm:wp-prettier@2.2.1-beta-1", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.2.1-beta-1.tgz", - "integrity": "sha512-+JHkqs9LC/JPp51yy1hzs3lQ7qeuWCwOcSzpQNeeY/G7oSpnF61vxt7hRh87zNRTr6ob2ndy0W8rVzhgrcA+Gw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "source-map-loader": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-0.2.4.tgz", - "integrity": "sha512-OU6UJUty+i2JDpTItnizPrlpOIBLmQbWMuBg9q5bVtnHACqw1tn9nNwqJLbv0/00JjnJb/Ee5g5WS5vrRv7zIQ==", - "dev": true, - "requires": { - "async": "^2.5.0", - "loader-utils": "^1.1.0" - } - }, - "ssri": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", - "integrity": "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ==", - "dev": true, - "requires": { - "minipass": "^3.1.1" - } - }, - "terser-webpack-plugin": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-3.1.0.tgz", - "integrity": "sha512-cjdZte66fYkZ65rQ2oJfrdCAkkhJA7YLYk5eGOcGCSGlq0ieZupRdjedSQXYknMPo2IveQL+tPdrxUkERENCFA==", - "dev": true, - "requires": { - "cacache": "^15.0.5", - "find-cache-dir": "^3.3.1", - "jest-worker": "^26.2.1", - "p-limit": "^3.0.2", - "schema-utils": "^2.6.6", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.8.0", - "webpack-sources": "^1.4.3" - }, - "dependencies": { - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "webpack": { - "version": "4.46.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.46.0.tgz", - "integrity": "sha512-6jJuJjg8znb/xRItk7bkT0+Q7AHCYjjFnvKIWQPkNIOyRqoCGvkOs0ipeQzrqz4l5FtN5ZI/ukEHroeX/o1/5Q==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.5.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.7.4", - "webpack-sources": "^1.4.1" - }, - "dependencies": { - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - } - }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "dev": true - }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" - } - }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "dev": true, - "requires": { - "locate-path": "^3.0.0" - } - }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "dev": true, - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" - } - }, - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", - "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - } - }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "dev": true, - "requires": { - "p-try": "^2.0.0" - } - }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "dev": true, - "requires": { - "p-limit": "^2.0.0" - } - }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=", - "dev": true - }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", - "dev": true, - "requires": { - "find-up": "^3.0.0" - } - }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", - "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", - "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" - } - }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" - } - }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", - "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - } - } - } - }, - "webpack-sources": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-2.3.1.tgz", - "integrity": "sha512-y9EI9AO42JjEcrTJFOYmVywVZdKVUfOvDUPsJea5GIr1JOEGFVqwlY2K098fFoIjOkDzHn2AjRvM8dsBZu+gCA==", - "dev": true, - "requires": { - "source-list-map": "^2.0.1", - "source-map": "^0.6.1" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } + "node_modules/@parcel/watcher-linux-arm-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-glibc/-/watcher-linux-arm-glibc-2.5.0.tgz", + "integrity": "sha512-0VQY1K35DQET3dVYWpOaPFecqOT9dbuCfzjxoQyif1Wc574t3kOSkKevULddcR9znz1TcklCE7Ht6NIxjvTqLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/server-side-render": { - "version": "2.1.12", - "resolved": "https://registry.npmjs.org/@wordpress/server-side-render/-/server-side-render-2.1.12.tgz", - "integrity": "sha512-NrxFv/u6xzs97Pgyb0BbhB2f7pAII2trD8nyDY+/JfjNcTguulthRh9kmmPWp/vo6kgyEO1xyo6yHUnHLnh/vQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "@wordpress/deprecated": "^3.1.2", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/url": "^3.1.2", - "lodash": "^4.17.21" - } - }, - "@wordpress/shortcode": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/shortcode/-/shortcode-3.1.2.tgz", - "integrity": "sha512-pZQf4nu4iQdyQIz3OnphvPcKTL9jlEIR1a58icis+PDnqbr0gv4PGwS3Hbamlm4p7+bkxm5Pskfj3n3ykqwGLQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21", - "memize": "^1.1.0" + "node_modules/@parcel/watcher-linux-arm-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm-musl/-/watcher-linux-arm-musl-2.5.0.tgz", + "integrity": "sha512-6uHywSIzz8+vi2lAzFeltnYbdHsDm3iIB57d4g5oaB9vKwjb6N6dRIgZMujw4nm5r6v9/BQH0noq6DzHrqr2pA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/stylelint-config": { - "version": "19.1.0", - "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-19.1.0.tgz", - "integrity": "sha512-K/wB9rhB+pH5WvDh3fV3DN5C3Bud+jPGXmnPY8fOXKMYI3twCFozK/j6sVuaJHqGp/0kKEF0hkkGh+HhD30KGQ==", + "node_modules/@parcel/watcher-linux-arm64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-glibc/-/watcher-linux-arm64-glibc-2.5.0.tgz", + "integrity": "sha512-BfNjXwZKxBy4WibDb/LDCriWSKLz+jJRL3cM/DllnHH5QUyoiUNEp3GmL80ZqxeumoADfCCP19+qiYiC8gUBjA==", + "cpu": [ + "arm64" + ], "dev": true, - "requires": { - "stylelint-config-recommended": "^3.0.0", - "stylelint-config-recommended-scss": "^4.2.0", - "stylelint-scss": "^3.17.2" + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/token-list": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/@wordpress/token-list/-/token-list-2.1.1.tgz", - "integrity": "sha512-haBjgsroaRjNBZ/wHd6nZamYL3Yfrt0s13Py+aR1ZKtYv+/Rmwu9VB45iB6Xb/G+v3xexopEM8uA8Zks5PNxbQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21" + "node_modules/@parcel/watcher-linux-arm64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-arm64-musl/-/watcher-linux-arm64-musl-2.5.0.tgz", + "integrity": "sha512-S1qARKOphxfiBEkwLUbHjCY9BWPdWnW9j7f7Hb2jPplu8UZ3nes7zpPOW9bkLbHRvWM0WDTsjdOTUgW0xLBN1Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/url": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/url/-/url-3.1.2.tgz", - "integrity": "sha512-hT214NQG2p+FiL4jdKPJItHMEeA70uqvhKlnPOa7qqf9u+6QMnhVplxFxWSIu5cB7glmx5JQG4EMR2Ohz3jgTg==", - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21", - "react-native-url-polyfill": "^1.1.2" + "node_modules/@parcel/watcher-linux-x64-glibc": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-glibc/-/watcher-linux-x64-glibc-2.5.0.tgz", + "integrity": "sha512-d9AOkusyXARkFD66S6zlGXyzx5RvY+chTP9Jp0ypSTC9d4lzyRs9ovGf/80VCxjKddcUvnsGwCHWuF2EoPgWjw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/viewport": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/@wordpress/viewport/-/viewport-3.1.6.tgz", - "integrity": "sha512-SwIL26Nd691mO8R21GUrODjVqJx1Y1DVNHbX9VusJxvf7citc94dZnXXHiQ8zc7vlg5ryMBF49/EFn8rZ8Zn8w==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/compose": "^4.1.6", - "@wordpress/data": "^5.1.6", - "lodash": "^4.17.21" + "node_modules/@parcel/watcher-linux-x64-musl": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-linux-x64-musl/-/watcher-linux-x64-musl-2.5.0.tgz", + "integrity": "sha512-iqOC+GoTDoFyk/VYSFHwjHhYrk8bljW6zOhPuhi5t9ulqiYq1togGJB5e3PwYVFFfeVgc6pbz3JdQyDoBszVaA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@wordpress/warning": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-2.1.2.tgz", - "integrity": "sha512-MqMo5AYffG/Gi3h1uRFJGBxp4TGmxw+7A4W81oe7VC9linJYhbNyeyvaMlL02m06KG2szWwl4fn0bdhyre433w==" - }, - "@wordpress/widgets": { - "version": "1.1.19", - "resolved": "https://registry.npmjs.org/@wordpress/widgets/-/widgets-1.1.19.tgz", - "integrity": "sha512-6ojIQVbeeS3fhXs+nNdmKIc6BtYnPoPq6qXLcTFGV9yXPJs8zX5RRdI3ziEFeV/IILCoIk4fmbw87ncIQwahQA==", - "requires": { - "@babel/runtime": "^7.13.10", - "@wordpress/api-fetch": "^5.1.2", - "@wordpress/block-editor": "^6.1.14", - "@wordpress/blocks": "^9.1.8", - "@wordpress/components": "^14.1.11", - "@wordpress/compose": "^4.1.6", - "@wordpress/core-data": "^3.1.12", - "@wordpress/data": "^5.1.6", - "@wordpress/element": "^3.1.2", - "@wordpress/i18n": "^4.1.2", - "@wordpress/icons": "^4.0.3", - "@wordpress/notices": "^3.1.6", - "@wordpress/url": "^3.1.2", - "classnames": "^2.2.5", - "lodash": "^4.17.21" - } - }, - "@wordpress/wordcount": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@wordpress/wordcount/-/wordcount-3.1.2.tgz", - "integrity": "sha512-jFumH3IJzbEtGupxfre7asRMF6zHmNPeOlNgJw7luaeE1YFwgXkCtflDFf2Y6fC09xJmwuDPwxLnoII8RLhNtQ==", - "requires": { - "@babel/runtime": "^7.13.10", - "lodash": "^4.17.21" + "node_modules/@parcel/watcher-win32-arm64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-arm64/-/watcher-win32-arm64-2.5.0.tgz", + "integrity": "sha512-twtft1d+JRNkM5YbmexfcH/N4znDtjgysFaV9zvZmmJezQsKpkfLYJ+JFV3uygugK6AtIM2oADPkB2AdhBrNig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "dev": true - }, - "@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "dev": true - }, - "Base64": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/Base64/-/Base64-1.1.0.tgz", - "integrity": "sha512-qeacf8dvGpf+XAT27ESHMh7z84uRzj/ua2pQdJg483m3bEXv/kVFtDnMgvf70BQGqzbZhR9t6BmASzKvqfJf3Q==" - }, - "abab": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.5.tgz", - "integrity": "sha512-9IK9EadsbHo6jLWIpxpR6pL0sazTXV6+SQv25ZB+F7Bj9mJNaOc4nCRabwd5M/JwmUa8idz6Eci6eKfJryPs6Q==", - "dev": true - }, - "abbrev": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", - "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", - "dev": true - }, - "abort-controller": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", - "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", - "requires": { - "event-target-shim": "^5.0.0" + "node_modules/@parcel/watcher-win32-ia32": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-ia32/-/watcher-win32-ia32-2.5.0.tgz", + "integrity": "sha512-+rgpsNRKwo8A53elqbbHXdOMtY/tAtTzManTWShB5Kk54N8Q9mzNWV7tV+IbGueCbcj826MfWGU3mprWtuf1TA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "accepts": { - "version": "1.3.7", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", - "integrity": "sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==", - "requires": { - "mime-types": "~2.1.24", - "negotiator": "0.6.2" + "node_modules/@parcel/watcher-win32-x64": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@parcel/watcher-win32-x64/-/watcher-win32-x64-2.5.0.tgz", + "integrity": "sha512-lPrxve92zEHdgeff3aiu4gDOIt4u7sJYha6wbdEZDCDUhtjTsOMiaJzG5lMY4GkWH8p0fMmO2Ppq5G5XXG+DQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" } }, - "acorn": { - "version": "6.4.2", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-6.4.2.tgz", - "integrity": "sha512-XtGIhXwF8YM8bJhGxG5kXgjkEuNGLTkoYqVE+KMR+aspr4KGYmKYg7yUe3KghyQ9yheNwLnjmzh/7+gfDBmHCQ==", - "dev": true - }, - "acorn-globals": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/acorn-globals/-/acorn-globals-6.0.0.tgz", - "integrity": "sha512-ZQl7LOWaF5ePqqcX4hLuv/bLXYQNfNWw2c0/yX/TsPRKamzHcTGQnlCjHT3TsmkOUVEPS3crCxiPfdzE/Trlhg==", + "node_modules/@parcel/watcher/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "requires": { - "acorn": "^7.1.1", - "acorn-walk": "^7.1.1" - }, + "license": "MIT", + "optional": true, "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - } + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "dev": true - }, - "acorn-walk": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-7.2.0.tgz", - "integrity": "sha512-OPdCF6GsMIP+Az+aWfAAOEt2/+iVDKE7oy6lJ098aoe59oAmK76qV6Gw60SbZ8jHuG2wH058GF4pLFbYamYrVA==", - "dev": true - }, - "agent-base": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-5.1.1.tgz", - "integrity": "sha512-TMeqbNl2fMW0nMjTEPOwe3J/PRFP4vqeoNuQMG0HlMrtm5QxKqdvAkZ1pRBQ/ulIyDD5Yq0nJ7YbdD8ey0TO3g==", - "dev": true - }, - "aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", + "node_modules/@parcel/watcher/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "requires": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, + "license": "MIT", + "optional": true, "dependencies": { - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - } + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "airbnb-prop-types": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/airbnb-prop-types/-/airbnb-prop-types-2.16.0.tgz", - "integrity": "sha512-7WHOFolP/6cS96PhKNrslCLMYAI8yB1Pp6u6XmxozQOiZbsI5ycglZr5cHhBFfuRcQQjzCMith5ZPZdYiJCxUg==", - "requires": { - "array.prototype.find": "^2.1.1", - "function.prototype.name": "^1.1.2", - "is-regex": "^1.1.0", - "object-is": "^1.1.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.2", - "prop-types": "^15.7.2", - "prop-types-exact": "^1.2.0", - "react-is": "^16.13.1" + "node_modules/@parcel/watcher/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.12.0" } }, - "ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "node_modules/@parcel/watcher/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" + "license": "MIT", + "optional": true, + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "ajv-errors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", - "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", - "dev": true - }, - "ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "dev": true - }, - "align-text": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/align-text/-/align-text-0.1.4.tgz", - "integrity": "sha1-DNkKVhCT810KmSVsIrcGlDP60Rc=", - "requires": { - "kind-of": "^3.0.2", - "longest": "^1.0.1", - "repeat-string": "^1.5.2" + "node_modules/@parcel/watcher/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "alphanum-sort": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/alphanum-sort/-/alphanum-sort-1.0.2.tgz", - "integrity": "sha1-l6ERlkmyEa0zaR2fn0hqjsn74KM=", - "dev": true + "node_modules/@paulirish/trace_engine": { + "version": "0.0.39", + "resolved": "https://registry.npmjs.org/@paulirish/trace_engine/-/trace_engine-0.0.39.tgz", + "integrity": "sha512-2Y/ejHX5DDi5bjfWY/0c/BLVSfQ61Jw1Hy60Hnh0hfEO632D3FVctkzT4Q/lVAdvIPR0bUaok9JDTr1pu/OziA==", + "dev": true, + "dependencies": { + "third-party-web": "latest" + } }, - "ansi-colors": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", - "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", - "dev": true + "node_modules/@pkgr/core": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@pkgr/core/-/core-0.2.9.tgz", + "integrity": "sha512-QNqXyfVS2wm9hweSYD2O7F0G06uurj9kZ96TRQE5Y9hU7+tgdZwIkbAKc5Ocy1HxEY2kuDQa6cQ1WRs/O5LFKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/pkgr" + } }, - "ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", + "node_modules/@playwright/test": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.58.2.tgz", + "integrity": "sha512-akea+6bHYBBfA9uQqSYmlJXn61cTa+jbO87xVLCWbTqbWadRVmhxlXATaOjOgcBaWU4ePo0wB41KMFv3o35IXA==", "dev": true, - "requires": { - "type-fest": "^0.21.3" + "license": "Apache-2.0", + "dependencies": { + "playwright": "1.58.2" }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.6.2.tgz", + "integrity": "sha512-IhIAD5n4XvGHuL9nAgWfsBR0TdxtjrUWETYKCBHxauYXEv+b+ctEbs9neEgPC7Ecgzv4bpZTBwesAoGDeFymzA==", + "dev": true, + "license": "MIT", "dependencies": { + "anser": "^2.1.1", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "@types/webpack": "5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <6.0.0", + "webpack": "^5.0.0", + "webpack-dev-server": "^4.8.0 || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, "type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "dev": true + "optional": true + }, + "webpack-dev-server": { + "optional": true + }, + "webpack-hot-middleware": { + "optional": true + }, + "webpack-plugin-serve": { + "optional": true } } }, - "ansi-html": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.7.tgz", - "integrity": "sha1-gTWEAhliqenm/QOflA0S9WynhZ4=", - "dev": true + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "ansi-regex": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-4.1.0.tgz", - "integrity": "sha512-1apePfXM1UOSqw0o9IiFAovVz9M5S1Dg+4TrDwfMewQ6p/rmMueb7tWZjQ1rx4Loy1ArBggoqGpfqqdI4rondg==" + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } }, - "ansi-styles": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", - "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", - "requires": { - "color-convert": "^1.9.0" + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "anymatch": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.1.tgz", - "integrity": "sha512-mM8522psRCqzV+6LhomX5wgp25YVibjh8Wj23I5RPkPppSVSjyKD2A2mBJmWGa+KN7f2D6LNh9jkBCeyLktzjg==", - "requires": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" + "node_modules/@pmmmwh/react-refresh-webpack-plugin/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "aproba": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", - "integrity": "sha512-Y9J6ZjXtoYh8RnXVCMOU/ttDmk1aBjunq9vO0ta5x85WDQiQfUF9sIPBITdbiiIVcBo03Hi3jMxigBtsddlXRw==", + "node_modules/@polka/url": { + "version": "1.0.0-next.24", + "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.24.tgz", + "integrity": "sha512-2LuNTFBIO0m7kKIQvvPHN6UE63VjpmL9rnEEaOOaiSPbZK+zUOYIzBAWcED+3XYzhYsd/0mD57VdxAEqqV52CQ==", "dev": true }, - "arch": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", - "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", - "dev": true, - "optional": true - }, - "archive-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", - "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", + "node_modules/@puppeteer/browsers": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.3.0.tgz", + "integrity": "sha512-ioXoq9gPxkss4MYhD+SFaU9p1IHFUX0ILAWFPyjGaBdjLsYAlZw6j1iLA0N/m12uVHLFDfSYNF7EQccjinIMDA==", "dev": true, - "optional": true, - "requires": { - "file-type": "^4.2.0" - }, + "license": "Apache-2.0", "dependencies": { - "file-type": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", - "integrity": "sha1-G2AOX8ofvcboDApwxxyNul95BsU=", - "dev": true, - "optional": true - } + "debug": "^4.3.5", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.4.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" } }, - "argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "node_modules/@rtsao/scc": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", + "integrity": "sha512-zt6OdqaDoOnJ1ZYsCYGt9YmWzDXl4vQdKTyJev62gFhRGKdx7mcT54V9KIjg+d2wi9EXsPvAPKe7i7WjfVWB8g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@sentry-internal/tracing": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry-internal/tracing/-/tracing-7.120.3.tgz", + "integrity": "sha512-Ausx+Jw1pAMbIBHStoQ6ZqDZR60PsCByvHdw/jdH9AqPrNE9xlBSf9EwcycvmrzwyKspSLaB52grlje2cRIUMg==", "dev": true, - "requires": { - "sprintf-js": "~1.0.2" - }, "dependencies": { - "sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", - "dev": true - } + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" } }, - "aria-query": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-4.2.2.tgz", - "integrity": "sha512-o/HelwhuKpTj/frsOsbNLNgnNGVIFsVP/SW2BSF14gVl7kAfMOJ6/8wUAUvG1R1NHKrfG+2sHZTu0yauT1qBrA==", + "node_modules/@sentry/core": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-7.120.3.tgz", + "integrity": "sha512-vyy11fCGpkGK3qI5DSXOjgIboBZTriw0YDx/0KyX5CjIjDDNgp5AGgpgFkfZyiYiaU2Ww3iFuKo4wHmBusz1uA==", "dev": true, - "requires": { - "@babel/runtime": "^7.10.2", - "@babel/runtime-corejs3": "^7.10.2" + "dependencies": { + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" } }, - "arr-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", - "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", - "dev": true - }, - "arr-flatten": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", - "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", - "dev": true - }, - "arr-union": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", - "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", - "dev": true - }, - "array-each": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", - "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", - "dev": true - }, - "array-find-index": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", - "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", - "dev": true, - "optional": true - }, - "array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=", - "dev": true - }, - "array-includes": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.3.tgz", - "integrity": "sha512-gcem1KlBU7c9rB+Rq8/3PPKsK2kjqeEBa3bD5kkQo4nYlOHQCJqIJFqBXDEfwaRuYTT4E+FxA9xez7Gf/e3Q7A==", + "node_modules/@sentry/integrations": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/integrations/-/integrations-7.120.3.tgz", + "integrity": "sha512-6i/lYp0BubHPDTg91/uxHvNui427df9r17SsIEXa2eKDwQ9gW2qRx5IWgvnxs2GV/GfSbwcx4swUB3RfEWrXrQ==", "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "get-intrinsic": "^1.1.1", - "is-string": "^1.0.5" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - } - } - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "dependencies": { + "@sentry/core": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3", + "localforage": "^1.8.1" + }, + "engines": { + "node": ">=8" } }, - "array-slice": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", - "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", - "dev": true - }, - "array-union": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", - "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", + "node_modules/@sentry/node": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/node/-/node-7.120.3.tgz", + "integrity": "sha512-t+QtekZedEfiZjbkRAk1QWJPnJlFBH/ti96tQhEq7wmlk3VszDXraZvLWZA0P2vXyglKzbWRGkT31aD3/kX+5Q==", "dev": true, - "requires": { - "array-uniq": "^1.0.1" + "dependencies": { + "@sentry-internal/tracing": "7.120.3", + "@sentry/core": "7.120.3", + "@sentry/integrations": "7.120.3", + "@sentry/types": "7.120.3", + "@sentry/utils": "7.120.3" + }, + "engines": { + "node": ">=8" } }, - "array-uniq": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", - "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", - "dev": true - }, - "array-unique": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", - "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", - "dev": true - }, - "array.prototype.filter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/array.prototype.filter/-/array.prototype.filter-1.0.0.tgz", - "integrity": "sha512-TfO1gz+tLm+Bswq0FBOXPqAchtCr2Rn48T8dLJoRFl8NoEosjZmzptmuo1X8aZBzZcqsR1W8U761tjACJtngTQ==", + "node_modules/@sentry/types": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-7.120.3.tgz", + "integrity": "sha512-C4z+3kGWNFJ303FC+FxAd4KkHvxpNFYAFN8iMIgBwJdpIl25KZ8Q/VdGn0MLLUEHNLvjob0+wvwlcRBBNLXOow==", "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0", - "es-array-method-boxes-properly": "^1.0.0", - "is-string": "^1.0.5" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "engines": { + "node": ">=8" } }, - "array.prototype.find": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.1.1.tgz", - "integrity": "sha512-mi+MYNJYLTx2eNYy+Yh6raoQacCsNeeMUaspFPh9Y141lFSsWxxB8V9mM2ye+eqiRs917J6/pJ4M9ZPzenWckA==", - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.4" - } - }, - "array.prototype.flat": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.2.4.tgz", - "integrity": "sha512-4470Xi3GAPAjZqFcljX2xzckv1qeKPizoNkiS0+O4IoPR2ZNpcjE0pkhdihlDouK+x6QOast26B4Q/O9DJnwSg==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" - }, + "node_modules/@sentry/utils": { + "version": "7.120.3", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-7.120.3.tgz", + "integrity": "sha512-UDAOQJtJDxZHQ5Nm1olycBIsz2wdGX8SdzyGVHmD8EOQYAeDZQyIlQYohDe9nazdIOQLZCIc3fU0G9gqVLkaGQ==", + "dev": true, "dependencies": { - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - } + "@sentry/types": "7.120.3" + }, + "engines": { + "node": ">=8" } }, - "array.prototype.flatmap": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.2.4.tgz", - "integrity": "sha512-r9Z0zYoxqHz60vvQbWEdXIEtCwHF0yxaWfno9qzXeNHvfyl3BZqygmGzb84dsubyaXLH4husF+NFgMSdpZhk2Q==", + "node_modules/@sinclair/typebox": { + "version": "0.27.10", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.10.tgz", + "integrity": "sha512-MTBk/3jGLNB2tVxv6uLlFh1iu64iYOQ2PbdOSK3NW8JZsmlaOh2q6sdtKowBhfw8QFLmYNzTW4/oK4uATIi6ZA==", "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "function-bind": "^1.1.1" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } - } - } - } - }, - "arrify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", - "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", - "dev": true + "license": "MIT" }, - "asn1": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", - "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", + "node_modules/@sindresorhus/is": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-0.7.0.tgz", + "integrity": "sha512-ONhaKPIufzzrlNbqtWFFd+jlnemX6lJAgq9ZeiZtS7I1PIf/la7CW4m83rTXRnVnsMbW2k56pGYu7AUFJD9Pow==", "dev": true, - "requires": { - "safer-buffer": "~2.1.0" + "optional": true, + "engines": { + "node": ">=4" } }, - "asn1.js": { - "version": "5.4.1", - "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz", - "integrity": "sha512-+I//4cYPccV8LdmBLiX8CYvf9Sp3vQsrqu2QNXRcrbiWvcx/UdlFiqUJJzxRQxgsZmvhXhn4cSKeSmoFjVdupA==", + "node_modules/@sinonjs/commons": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", + "integrity": "sha512-K3mCHKQ9sVh8o1C9cxkwxaOmXoAMlDxC1mYyHrjqOWEcBjYr76t96zL2zlj5dUGZ3HSw240X1qgH3Mjf1yJWpQ==", "dev": true, - "requires": { - "bn.js": "^4.0.0", - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0", - "safer-buffer": "^2.1.0" - }, + "license": "BSD-3-Clause", "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } + "type-detect": "4.0.8" } }, - "assert": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.0.tgz", - "integrity": "sha512-EDsgawzwoun2CZkCgtxJbv392v4nbk9XDD06zI+kQYoBM/3RBWLlEyJARDOmhAAosBjWACEkKL6S+lIZtcAubA==", + "node_modules/@sinonjs/fake-timers": { + "version": "10.3.0", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-10.3.0.tgz", + "integrity": "sha512-V4BG07kuYSUkTCSBHG8G8TNhM+F19jXFWnQtzj+we8DrkpSBCee9Z3Ms8yiGer/dlmhe35/Xdgyo3/0rQKg7YA==", "dev": true, - "requires": { - "object-assign": "^4.1.1", - "util": "0.10.3" - }, "dependencies": { - "inherits": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz", - "integrity": "sha1-sX0I0ya0Qj5Wjv9xn5GwscvfafE=", - "dev": true - }, - "util": { - "version": "0.10.3", - "resolved": "https://registry.npmjs.org/util/-/util-0.10.3.tgz", - "integrity": "sha1-evsa/lCAUkZInj23/g7TeTNqwPk=", - "dev": true, - "requires": { - "inherits": "2.0.1" - } - } + "@sinonjs/commons": "^3.0.0" } }, - "assert-plus": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", - "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", - "dev": true - }, - "assign-symbols": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", - "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", - "dev": true - }, - "ast-types-flow": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.7.tgz", - "integrity": "sha1-9wtzXGvKGlycItmCw+Oef+ujva0=", - "dev": true + "node_modules/@sinonjs/samsam": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@sinonjs/samsam/-/samsam-8.0.0.tgz", + "integrity": "sha512-Bp8KUVlLp8ibJZrnvq2foVhP0IVX2CIprMJPK0vqGqgrDa0OHVKeZyBykqskkrdxV6yKBPmGasO8LVjAKR3Gew==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^2.0.0", + "lodash.get": "^4.4.2", + "type-detect": "^4.0.8" + } }, - "astral-regex": { + "node_modules/@sinonjs/samsam/node_modules/@sinonjs/commons": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", - "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", - "dev": true - }, - "async": { - "version": "2.6.3", - "resolved": "https://registry.npmjs.org/async/-/async-2.6.3.tgz", - "integrity": "sha512-zflvls11DCy+dQWzTW2dzuilv8Z5X/pjfmZOWba6TNIVDm+2UDaJmXSOXlasHKfNBs8oo3M0aT50fDEWfKZjXg==", + "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-2.0.0.tgz", + "integrity": "sha512-uLa0j859mMrg2slwQYdO/AkrOfmH+X6LTVmNTS9CqexuE2IvVORIkSpJLqePAbEnKJ77aMmCwr1NUZ57120Xcg==", "dev": true, - "requires": { - "lodash": "^4.17.14" + "dependencies": { + "type-detect": "4.0.8" } }, - "async-each": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/async-each/-/async-each-1.0.3.tgz", - "integrity": "sha512-z/WhQ5FPySLdvREByI2vZiTWwCnF0moMJ1hK9YQwDTHKh6I7/uSckMetoRGb5UBZPC1z0jlw+n/XCgjeH7y1AQ==", - "dev": true - }, - "asynckit": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", - "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", - "dev": true - }, - "atob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", - "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", + "node_modules/@sinonjs/text-encoding": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@sinonjs/text-encoding/-/text-encoding-0.7.2.tgz", + "integrity": "sha512-sXXKG+uL9IrKqViTtao2Ws6dy0znu9sOaP1di/jKGW1M6VssO8vlpXCQcpZ+jisQ1tTFAC5Jo/EOzFbggBagFQ==", "dev": true }, - "audio-context-polyfill": { + "node_modules/@standard-schema/spec": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/audio-context-polyfill/-/audio-context-polyfill-1.0.0.tgz", - "integrity": "sha1-S3KPrwoZVVGU1PvQVYL4M/3NE3s=" - }, - "autoprefixer": { - "version": "9.8.6", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-9.8.6.tgz", - "integrity": "sha512-XrvP4VVHdRBCdX1S3WXVD8+RyG9qeb1D5Sn1DeLiG2xfSpzellk5k54xbUERJ3M5DggQxes39UGOTP8CFrEGbg==", - "dev": true, - "requires": { - "browserslist": "^4.12.0", - "caniuse-lite": "^1.0.30001109", - "colorette": "^1.2.1", - "normalize-range": "^0.1.2", - "num2fraction": "^1.2.2", - "postcss": "^7.0.32", - "postcss-value-parser": "^4.1.0" - }, + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", + "integrity": "sha512-m2bOd0f2RT9k8QJx1JN85cZYyH1RqFBdlwtkSlf4tBDYLCiiZnv1fIIwacK6cqwXavOydf0NPToMQgpKq+dVlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@stylistic/stylelint-plugin": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@stylistic/stylelint-plugin/-/stylelint-plugin-3.1.3.tgz", + "integrity": "sha512-85fsmzgsIVmyG3/GFrjuYj6Cz8rAM7IZiPiXCMiSMfoDOC1lOrzrXPDk24WqviAghnPqGpx8b0caK2PuewWGFg==", + "dev": true, + "license": "MIT", "dependencies": { - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - } + "@csstools/css-parser-algorithms": "^3.0.1", + "@csstools/css-tokenizer": "^3.0.1", + "@csstools/media-query-list-parser": "^3.0.1", + "is-plain-object": "^5.0.0", + "postcss": "^8.4.41", + "postcss-selector-parser": "^6.1.2", + "postcss-value-parser": "^4.2.0", + "style-search": "^0.1.0" + }, + "engines": { + "node": "^18.12 || >=20.9" + }, + "peerDependencies": { + "stylelint": "^16.8.0" } }, - "autosize": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/autosize/-/autosize-4.0.4.tgz", - "integrity": "sha512-5yxLQ22O0fCRGoxGfeLSNt3J8LB1v+umtpMnPW6XjkTWXKoN0AmXAIhelJcDtFT/Y/wYWmfE+oqU10Q0b8FhaQ==" - }, - "aws-sign2": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", - "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", - "dev": true - }, - "aws4": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", - "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", - "dev": true - }, - "axe-core": { - "version": "4.3.3", - "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.3.3.tgz", - "integrity": "sha512-/lqqLAmuIPi79WYfRpy2i8z+x+vxU3zX2uAm0gs1q52qTuKwolOj1P8XbufpXcsydrpKx2yGn2wzAnxCMV86QA==", - "dev": true - }, - "axios": { - "version": "0.21.1", - "resolved": "https://registry.npmjs.org/axios/-/axios-0.21.1.tgz", - "integrity": "sha512-dKQiRHxGD9PPRIUNIWvZhPTPpl1rf/OxTYKsqKUDjBwYylTvV7SjSHJb9ratfyzM6wCdLCOYLzs73qpg5c4iGA==", + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", "dev": true, - "requires": { - "follow-redirects": "^1.10.0" + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "axobject-query": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-2.2.0.tgz", - "integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA==", - "dev": true - }, - "babel-eslint": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/babel-eslint/-/babel-eslint-10.1.0.tgz", - "integrity": "sha512-ifWaTHQ0ce+448CYop8AdrQiBsGrnC+bMgfyKFdi6EsPLTAWG+QfyDeM6OH+FmWnKvEq5NnBMLvlBUPKQZoDSg==", + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@babel/parser": "^7.7.0", - "@babel/traverse": "^7.7.0", - "@babel/types": "^7.7.0", - "eslint-visitor-keys": "^1.0.0", - "resolve": "^1.12.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-26.6.3.tgz", - "integrity": "sha512-pl4Q+GAVOHwvjrck6jKjvmGhnO3jHX/xuB9d27f+EJZ/6k+6nMuPjorrYp7s++bKKdANwzElBWnLWaObvTnaZA==", + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", "dev": true, - "requires": { - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__core": "^7.1.7", - "babel-plugin-istanbul": "^6.0.0", - "babel-preset-jest": "^26.6.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "slash": "^3.0.0" + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-loader": { - "version": "8.2.2", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-8.2.2.tgz", - "integrity": "sha512-JvTd0/D889PQBtUXJ2PXaKU/pjZDMtHA9V2ecm+eNRmmBCMR09a+fmpGTNwnJtFmFl5Ei7Vy47LjBb+L0wQ99g==", - "dev": true, - "requires": { - "find-cache-dir": "^3.3.1", - "loader-utils": "^1.4.0", - "make-dir": "^3.1.0", - "schema-utils": "^2.6.5" - }, - "dependencies": { - "find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - }, - "schema-utils": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-2.7.1.tgz", - "integrity": "sha512-SHiNtMOUGWBQJwzISiVYKu82GiV4QYGePp3odlY1tuKO7gPtphAT5R/py0fA6xtbgLL/RvtJZnU9b8s0F1q0Xg==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.5", - "ajv": "^6.12.4", - "ajv-keywords": "^3.5.2" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "dev": true, - "requires": { - "object.assign": "^4.1.0" - } - }, - "babel-plugin-emotion": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-emotion/-/babel-plugin-emotion-10.2.2.tgz", - "integrity": "sha512-SMSkGoqTbTyUTDeuVuPIWifPdUGkTk1Kf9BWRiXIOIcuyMfsdp2EjeiiFvOzX8NOBvEh/ypKYvUh2rkgAJMCLA==", - "requires": { - "@babel/helper-module-imports": "^7.0.0", - "@emotion/hash": "0.8.0", - "@emotion/memoize": "0.7.4", - "@emotion/serialize": "^0.11.16", - "babel-plugin-macros": "^2.0.0", - "babel-plugin-syntax-jsx": "^6.18.0", - "convert-source-map": "^1.5.0", - "escape-string-regexp": "^1.0.5", - "find-root": "^1.1.0", - "source-map": "^0.5.7" + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-istanbul": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.0.0.tgz", - "integrity": "sha512-AF55rZXpe7trmEylbaE1Gv54wn6rwU03aptvRoVIGP8YykoSxqdVLV1TfwflBCE/QtHmqtP8SWlTENqbK8GCSQ==", + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", "dev": true, - "requires": { - "@babel/helper-plugin-utils": "^7.0.0", - "@istanbuljs/load-nyc-config": "^1.0.0", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-instrument": "^4.0.0", - "test-exclude": "^6.0.0" + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-jest-hoist": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-26.6.2.tgz", - "integrity": "sha512-PO9t0697lNTmcEHH69mdtYiOIkkOlj9fySqfO3K1eCcdISevLAE0xY59VLLUj0SoiPiTX/JU2CYFpILydUa5Lw==", + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", "dev": true, - "requires": { - "@babel/template": "^7.3.3", - "@babel/types": "^7.3.3", - "@types/babel__core": "^7.0.0", - "@types/babel__traverse": "^7.0.6" + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-macros": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/babel-plugin-macros/-/babel-plugin-macros-2.8.0.tgz", - "integrity": "sha512-SEP5kJpfGYqYKpBrj5XU3ahw5p5GOHJ0U5ssOSQ/WBVdwkD2Dzlce95exQTs3jOVWPPKLBN2rlEWkCK7dSmLvg==", - "requires": { - "@babel/runtime": "^7.7.2", - "cosmiconfig": "^6.0.0", - "resolve": "^1.12.0" + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-polyfill-corejs2": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.2.2.tgz", - "integrity": "sha512-kISrENsJ0z5dNPq5eRvcctITNHYXWOA4DUZRFYCz3jYCcvTb/A546LIddmoGNMVYg2U38OyFeNosQwI9ENTqIQ==", + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", "dev": true, - "requires": { - "@babel/compat-data": "^7.13.11", - "@babel/helper-define-polyfill-provider": "^0.2.2", - "semver": "^6.1.1" - }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "babel-plugin-polyfill-corejs3": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.2.4.tgz", - "integrity": "sha512-z3HnJE5TY/j4EFEa/qpQMSbcUJZ5JQi+3UFjXzn6pQCmIKc5Ug5j98SuYyH+m4xQnvKlMDIW4plLfgyVnd0IcQ==", + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.2", - "core-js-compat": "^3.14.0" + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "babel-plugin-polyfill-regenerator": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.2.2.tgz", - "integrity": "sha512-Goy5ghsc21HgPDFtzRkSirpZVW35meGoTmTOb2bxqdl60ghub4xOidgNTHaZfQ2FaxQsKmwvXtOAkcIS4SMBWg==", + "node_modules/@svgr/core/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/@svgr/core/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", "dev": true, - "requires": { - "@babel/helper-define-polyfill-provider": "^0.2.2" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "babel-plugin-syntax-jsx": { - "version": "6.18.0", - "resolved": "https://registry.npmjs.org/babel-plugin-syntax-jsx/-/babel-plugin-syntax-jsx-6.18.0.tgz", - "integrity": "sha1-CvMqmm4Tyno/1QaeYtew9Y0NiUY=" - }, - "babel-preset-current-node-syntax": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", - "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", + "node_modules/@svgr/core/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, - "requires": { - "@babel/plugin-syntax-async-generators": "^7.8.4", - "@babel/plugin-syntax-bigint": "^7.8.3", - "@babel/plugin-syntax-class-properties": "^7.8.3", - "@babel/plugin-syntax-import-meta": "^7.8.3", - "@babel/plugin-syntax-json-strings": "^7.8.3", - "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", - "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", - "@babel/plugin-syntax-numeric-separator": "^7.8.3", - "@babel/plugin-syntax-object-rest-spread": "^7.8.3", - "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", - "@babel/plugin-syntax-optional-chaining": "^7.8.3", - "@babel/plugin-syntax-top-level-await": "^7.8.3" + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "babel-preset-jest": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-26.6.2.tgz", - "integrity": "sha512-YvdtlVm9t3k777c5NPQIv6cxFFFapys25HiUmuSgHwIZhfifweR5c5Sf5nwE3MAbfu327CYSvps8Yx6ANLyleQ==", + "node_modules/@svgr/core/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, - "requires": { - "babel-plugin-jest-hoist": "^26.6.2", - "babel-preset-current-node-syntax": "^1.0.0" + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "backbone": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.4.0.tgz", - "integrity": "sha512-RLmDrRXkVdouTg38jcgHhyQ/2zjg7a8E6sz2zxfz21Hh17xDJYUHBZimVIt5fUyS8vbfpeSmTL3gUjTEvUV3qQ==", - "requires": { - "underscore": ">=1.8.3" + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "bail": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/bail/-/bail-1.0.5.tgz", - "integrity": "sha512-xFbRxM1tahm08yHBP16MMjVUAvDaBMD38zsM9EMAUN61omwLmKlOpB/Zku5QkjZ8TZ4vn53pj+t518cH0S03RQ==", - "dev": true - }, - "balanced-match": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", - "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=" - }, - "base": { - "version": "0.11.2", - "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", - "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", + "node_modules/@svgr/hast-util-to-babel-ast/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "requires": { - "cache-base": "^1.0.1", - "class-utils": "^0.3.5", - "component-emitter": "^1.2.1", - "define-property": "^1.0.0", - "isobject": "^3.0.1", - "mixin-deep": "^1.2.0", - "pascalcase": "^0.1.1" + "engines": { + "node": ">=0.12" }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "base64-arraybuffer": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/base64-arraybuffer/-/base64-arraybuffer-0.1.4.tgz", - "integrity": "sha1-mBjHngWbE1X5fgQooBfIOOkLqBI=" - }, - "base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==" - }, - "base64id": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", - "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==" - }, - "batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha1-3DQxT05nkxgJP8dgJyUl+UvyXBY=", - "dev": true + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" + } }, - "bcrypt-pbkdf": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", - "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", "dev": true, - "requires": { - "tweetnacl": "^0.14.3" + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", + "node_modules/@svgr/plugin-svgo/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "bin-build": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", - "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", + "node_modules/@svgr/plugin-svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, - "optional": true, - "requires": { - "decompress": "^4.0.0", - "download": "^6.2.2", - "execa": "^0.7.0", - "p-map-series": "^1.0.0", - "tempfile": "^2.0.0" - }, - "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "optional": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - } + "engines": { + "node": ">= 10" } }, - "bin-check": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", - "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", + "node_modules/@svgr/plugin-svgo/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", "dev": true, - "optional": true, - "requires": { - "execa": "^0.7.0", - "executable": "^4.1.0" - }, "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "optional": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { "optional": true } } }, - "bin-version": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", - "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", + "node_modules/@svgr/plugin-svgo/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, - "optional": true, - "requires": { - "execa": "^1.0.0", - "find-versions": "^3.0.0" + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "bin-version-check": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", - "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", + "node_modules/@svgr/plugin-svgo/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, - "optional": true, - "requires": { - "bin-version": "^3.0.0", - "semver": "^5.6.0", - "semver-truncate": "^1.1.2" - }, "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "optional": true - } + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "bin-wrapper": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", - "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "node_modules/@svgr/plugin-svgo/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "optional": true, - "requires": { - "bin-check": "^4.1.0", - "bin-version-check": "^4.0.0", - "download": "^7.1.0", - "import-lazy": "^3.1.0", - "os-filter-obj": "^2.0.0", - "pify": "^4.0.1" + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" }, - "dependencies": { - "download": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", - "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", - "dev": true, - "optional": true, - "requires": { - "archive-type": "^4.0.0", - "caw": "^2.0.1", - "content-disposition": "^0.5.2", - "decompress": "^4.2.0", - "ext-name": "^5.0.0", - "file-type": "^8.1.0", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^8.3.1", - "make-dir": "^1.2.0", - "p-event": "^2.1.0", - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } - } - }, - "file-type": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", - "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - }, - "got": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", - "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", - "dev": true, - "optional": true, - "requires": { - "@sindresorhus/is": "^0.7.0", - "cacheable-request": "^2.1.1", - "decompress-response": "^3.3.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "into-stream": "^3.1.0", - "is-retry-allowed": "^1.1.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "mimic-response": "^1.0.0", - "p-cancelable": "^0.4.0", - "p-timeout": "^2.0.1", - "pify": "^3.0.0", - "safe-buffer": "^5.1.1", - "timed-out": "^4.0.1", - "url-parse-lax": "^3.0.0", - "url-to-options": "^1.0.1" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } - } - }, - "import-lazy": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", - "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", - "dev": true, - "optional": true - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "optional": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } - } - }, - "p-cancelable": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", - "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", - "dev": true, - "optional": true - }, - "p-event": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", - "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", - "dev": true, - "optional": true, - "requires": { - "p-timeout": "^2.0.1" - } - }, - "p-timeout": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", - "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", - "dev": true, - "optional": true, - "requires": { - "p-finally": "^1.0.0" - } - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true, - "optional": true - }, - "url-parse-lax": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", - "integrity": "sha1-FrXK/Afb42dsGxmZF3gj1lA6yww=", - "dev": true, - "optional": true, - "requires": { - "prepend-http": "^2.0.0" - } - } + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "binary-extensions": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", - "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==" - }, - "bindings": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/bindings/-/bindings-1.5.0.tgz", - "integrity": "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==", + "node_modules/@svgr/plugin-svgo/node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dev": true, - "optional": true, - "requires": { - "file-uri-to-path": "1.0.0" + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "bl": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-4.0.3.tgz", - "integrity": "sha512-fs4G6/Hu4/EE+F75J8DuN/0IpQqNjAdC7aEQv7Qt8MHGUH7Ckv2MwTEEeN9QehD0pfIDkMI1bkHYkKy7xHyKIg==", - "dev": true, - "requires": { - "buffer": "^5.5.0", - "inherits": "^2.0.4", - "readable-stream": "^3.4.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "node_modules/@svgr/plugin-svgo/node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "bluebird": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", - "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", - "dev": true - }, - "bn.js": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.1.3.tgz", - "integrity": "sha512-GkTiFpjFtUzU9CbMeJ5iazkCzGL3jrhzerzZIuqLABjbwRaFt33I9tUdSNryIptM+RxDet6OKm2WnLXzW51KsQ==", + "node_modules/@svgr/plugin-svgo/node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true }, - "body": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", - "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", + "node_modules/@svgr/plugin-svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "requires": { - "continuable-cache": "^0.3.1", - "error": "^7.0.0", - "raw-body": "~1.1.0", - "safe-json-parse": "~1.0.1" - }, "dependencies": { - "bytes": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", - "integrity": "sha1-NWnt6Lo0MV+rmcPpLLBMciDeH6g=", - "dev": true - }, - "raw-body": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", - "integrity": "sha1-HQJ8K/oRasxmI7yo8AAWVyqH1CU=", - "dev": true, - "requires": { - "bytes": "1", - "string_decoder": "0.10" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - } - } - }, - "body-parser": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.19.0.tgz", - "integrity": "sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==", - "requires": { - "bytes": "3.1.0", - "content-type": "~1.0.4", - "debug": "2.6.9", - "depd": "~1.1.2", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "on-finished": "~2.3.0", - "qs": "6.7.0", - "raw-body": "2.4.0", - "type-is": "~1.6.17" + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==" - } + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "body-scroll-lock": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/body-scroll-lock/-/body-scroll-lock-3.1.5.tgz", - "integrity": "sha512-Yi1Xaml0EvNA0OYWxXiYNqY24AfWkbA6w5vxE7GWxtKfzIbZM+Qw+aSmkgsbWzbHiy/RCSkUZBplVxTA+E4jJg==" - }, - "bonjour": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/bonjour/-/bonjour-3.5.0.tgz", - "integrity": "sha1-jokKGD2O6aI5OzhExpGkK897yfU=", - "dev": true, - "requires": { - "array-flatten": "^2.1.0", - "deep-equal": "^1.0.1", - "dns-equal": "^1.0.0", - "dns-txt": "^2.0.2", - "multicast-dns": "^6.0.1", - "multicast-dns-service-types": "^1.1.0" - }, - "dependencies": { - "array-flatten": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-2.1.2.tgz", - "integrity": "sha512-hNfzcOV8W4NdualtqBFPyVO+54DSJuZGY9qT4pRroB6S9e3iiido2ISIC5h9R2sPJ8H3FHCIiEnsv1lPXO3KtQ==", - "dev": true + "node_modules/@svgr/plugin-svgo/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" } - } - }, - "boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", - "dev": true + ] }, - "brace-expansion": { - "version": "1.1.11", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", - "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "requires": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" + "node_modules/@svgr/plugin-svgo/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "braces": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", - "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "node_modules/@svgr/plugin-svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, - "requires": { - "arr-flatten": "^1.1.0", - "array-unique": "^0.3.2", - "extend-shallow": "^2.0.1", - "fill-range": "^4.0.0", - "isobject": "^3.0.1", - "repeat-element": "^1.1.2", - "snapdragon": "^0.8.1", - "snapdragon-node": "^2.0.1", - "split-string": "^3.0.2", - "to-regex": "^3.0.1" + "engines": { + "node": ">=0.12" }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "brcast": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brcast/-/brcast-2.0.2.tgz", - "integrity": "sha512-Tfn5JSE7hrUlFcOoaLzVvkbgIemIorMIyoMr3TgvszWW7jFt2C9PdeMLtysYD9RU0MmU17b69+XJG1eRY2OBRg==" - }, - "brorand": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", - "integrity": "sha1-EsJe/kCkXjwyPrhnWgoM5XsiNx8=", - "dev": true + "node_modules/@svgr/plugin-svgo/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } }, - "browser-process-hrtime": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/browser-process-hrtime/-/browser-process-hrtime-1.0.0.tgz", - "integrity": "sha512-9o5UecI3GhkpM6DrXr69PblIuWxPKk9Y0jHBRhdocZ2y7YECBFCsHm79Pr3OyR2AvjhDkabFJaDJMYRazHgsow==", + "node_modules/@svgr/plugin-svgo/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, - "browserify-aes": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", - "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", - "dev": true, - "requires": { - "buffer-xor": "^1.0.3", - "cipher-base": "^1.0.0", - "create-hash": "^1.1.0", - "evp_bytestokey": "^1.0.3", - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "node_modules/@svgr/plugin-svgo/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "browserify-cipher": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", - "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "node_modules/@svgr/plugin-svgo/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "requires": { - "browserify-aes": "^1.0.4", - "browserify-des": "^1.0.0", - "evp_bytestokey": "^1.0.0" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" } }, - "browserify-des": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", - "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "node_modules/@svgr/plugin-svgo/node_modules/svgo": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "des.js": "^1.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" + "license": "MIT", + "dependencies": { + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "browserify-rsa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.0.tgz", - "integrity": "sha512-AdEER0Hkspgno2aR97SAf6vi0y0k8NuOpGnVH3O99rcA5Q6sh8QxcngtHuJ6uXwnfAXNM4Gn1Gb7/MV1+Ymbog==", + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", "dev": true, - "requires": { - "bn.js": "^5.0.0", - "randombytes": "^2.0.1" + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "browserify-sign": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.1.tgz", - "integrity": "sha512-/vrA5fguVAKKAVTNJjgSm1tRQDHUU6DbwO9IROu/0WAzC8PKhucDSh18J0RMvVeHAn5puMd+QHC2erPRNf8lmg==", - "dev": true, - "requires": { - "bn.js": "^5.1.1", - "browserify-rsa": "^4.0.1", - "create-hash": "^1.2.0", - "create-hmac": "^1.1.7", - "elliptic": "^6.5.3", - "inherits": "^2.0.4", - "parse-asn1": "^5.1.5", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } - } + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true }, - "browserify-zlib": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", - "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", "dev": true, - "requires": { - "pako": "~1.0.5" + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "browserslist": { - "version": "4.16.6", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", - "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", + "node_modules/@types/babel__core": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.2.tgz", + "integrity": "sha512-pNpr1T1xLUc2l3xJKuPtsEky3ybxN3m4fJkknfIpTCTfIZCDW57oAg+EfCgIIp2rvCe0Wn++/FfodDS4YXxBwA==", "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001219", - "colorette": "^1.2.2", - "electron-to-chromium": "^1.3.723", - "escalade": "^3.1.1", - "node-releases": "^1.1.71" - }, "dependencies": { - "colorette": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", - "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", - "dev": true - } + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" } }, - "bser": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", - "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", + "node_modules/@types/babel__generator": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.6.5.tgz", + "integrity": "sha512-h9yIuWbJKdOPLJTbmSpPzkF67e659PbQDba7ifWm5BJ8xTv+sDmS7rFmywkWOvXedGTivCdeGSIIX8WLcRTz8w==", "dev": true, - "requires": { - "node-int64": "^0.4.0" + "dependencies": { + "@babel/types": "^7.0.0" } }, - "buffer": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", - "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", - "requires": { - "base64-js": "^1.3.1", - "ieee754": "^1.1.13" + "node_modules/@types/babel__template": { + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.2.tgz", + "integrity": "sha512-/AVzPICMhMOMYoSx9MoKpGDKdBRsIXMNByh1PXSZoa+v6ZoLa8xxtsT/uLQ/NJm0XVAWl/BvId4MlDeXJaeIZQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" } }, - "buffer-alloc": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", - "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", + "node_modules/@types/babel__traverse": { + "version": "7.20.2", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.20.2.tgz", + "integrity": "sha512-ojlGK1Hsfce93J0+kn3H5R73elidKUaZonirN33GSmgTUMpzI/MIFfSpF3haANe3G1bEBS9/9/QEqwTzwqFsKw==", "dev": true, - "optional": true, - "requires": { - "buffer-alloc-unsafe": "^1.1.0", - "buffer-fill": "^1.0.0" + "dependencies": { + "@babel/types": "^7.20.7" } }, - "buffer-alloc-unsafe": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", - "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", + "node_modules/@types/body-parser": { + "version": "1.19.5", + "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", + "integrity": "sha512-fB3Zu92ucau0iQ0JMCFQE7b/dv8Ot07NI3KaZIkIUNXq82k4eBAqUaneXfleGY9JWskeS9y+u0nXMyspcuQrCg==", "dev": true, - "optional": true - }, - "buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", - "dev": true + "dependencies": { + "@types/connect": "*", + "@types/node": "*" + } }, - "buffer-fill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", - "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", + "node_modules/@types/bonjour": { + "version": "3.5.13", + "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", + "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", "dev": true, - "optional": true - }, - "buffer-from": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.1.tgz", - "integrity": "sha512-MQcXEUbCKtEo7bhqEs6560Hyd4XaovZlO/k9V3hjVUF/zwW7KBVdSK4gIt/bzwS9MbR5qob+F5jusZsb0YQK2A==", - "dev": true - }, - "buffer-indexof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/buffer-indexof/-/buffer-indexof-1.1.1.tgz", - "integrity": "sha512-4/rOEg86jivtPTeOUUT61jJO1Ya1TrR/OkqCSZDyq84WJh3LuuiphBYJN+fm5xufIk4XAFcEwte/8WzC8If/1g==", - "dev": true - }, - "buffer-xor": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", - "integrity": "sha1-JuYe0UIvtw3ULm42cp7VHYVf6Nk=", - "dev": true - }, - "builtin-status-codes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", - "integrity": "sha1-hZgoeOIbmOHGZCXgPQF0eI9Wnug=", - "dev": true + "dependencies": { + "@types/node": "*" + } }, - "bytes": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.0.tgz", - "integrity": "sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==" - }, - "cacache": { - "version": "12.0.4", - "resolved": "https://registry.npmjs.org/cacache/-/cacache-12.0.4.tgz", - "integrity": "sha512-a0tMB40oefvuInr4Cwb3GerbL9xTj1D5yg0T5xrjGCGyfvbxseIXX7BAO/u/hIXdafzOI5JC3wDwHyf24buOAQ==", - "dev": true, - "requires": { - "bluebird": "^3.5.5", - "chownr": "^1.1.1", - "figgy-pudding": "^3.5.1", - "glob": "^7.1.4", - "graceful-fs": "^4.1.15", - "infer-owner": "^1.0.3", - "lru-cache": "^5.1.1", - "mississippi": "^3.0.0", - "mkdirp": "^0.5.1", - "move-concurrently": "^1.0.1", - "promise-inflight": "^1.0.1", - "rimraf": "^2.6.3", - "ssri": "^6.0.1", - "unique-filename": "^1.1.1", - "y18n": "^4.0.0" - }, - "dependencies": { - "lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "dev": true, - "requires": { - "yallist": "^3.0.2" - } - }, - "yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "dev": true - } + "node_modules/@types/codemirror": { + "version": "5.60.17", + "resolved": "https://registry.npmjs.org/@types/codemirror/-/codemirror-5.60.17.tgz", + "integrity": "sha512-AZq2FIsUHVMlp7VSe2hTfl5w4pcUkoFkM3zVsRKsn1ca8CXRDYvnin04+HP2REkwsxemuHqvDofdlhUWNpbwfw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/tern": "*" } }, - "cache-base": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", - "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "node_modules/@types/connect": { + "version": "3.4.38", + "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", + "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", "dev": true, - "requires": { - "collection-visit": "^1.0.0", - "component-emitter": "^1.2.1", - "get-value": "^2.0.6", - "has-value": "^1.0.0", - "isobject": "^3.0.1", - "set-value": "^2.0.0", - "to-object-path": "^0.3.0", - "union-value": "^1.0.0", - "unset-value": "^1.0.0" + "dependencies": { + "@types/node": "*" } }, - "cacheable-request": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", - "integrity": "sha1-DYCIAbY0KtM8kd+dC0TcCbkeXD0=", + "node_modules/@types/connect-history-api-fallback": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", + "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", "dev": true, - "optional": true, - "requires": { - "clone-response": "1.0.2", - "get-stream": "3.0.0", - "http-cache-semantics": "3.8.1", - "keyv": "3.0.0", - "lowercase-keys": "1.0.0", - "normalize-url": "2.0.1", - "responselike": "1.0.2" - }, "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - }, - "lowercase-keys": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", - "integrity": "sha1-TjNms55/VFfjXxMkvfb4jQv8cwY=", - "dev": true, - "optional": true - }, - "normalize-url": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", - "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", - "dev": true, - "optional": true, - "requires": { - "prepend-http": "^2.0.0", - "query-string": "^5.0.1", - "sort-keys": "^2.0.0" - } - }, - "prepend-http": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", - "integrity": "sha1-6SQ0v6XqjBn0HN/UAddBo8gZ2Jc=", - "dev": true, - "optional": true - }, - "query-string": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", - "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", - "dev": true, - "optional": true, - "requires": { - "decode-uri-component": "^0.2.0", - "object-assign": "^4.1.0", - "strict-uri-encode": "^1.0.0" - } - }, - "sort-keys": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", - "integrity": "sha1-ZYU1WEhh7JfXMNbPQYIuH1ZoQSg=", - "dev": true, - "optional": true, - "requires": { - "is-plain-obj": "^1.0.0" - } - } + "@types/express-serve-static-core": "*", + "@types/node": "*" } }, - "call-bind": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.0.tgz", - "integrity": "sha512-AEXsYIyyDY3MCzbwdhzG3Jx1R0J2wetQyUynn6dYHAO+bg8l1k7jwZtRv4ryryFs7EP+NDlikJlVe59jr0cM2w==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.0" + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" } }, - "call-me-maybe": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", - "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", - "dev": true + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } }, - "caller-callsite": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-callsite/-/caller-callsite-2.0.0.tgz", - "integrity": "sha1-hH4PzgoiN1CpoCfFSzNzGtMVQTQ=", + "node_modules/@types/espree": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/@types/espree/-/espree-10.1.0.tgz", + "integrity": "sha512-uPQZdoUWWMuO6WS8/dwX1stZH/vOBa/wAniGnYEFI0IuU9RmLx6PLmo+VGfNOlbRc5I7hBsQc8H0zcdVI37kxg==", "dev": true, - "requires": { - "callsites": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "callsites": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-2.0.0.tgz", - "integrity": "sha1-BuuE8A7qQT2oav/vrL/7Ngk7PFA=", - "dev": true - } + "acorn": "^8.12.0", + "eslint-visitor-keys": "^4.0.0" } }, - "caller-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-2.0.0.tgz", - "integrity": "sha1-Ro+DBE42mrIBD6xfBs7uFbsssfQ=", + "node_modules/@types/espree/node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", "dev": true, - "requires": { - "caller-callsite": "^2.0.0" + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" } }, - "callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==" + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" }, - "camelcase": { - "version": "5.3.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", - "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==" + "node_modules/@types/express": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.21.tgz", + "integrity": "sha512-ejlPM315qwLpaQlQDTjPdsUFSc6ZsP4AN6AlWnogPjQ7CVi7PYF3YVz+CY3jE2pwYf7E/7HlDAN0rV2GxTG0HQ==", + "dev": true, + "dependencies": { + "@types/body-parser": "*", + "@types/express-serve-static-core": "^4.17.33", + "@types/qs": "*", + "@types/serve-static": "*" + } }, - "camelcase-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", - "integrity": "sha1-MIvur/3ygRkFHvodkyITyRuPkuc=", + "node_modules/@types/express-serve-static-core": { + "version": "4.17.41", + "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.17.41.tgz", + "integrity": "sha512-OaJ7XLaelTgrvlZD8/aa0vvvxZdUmlCn6MtWeB7TkiKW70BQLc9XEPpDLPdbo52ZhXUCrznlWdCHWxJWtdyajA==", "dev": true, - "optional": true, - "requires": { - "camelcase": "^2.0.0", - "map-obj": "^1.0.0" - }, "dependencies": { - "camelcase": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", - "integrity": "sha1-fB0W1nmhu+WcoCys7PsBHiAfWh8=", - "dev": true, - "optional": true - } + "@types/node": "*", + "@types/qs": "*", + "@types/range-parser": "*", + "@types/send": "*" } }, - "caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "node_modules/@types/graceful-fs": { + "version": "4.1.6", + "resolved": "https://registry.npmjs.org/@types/graceful-fs/-/graceful-fs-4.1.6.tgz", + "integrity": "sha512-Sig0SNORX9fdW+bQuTEovKj3uHcUL6LQKbCrrqb1X7J6/ReAbhCXRAhc+SMejhLELFj2QcyuxmUooZ4bt5ReSw==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" + "dependencies": { + "@types/node": "*" } }, - "caniuse-lite": { - "version": "1.0.30001245", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001245.tgz", - "integrity": "sha512-768fM9j1PKXpOCKws6eTo3RHmvTUsG9UrpT4WoREFeZgJBTi4/X9g565azS/rVUGtqb8nt7FjLeF5u4kukERnA==", + "node_modules/@types/htmlhint": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@types/htmlhint/-/htmlhint-1.1.5.tgz", + "integrity": "sha512-BnMb05tZKcK0M/GK28H1jmCYRDqhmMUbxakbmmrBJ2vNpKPHLmAEWkq4UXdPN3cq3MDySZizhcbmYEg9i9G/QA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/http-errors": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.4.tgz", + "integrity": "sha512-D0CFMMtydbJAegzOyHjtiKPLlvnm3iTZyZRSZoLq2mRhDdmLfIWOCYPfQJ4cu2erKghU++QvjcUjp/5h7hESpA==", "dev": true }, - "capture-exit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/capture-exit/-/capture-exit-2.0.0.tgz", - "integrity": "sha512-PiT/hQmTonHhl/HFGN+Lx3JJUznrVYJ3+AQsnthneZbvW7x+f08Tk7yLJTLEOUvBTbduLeeBkxEaYXUOUrRq6g==", + "node_modules/@types/http-proxy": { + "version": "1.17.14", + "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.14.tgz", + "integrity": "sha512-SSrD0c1OQzlFX7pGu1eXxSEjemej64aaNPRhhVYUGqXh0BtldAAx37MG8btcumvpgKyZp1F5Gn3JkktdxiFv6w==", "dev": true, - "requires": { - "rsvp": "^4.8.4" + "dependencies": { + "@types/node": "*" } }, - "caseless": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", - "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", - "dev": true + "node_modules/@types/istanbul-lib-coverage": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", + "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", + "dev": true, + "license": "MIT" }, - "catharsis": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", - "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", + "node_modules/@types/istanbul-lib-report": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", + "integrity": "sha512-plGgXAPfVKFoYfa9NpYDAkseG+g6Jr294RqeqcqDixSbU34MZVJRi/P+7Y8GDpzkEwLaGZZOpKIEmeVZNtKsrg==", "dev": true, - "requires": { - "lodash": "^4.17.15" + "dependencies": { + "@types/istanbul-lib-coverage": "*" } }, - "caw": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", - "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", + "node_modules/@types/istanbul-reports": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", + "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", "dev": true, - "optional": true, - "requires": { - "get-proxy": "^2.0.0", - "isurl": "^1.0.0-alpha5", - "tunnel-agent": "^0.6.0", - "url-to-options": "^1.0.1" + "license": "MIT", + "dependencies": { + "@types/istanbul-lib-report": "*" } }, - "center-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/center-align/-/center-align-0.1.3.tgz", - "integrity": "sha1-qg0yYptu6XIgBBHL1EYckHvCt60=", - "requires": { - "align-text": "^0.1.3", - "lazy-cache": "^1.0.3" + "node_modules/@types/jquery": { + "version": "3.5.34", + "resolved": "https://registry.npmjs.org/@types/jquery/-/jquery-3.5.34.tgz", + "integrity": "sha512-3m3939S3erqmTLJANS/uy0B6V7BorKx7RorcGZVjZ62dF5PAGbKEDZK1CuLtKombJkFA2T1jl8LAIIs7IV6gBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/sizzle": "*" } }, - "chalk": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz", - "integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==", + "node_modules/@types/jsdom": { + "version": "21.1.7", + "resolved": "https://registry.npmjs.org/@types/jsdom/-/jsdom-21.1.7.tgz", + "integrity": "sha512-yOriVnggzrnQ3a9OKOCxaVuSug3w3/SbOj5i7VwXWZEyUNl3bLF9V3MfxGbZKuwqJOQyRfqXyROBB1CoZLFWzA==", "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "@types/node": "*", + "@types/tough-cookie": "*", + "parse5": "^7.0.0" } }, - "char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/linkify-it": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/linkify-it/-/linkify-it-3.0.2.tgz", + "integrity": "sha512-HZQYqbiFVWufzCwexrvh694SOim8z2d+xJl5UNamcvQFejLY/2YUtzXHYi3cHdI7PMlS8ejH2slRAOJQ32aNbA==", "dev": true }, - "character-entities": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-1.2.4.tgz", - "integrity": "sha512-iBMyeEHxfVnIakwOuDXpVkc54HijNgCyQB2w0VfGQThle6NXn50zU6V/u+LDhxHcDUPojn6Kpga3PTAD8W1bQw==", + "node_modules/@types/markdown-it": { + "version": "12.2.3", + "resolved": "https://registry.npmjs.org/@types/markdown-it/-/markdown-it-12.2.3.tgz", + "integrity": "sha512-GKMHFfv3458yYy+v/N8gjufHO6MSZKCOXpZc5GXIWWy8uldwfmPn98vp81gZ5f9SVw8YYBctgfJ22a2d7AOMeQ==", + "dev": true, + "dependencies": { + "@types/linkify-it": "*", + "@types/mdurl": "*" + } + }, + "node_modules/@types/mdurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@types/mdurl/-/mdurl-1.0.2.tgz", + "integrity": "sha512-eC4U9MlIcu2q0KQmXszyn5Akca/0jrQmwDRgpAMJai7qBWq4amIQhZyNau4VYGtCeALvW1/NtjzJJ567aZxfKA==", "dev": true }, - "character-entities-legacy": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-1.1.4.tgz", - "integrity": "sha512-3Xnr+7ZFS1uxeiUDvV02wQ+QDbc55o97tIV5zHScSPJpcLm/r0DFPcoY3tYRp+VZukxuMeKgXYmsXQHO05zQeA==", + "node_modules/@types/mime": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", + "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", "dev": true }, - "character-reference-invalid": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-1.1.4.tgz", - "integrity": "sha512-mKKUkUbhPpQlCOfIuZkvSEgktjPFIsZKRRbC6KWVEMvlzblj3i3asQv5ODsrwt0N3pHAEvjP8KTQPHkp0+6jOg==", + "node_modules/@types/minimist": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@types/minimist/-/minimist-1.2.2.tgz", + "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, - "chardet": { - "version": "0.4.2", - "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", - "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "node_modules/@types/node": { + "version": "14.14.20", + "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.20.tgz", + "integrity": "sha512-Y93R97Ouif9JEOWPIUyU+eyIdyRqQR0I8Ez1dzku4hDx34NWh4HbtIc3WNzwB1Y9ULvNGeu5B8h8bVL5cAk4/A==", "dev": true }, - "check-node-version": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.1.0.tgz", - "integrity": "sha512-TSXGsyfW5/xY2QseuJn8/hleO2AU7HxVCdkc900jp1vcfzF840GkjvRT7CHl8sRtWn23n3X3k0cwH9RXeRwhfw==", + "node_modules/@types/node-forge": { + "version": "1.3.11", + "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.11.tgz", + "integrity": "sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==", "dev": true, - "requires": { - "chalk": "^3.0.0", - "map-values": "^1.0.1", - "minimist": "^1.2.0", - "object-filter": "^1.0.2", - "run-parallel": "^1.1.4", - "semver": "^6.3.0" - }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "@types/node": "*" } }, - "cheerio": { - "version": "1.0.0-rc.10", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.10.tgz", - "integrity": "sha512-g0J0q/O6mW8z5zxQ3A8E8J1hUgp4SMOvEoW/x84OwyHKe/Zccz83PVT4y5Crcr530FV6NgmKI1qvGTKVl9XXVw==", - "dev": true, - "requires": { - "cheerio-select": "^1.5.0", - "dom-serializer": "^1.3.2", - "domhandler": "^4.2.0", - "htmlparser2": "^6.1.0", - "parse5": "^6.0.1", - "parse5-htmlparser2-tree-adapter": "^6.0.1", - "tslib": "^2.2.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==", - "dev": true - } - } + "node_modules/@types/normalize-package-data": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz", + "integrity": "sha512-Gj7cI7z+98M282Tqmp2K5EIsoouUEzbBJhQQzDE3jSIRk6r9gsz0oUokqIUR4u1R3dMHo0pDHM7sNOHyhulypw==", + "dev": true }, - "cheerio-select": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-1.5.0.tgz", - "integrity": "sha512-qocaHPv5ypefh6YNxvnbABM07KMxExbtbfuJoIie3iZXX1ERwYmJcIiRrr9H05ucQP1k28dav8rpdDgjQd8drg==", - "dev": true, - "requires": { - "css-select": "^4.1.3", - "css-what": "^5.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0", - "domutils": "^2.7.0" - }, - "dependencies": { - "css-select": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.1.3.tgz", - "integrity": "sha512-gT3wBNd9Nj49rAbmtFHj1cljIAOLYSX1nZ8CB7TBO3INYckygm5B7LISU/szY//YmdiSLbJvDLOx9VnMVpMBxA==", - "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^5.0.0", - "domhandler": "^4.2.0", - "domutils": "^2.6.0", - "nth-check": "^2.0.0" - } - }, - "css-what": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-5.0.1.tgz", - "integrity": "sha512-FYDTSHb/7KXsWICVsxdmiExPjCfRC4qRFBdVwv7Ax9hMnvMmEjP9RfxTEZ3qPZGmADDn2vAKSo9UcN1jKVYscg==", - "dev": true - }, - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } - }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } - }, - "nth-check": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.0.0.tgz", - "integrity": "sha512-i4sc/Kj8htBrAiH1viZ0TgU8Y5XqCaV/FziYK6TBczxmeKm3AEFWqqF3195yKudrarqy7Zu80Ra5dobFjn9X/Q==", - "dev": true, - "requires": { - "boolbase": "^1.0.0" - } - } - } + "node_modules/@types/parse-json": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/@types/parse-json/-/parse-json-4.0.0.tgz", + "integrity": "sha512-//oorEZjL6sbPcKUaCdIGlIUeH26mgzimjBB77G6XRgnDl/L5wOnpyBGRe/Mmf5CVW3PwEBE1NjiMZ/ssFh4wA==", + "dev": true }, - "chokidar": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.0.tgz", - "integrity": "sha512-JgQM9JS92ZbFR4P90EvmzNpSGhpPBGBSj10PILeDyYFwp4h2/D9OM03wsJ4zW1fEp4ka2DGrnUeD7FuvQ2aZ2Q==", - "requires": { - "anymatch": "~3.1.1", - "braces": "~3.0.2", - "fsevents": "~2.3.1", - "glob-parent": "~5.1.0", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.5.0" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - } - } + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true, + "license": "MIT" }, - "chownr": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", - "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "node_modules/@types/q": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@types/q/-/q-1.5.4.tgz", + "integrity": "sha512-1HcDas8SEj4z1Wc696tH56G8OlRaH/sqZOynNNB+HF0WOeXPaxTtbYzJY2oEfiUxjSKjhCKr+MvR7dCHcEelug==", + "dev": true, + "optional": true + }, + "node_modules/@types/qs": { + "version": "6.9.11", + "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.11.tgz", + "integrity": "sha512-oGk0gmhnEJK4Yyk+oI7EfXsLayXatCWPHary1MtcmbAifkobT9cM9yutG/hZKIseOU0MqbIwQ/u2nn/Gb+ltuQ==", "dev": true }, - "chrome-trace-event": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.2.tgz", - "integrity": "sha512-9e/zx1jw7B4CO+c/RXoCsfg/x1AfUBioy4owYH0bJprEYAx5hRFLRhWBqHAG57D0ZM4H7vxbP7bPe0VwhQRYDQ==", + "node_modules/@types/range-parser": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", + "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", + "dev": true + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", "dev": true, - "requires": { - "tslib": "^1.9.0" - }, + "license": "MIT", "dependencies": { - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - } + "@types/prop-types": "*", + "csstype": "^3.2.2" } }, - "ci-info": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-2.0.0.tgz", - "integrity": "sha512-5tK7EtrZ0N+OLFMthtqOj4fI2Jeb88C4CAZPu25LDVUgXJ0A3Js4PMGqrn0JU1W0Mh1/Z8wZzYPxqUrXeBboCQ==", - "dev": true - }, - "cipher-base": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.4.tgz", - "integrity": "sha512-Kkht5ye6ZGmwv40uUDZztayT2ThLQGfnj/T71N/XzeZeo3nf8foyW7zGTsPYkEya3m5f3cAypH+qe7YOrM1U2Q==", + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "license": "MIT", + "peerDependencies": { + "@types/react": "^18.0.0" } }, - "cjs-module-lexer": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-0.6.0.tgz", - "integrity": "sha512-uc2Vix1frTfnuzxxu1Hp4ktSvM3QaI4oXl4ZUqL1wjTu/BGki9TrCWoqLTg/drR1KwAEarXuRFCG2Svr1GxPFw==", + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", "dev": true }, - "class-utils": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", - "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", + "node_modules/@types/sarif": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@types/sarif/-/sarif-2.1.7.tgz", + "integrity": "sha512-kRz0VEkJqWLf1LLVN4pT1cg1Z9wAuvI6L97V3m2f5B76Tg8d413ddvLBPTEHAZJlnn4XSvu0FkZtViCQGVyrXQ==", + "license": "MIT" + }, + "node_modules/@types/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/@types/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-FmgJfu+MOcQ370SD0ev7EI8TlCAfKYU+B4m5T3yXc1CiRN94g/SZPtsCkk506aUDtlMnFZvasDwHHUcZUEaYuA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/send": { + "version": "0.17.4", + "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.4.tgz", + "integrity": "sha512-x2EM6TJOybec7c52BX0ZspPodMsQUd5L6PRwOunVyVUhXiBSKf3AezDL8Dgvgt5o0UfKNfuA0eMLr2wLT4AiBA==", "dev": true, - "requires": { - "arr-union": "^3.1.0", - "define-property": "^0.2.5", - "isobject": "^3.0.0", - "static-extend": "^0.1.1" - }, "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } + "@types/mime": "^1", + "@types/node": "*" } }, - "classnames": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.1.tgz", - "integrity": "sha512-OlQdbZ7gLfGarSqxesMesDa5uz7KFbID8Kpq/SxIoNGDqY8lSYs0D+hhtBXhcdB3rcbXArFr7vlHheLk1voeNA==" - }, - "cldr-core": { - "version": "38.1.0", - "resolved": "https://registry.npmjs.org/cldr-core/-/cldr-core-38.1.0.tgz", - "integrity": "sha512-Da9xKjDp4qGGIX0VDsBqTan09iR5nuYD2a/KkfEaUyqKhu6wFVNRiCpPDXeRbpVwPBY6PgemV8WiHatMhcpy4A==" - }, - "clean-css": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.1.1.tgz", - "integrity": "sha512-GQ6HdEyJN0543mRTA/TkZ7RPoMXGWKq1shs9H86F2kLuixR0RI+xd4JfhJxWUW08FGKQXTKAKpVjKQXu5zkFNA==", + "node_modules/@types/serve-index": { + "version": "1.9.4", + "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", + "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", "dev": true, - "requires": { - "source-map": "~0.6.0" - }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "@types/express": "*" } }, - "clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "dev": true - }, - "clean-webpack-plugin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/clean-webpack-plugin/-/clean-webpack-plugin-3.0.0.tgz", - "integrity": "sha512-MciirUH5r+cYLGCOL5JX/ZLzOZbVr1ot3Fw+KcvbhUb6PM+yycqd9ZhIlcigQ5gl+XhppNmw3bEFuaaMNyLj3A==", + "node_modules/@types/serve-static": { + "version": "1.15.5", + "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.5.tgz", + "integrity": "sha512-PDRk21MnK70hja/YF8AHfC7yIsiQHn1rcXx7ijCFBX/k+XQJhQT/gw3xekXKJvx+5SXaMMS8oqQy09Mzvz2TuQ==", "dev": true, - "requires": { - "@types/webpack": "^4.4.31", - "del": "^4.1.1" + "dependencies": { + "@types/http-errors": "*", + "@types/mime": "*", + "@types/node": "*" } }, - "cli": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", - "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "node_modules/@types/sizzle": { + "version": "2.3.10", + "resolved": "https://registry.npmjs.org/@types/sizzle/-/sizzle-2.3.10.tgz", + "integrity": "sha512-TC0dmN0K8YcWEAEfiPi5gJP14eJe30TTGjkvek3iM/1NdHHsdCA/Td6GvNndMOo/iSnIsZ4HuuhrYPDAmbxzww==", "dev": true, - "requires": { - "exit": "0.1.2", - "glob": "^7.1.1" - } + "license": "MIT" }, - "cli-cursor": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", - "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", + "node_modules/@types/sockjs": { + "version": "0.3.36", + "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", + "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", "dev": true, - "requires": { - "restore-cursor": "^2.0.0" + "dependencies": { + "@types/node": "*" } }, - "cli-width": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", - "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", - "dev": true - }, - "clipboard": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", - "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } + "node_modules/@types/stack-utils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-2.0.3.tgz", + "integrity": "sha512-9aEbYZ3TbYMznPdcdr3SmIrLXwC/AKZXQeCf9Pgao5CKb8CyHuEX5jzWPTkvregvhRJHcpRO6BFoGW9ycaOkYw==", + "dev": true, + "license": "MIT" }, - "cliui": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", - "integrity": "sha512-PYeGSEmmHM6zvoef2w8TPzlrnNpXIjTipYK780YswmIP9vjxmd6Y2a3CB2Ks6/AU8NHjZugXvo8w3oWM2qnwXA==", - "requires": { - "string-width": "^3.1.0", - "strip-ansi": "^5.2.0", - "wrap-ansi": "^5.1.0" + "node_modules/@types/tern": { + "version": "0.23.9", + "resolved": "https://registry.npmjs.org/@types/tern/-/tern-0.23.9.tgz", + "integrity": "sha512-ypzHFE/wBzh+BlH6rrBgS5I/Z7RD21pGhZ2rltb/+ZrVM1awdZwjx7hE5XfuYgHWk9uvV5HLZN3SloevCAp3Bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*" } }, - "clone-deep": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", - "integrity": "sha1-TnPdCen7lxzDhnDF3O2cGJZIHMY=", + "node_modules/@types/tough-cookie": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/@types/tough-cookie/-/tough-cookie-4.0.5.tgz", + "integrity": "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA==", "dev": true, - "requires": { - "for-own": "^0.1.3", - "is-plain-object": "^2.0.1", - "kind-of": "^3.0.2", - "lazy-cache": "^1.0.3", - "shallow-clone": "^0.1.2" - } + "license": "MIT" }, - "clone-regexp": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clone-regexp/-/clone-regexp-2.2.0.tgz", - "integrity": "sha512-beMpP7BOtTipFuW8hrJvREQ2DrRu3BE7by0ZpibtfBA+qfHYvMGTc2Yb1JMYPKg/JUw0CHYvpg796aNTSW9z7Q==", + "node_modules/@types/underscore": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/@types/underscore/-/underscore-1.13.0.tgz", + "integrity": "sha512-L6LBgy1f0EFQZ+7uSA57+n2g/s4Qs5r06Vwrwn0/nuK1de+adz00NWaztRQ30aEqw5qOaWbPI8u2cGQ52lj6VA==", "dev": true, - "requires": { - "is-regexp": "^2.0.0" - } + "license": "MIT" }, - "clone-response": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", - "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "node_modules/@types/ws": { + "version": "8.5.10", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.10.tgz", + "integrity": "sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==", "dev": true, - "optional": true, - "requires": { - "mimic-response": "^1.0.0" + "dependencies": { + "@types/node": "*" } }, - "co": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", - "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", - "dev": true - }, - "coa": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", - "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "node_modules/@types/yargs": { + "version": "17.0.35", + "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.35.tgz", + "integrity": "sha512-qUHkeCyQFxMXg79wQfTtfndEC+N9ZZg76HJftDJp+qH2tV7Gj4OJi7l+PiWwJ+pWtW8GwSmqsDj/oymhrTWXjg==", "dev": true, - "requires": { - "@types/q": "^1.5.1", - "chalk": "^2.4.1", - "q": "^1.1.2" - }, + "license": "MIT", "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "@types/yargs-parser": "*" } }, - "coffee-script": { - "version": "1.12.7", - "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", - "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", - "dev": true - }, - "collapse-white-space": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-1.0.6.tgz", - "integrity": "sha512-jEovNnrhMuqyCcjfEJA56v0Xq8SkIoPKDyaHahwo3POf4qcSXqMYuwNcOTzp74vTsR9Tn08z4MxWqAhcekogkQ==", - "dev": true - }, - "collect-v8-coverage": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.1.tgz", - "integrity": "sha512-iBPtljfCNcTKNAto0KEtDfZ3qzjJvqE3aTGZsbhjSBlorqpXJlaWWtPO35D+ZImoC3KWejX64o+yPGxhWSTzfg==", + "node_modules/@types/yargs-parser": { + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.0.tgz", + "integrity": "sha512-iO9ZQHkZxHn4mSakYV0vFHAVDyEOIJQrV2uZ06HxEPcx+mt8swXoZHIbaaJ2crJYFfErySgktuTZ3BeLz+XmFA==", "dev": true }, - "collection-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", - "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "node_modules/@types/yauzl": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.9.1.tgz", + "integrity": "sha512-A1b8SU4D10uoPjwb0lnHmmu8wZhR9d+9o2PKBQT2jU5YPTKsxac6M2qGAdY7VcL+dHHhARVUDmeg0rOrcd9EjA==", "dev": true, - "requires": { - "map-visit": "^1.0.0", - "object-visit": "^1.0.0" + "optional": true, + "dependencies": { + "@types/node": "*" } }, - "color": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/color/-/color-3.1.3.tgz", - "integrity": "sha512-xgXAcTHa2HeFCGLE9Xs/R82hujGtu9Jd9x4NW3T34+OMs7VoPsjwzRczKHvTAHeJwWFwX5j15+MgAppE8ztObQ==", + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-6.21.0.tgz", + "integrity": "sha512-oy9+hTPCUFpngkEZUSzbf9MxI65wbKFoQYsgPdILTfbUldp5ovUuphZVe4i30emU9M/kP+T64Di0mxl7dSw3MA==", "dev": true, - "requires": { - "color-convert": "^1.9.1", - "color-string": "^1.5.4" + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.5.1", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/type-utils": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "graphemer": "^1.4.0", + "ignore": "^5.2.4", + "natural-compare": "^1.4.0", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^6.0.0 || ^6.0.0-alpha", + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "color-convert": { - "version": "1.9.3", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", - "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", - "requires": { - "color-name": "1.1.3" + "node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "color-name": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", - "integrity": "sha1-p9BVi9icQveV3UIyj3QIMcpTvCU=" - }, - "color-string": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.5.5.tgz", - "integrity": "sha512-jgIoum0OfQfq9Whcfc2z/VhCNcmQjWbey6qBX0vqt7YICflUmBCh9E9CiQD5GSJ+Uehixm3NUwHVhqUAWRivZg==", + "node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", "dev": true, - "requires": { - "color-name": "^1.0.0", - "simple-swizzle": "^0.2.2" + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "colorette": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.1.tgz", - "integrity": "sha512-puCDz0CzydiSYOrnXpz/PKd69zRrribezjtE9yd4zvytoRc8+RY/KJPvtPFKZS3E3wP6neGyMe0vOTlHO5L3Pw==", - "dev": true - }, - "colors": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", - "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", - "dev": true - }, - "combined-stream": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", - "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "node_modules/@typescript-eslint/type-utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-6.21.0.tgz", + "integrity": "sha512-rZQI7wHfao8qMX3Rd3xqeYSMCL3SoiSQLBATSiVKARdFGCYSRvmViieZjqc58jKgs8Y8i9YvVVhRbHSTA4VBag==", "dev": true, - "requires": { - "delayed-stream": "~1.0.0" + "license": "MIT", + "dependencies": { + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/utils": "6.21.0", + "debug": "^4.3.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, - "commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true - }, - "comment-parser": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.1.5.tgz", - "integrity": "sha512-RePCE4leIhBlmrqiYTvaqEeGYg7qpSl4etaIabKtdOQVi+mSTIBBklGUwIr79GXYnl3LpMwmDw4KeR2stNc6FA==", - "dev": true - }, - "commondir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz", - "integrity": "sha1-3dgA2gxmEnOTzKWVDqloo6rxJTs=", - "dev": true - }, - "component-emitter": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", - "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==" - }, - "compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", + "node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", "dev": true, - "requires": { - "mime-db": ">= 1.43.0 < 2" + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "compression": { - "version": "1.7.4", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.7.4.tgz", - "integrity": "sha512-jaSIDzP9pZVS4ZfQ+TzvtiWhdpFhE2RDHz8QJkpX9SIpLq88VueF5jJw6t+6CUQcAoA6t+x89MLrWAqpfDE8iQ==", + "node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", "dev": true, - "requires": { - "accepts": "~1.3.5", - "bytes": "3.0.0", - "compressible": "~2.0.16", - "debug": "2.6.9", - "on-headers": "~1.0.2", - "safe-buffer": "5.1.2", - "vary": "~1.1.2" - }, + "license": "BSD-2-Clause", "dependencies": { - "bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha1-0ygVQE1olpn4Wk6k+odV3ROpYEg=", - "dev": true - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true } } }, - "compute-scroll-into-view": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/compute-scroll-into-view/-/compute-scroll-into-view-1.0.17.tgz", - "integrity": "sha512-j4dx+Fb0URmzbwwMUrhqWM2BEWHdFGx+qZ9qqASHRPqvTYdqvWnHg0H1hIbcyLnvgnoNAVMlwkepyqM3DaIFUg==" - }, - "computed-style": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/computed-style/-/computed-style-0.1.4.tgz", - "integrity": "sha1-fzRP2FhLLkJb7cpKGvwOMAuwXXQ=" - }, - "concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=" + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } }, - "concat-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", - "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^2.2.2", - "typedarray": "^0.0.6" + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "config-chain": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", - "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", + "node_modules/@typescript-eslint/utils": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-6.21.0.tgz", + "integrity": "sha512-NfWVaC8HP9T8cbKQxHcsJBY5YE1O33+jpMwN45qzWWaPDZgLIbo12toGMWnmhvCpd3sIxkpDw3Wv1B3dYrbDQQ==", "dev": true, - "optional": true, - "requires": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.4.0", + "@types/json-schema": "^7.0.12", + "@types/semver": "^7.5.0", + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "semver": "^7.5.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" } }, - "connect": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", - "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", - "requires": { - "debug": "2.6.9", - "finalhandler": "1.1.2", - "parseurl": "~1.3.3", - "utils-merge": "1.0.1" - }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" } }, - "connect-history-api-fallback": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-1.6.0.tgz", - "integrity": "sha512-e54B99q/OUoH64zYYRf3HBP5z24G38h5D3qXu23JGRoigpX5Ss4r9ZnDk3g0Z8uQC2x2lPaJ+UlWBc1ZWBWdLg==", - "dev": true - }, - "console-browserify": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", - "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==", - "dev": true + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } }, - "console-stream": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", - "integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=", + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", "dev": true, - "optional": true + "license": "ISC" }, - "consolidated-events": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/consolidated-events/-/consolidated-events-2.0.2.tgz", - "integrity": "sha512-2/uRVMdRypf5z/TW/ncD/66l75P5hH2vM/GR8Jf8HLc2xnfJtmina6F6du8+v4Z2vTrMo7jC+W1tmEEuuELgkQ==" + "node_modules/@unrs/resolver-binding-android-arm-eabi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm-eabi/-/resolver-binding-android-arm-eabi-1.11.1.tgz", + "integrity": "sha512-ppLRUgHVaGRWUx0R0Ut06Mjo9gBaBkg3v/8AxusGLhsIotbBLuRk51rAzqLC8gq6NyyAojEXglNjzf6R948DNw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-android-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-android-arm64/-/resolver-binding-android-arm64-1.11.1.tgz", + "integrity": "sha512-lCxkVtb4wp1v+EoN+HjIG9cIIzPkX5OtM03pQYkG+U5O/wL53LC4QbIeazgiKqluGeVEeBlZahHalCaBvU1a2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-arm64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-arm64/-/resolver-binding-darwin-arm64-1.11.1.tgz", + "integrity": "sha512-gPVA1UjRu1Y/IsB/dQEsp2V1pm44Of6+LWvbLc9SDk1c2KhhDRDBUkQCYVWe6f26uJb3fOK8saWMgtX8IrMk3g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-darwin-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-darwin-x64/-/resolver-binding-darwin-x64-1.11.1.tgz", + "integrity": "sha512-cFzP7rWKd3lZaCsDze07QX1SC24lO8mPty9vdP+YVa3MGdVgPmFc59317b2ioXtgCMKGiCLxJ4HQs62oz6GfRQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@unrs/resolver-binding-freebsd-x64": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-freebsd-x64/-/resolver-binding-freebsd-x64-1.11.1.tgz", + "integrity": "sha512-fqtGgak3zX4DCB6PFpsH5+Kmt/8CIi4Bry4rb1ho6Av2QHTREM+47y282Uqiu3ZRF5IQioJQ5qWRV6jduA+iGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-gnueabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-gnueabihf/-/resolver-binding-linux-arm-gnueabihf-1.11.1.tgz", + "integrity": "sha512-u92mvlcYtp9MRKmP+ZvMmtPN34+/3lMHlyMj7wXJDeXxuM0Vgzz0+PPJNsro1m3IZPYChIkn944wW8TYgGKFHw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm-musleabihf": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm-musleabihf/-/resolver-binding-linux-arm-musleabihf-1.11.1.tgz", + "integrity": "sha512-cINaoY2z7LVCrfHkIcmvj7osTOtm6VVT16b5oQdS4beibX2SYBwgYLmqhBjA1t51CarSaBuX5YNsWLjsqfW5Cw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-gnu/-/resolver-binding-linux-arm64-gnu-1.11.1.tgz", + "integrity": "sha512-34gw7PjDGB9JgePJEmhEqBhWvCiiWCuXsL9hYphDF7crW7UgI05gyBAi6MF58uGcMOiOqSJ2ybEeCvHcq0BCmQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-arm64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-arm64-musl/-/resolver-binding-linux-arm64-musl-1.11.1.tgz", + "integrity": "sha512-RyMIx6Uf53hhOtJDIamSbTskA99sPHS96wxVE/bJtePJJtpdKGXO1wY90oRdXuYOGOTuqjT8ACccMc4K6QmT3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-ppc64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-ppc64-gnu/-/resolver-binding-linux-ppc64-gnu-1.11.1.tgz", + "integrity": "sha512-D8Vae74A4/a+mZH0FbOkFJL9DSK2R6TFPC9M+jCWYia/q2einCubX10pecpDiTmkJVUH+y8K3BZClycD8nCShA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-gnu/-/resolver-binding-linux-riscv64-gnu-1.11.1.tgz", + "integrity": "sha512-frxL4OrzOWVVsOc96+V3aqTIQl1O2TjgExV4EKgRY09AJ9leZpEg8Ak9phadbuX0BA4k8U5qtvMSQQGGmaJqcQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-riscv64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-riscv64-musl/-/resolver-binding-linux-riscv64-musl-1.11.1.tgz", + "integrity": "sha512-mJ5vuDaIZ+l/acv01sHoXfpnyrNKOk/3aDoEdLO/Xtn9HuZlDD6jKxHlkN8ZhWyLJsRBxfv9GYM2utQ1SChKew==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-s390x-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-s390x-gnu/-/resolver-binding-linux-s390x-gnu-1.11.1.tgz", + "integrity": "sha512-kELo8ebBVtb9sA7rMe1Cph4QHreByhaZ2QEADd9NzIQsYNQpt9UkM9iqr2lhGr5afh885d/cB5QeTXSbZHTYPg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-gnu": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-gnu/-/resolver-binding-linux-x64-gnu-1.11.1.tgz", + "integrity": "sha512-C3ZAHugKgovV5YvAMsxhq0gtXuwESUKc5MhEtjBpLoHPLYM+iuwSj3lflFwK3DPm68660rZ7G8BMcwSro7hD5w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-linux-x64-musl": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-linux-x64-musl/-/resolver-binding-linux-x64-musl-1.11.1.tgz", + "integrity": "sha512-rV0YSoyhK2nZ4vEswT/QwqzqQXw5I6CjoaYMOX0TqBlWhojUf8P94mvI7nuJTeaCkkds3QE4+zS8Ko+GdXuZtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@unrs/resolver-binding-wasm32-wasi": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-wasm32-wasi/-/resolver-binding-wasm32-wasi-1.11.1.tgz", + "integrity": "sha512-5u4RkfxJm+Ng7IWgkzi3qrFOvLvQYnPBmjmZQ8+szTK/b31fQCnleNl1GgEt7nIsZRIf5PLhPwT0WM+q45x/UQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^0.2.11" + }, + "engines": { + "node": ">=14.0.0" + } }, - "constants-browserify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", - "integrity": "sha1-wguW2MYXdIqvHBYCF2DNJ/y4y3U=", - "dev": true + "node_modules/@unrs/resolver-binding-win32-arm64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-arm64-msvc/-/resolver-binding-win32-arm64-msvc-1.11.1.tgz", + "integrity": "sha512-nRcz5Il4ln0kMhfL8S3hLkxI85BXs3o8EYoattsJNdsX4YUU89iOkVn7g0VHSRxFuVMdM4Q1jEpIId1Ihim/Uw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-ia32-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-ia32-msvc/-/resolver-binding-win32-ia32-msvc-1.11.1.tgz", + "integrity": "sha512-DCEI6t5i1NmAZp6pFonpD5m7i6aFrpofcp4LA2i8IIq60Jyo28hamKBxNrZcyOwVOZkgsRp9O2sXWBWP8MnvIQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@unrs/resolver-binding-win32-x64-msvc": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@unrs/resolver-binding-win32-x64-msvc/-/resolver-binding-win32-x64-msvc-1.11.1.tgz", + "integrity": "sha512-lrW200hZdbfRtztbygyaq/6jP6AKE8qQN2KvPcJ+x7wiD038YtnYtZ82IMNJ69GJibV7bwL3y9FgK+5w/pYt6g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] }, - "content-disposition": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.3.tgz", - "integrity": "sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==", + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", "dev": true, - "requires": { - "safe-buffer": "5.1.2" - }, + "license": "MIT", "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true - } + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" } }, - "content-type": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", - "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" }, - "continuable-cache": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", - "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=", - "dev": true + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" }, - "convert-source-map": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.8.0.tgz", - "integrity": "sha512-+OQdjP49zViI/6i7nIJpA8rAl4sV/JdPfU9nZs3VqOwGIgizICvuN2ru6fMd+4llL0tar18UYJXfZ/TWtmhUjA==", - "requires": { - "safe-buffer": "~5.1.1" - }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" } }, - "cookie": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.0.tgz", - "integrity": "sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==", - "dev": true - }, - "cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=", - "dev": true + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" }, - "copy-concurrently": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/copy-concurrently/-/copy-concurrently-1.0.5.tgz", - "integrity": "sha512-f2domd9fsVDFtaFcbaRZuYXwtdmnzqbADSwhSWYxYB/Q8zsdUUFMXVRwXGDMWmbEzAn1kdRrtI1T/KTFOL4X2A==", + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", "dev": true, - "requires": { - "aproba": "^1.1.1", - "fs-write-stream-atomic": "^1.0.8", - "iferr": "^0.1.5", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.0" + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" } }, - "copy-descriptor": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", - "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", - "dev": true + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } }, - "copy-webpack-plugin": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-5.1.2.tgz", - "integrity": "sha512-Uh7crJAco3AjBvgAy9Z75CjK8IG+gxaErro71THQ+vv/bl4HaQcpkexAY8KVW/T6D2W2IRr+couF/knIRkZMIQ==", + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", "dev": true, - "requires": { - "cacache": "^12.0.3", - "find-cache-dir": "^2.1.0", - "glob-parent": "^3.1.0", - "globby": "^7.1.1", - "is-glob": "^4.0.1", - "loader-utils": "^1.2.3", - "minimatch": "^3.0.4", - "normalize-path": "^3.0.0", - "p-limit": "^2.2.1", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "webpack-log": "^2.0.0" - }, - "dependencies": { - "dir-glob": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.2.2.tgz", - "integrity": "sha512-f9LBi5QWzIW3I6e//uxZoLBlUt9kcp66qo0sSCxL6YZKc75R1c4MFCoe/LaZiBGmgujvQdxc5Bn3QhfyvK5Hsw==", - "dev": true, - "requires": { - "path-type": "^3.0.0" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "globby": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/globby/-/globby-7.1.1.tgz", - "integrity": "sha1-+yzP+UAfhgCUXfral0QMypcrhoA=", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "^2.0.0", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - } - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" } }, - "core-js": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.17.0.tgz", - "integrity": "sha512-zXT4rclS9jM6tikbAUKAGLonuRKOJ2ZvBnZCEOJAbzuTLw4kKcuA5plNt8juzdU6O/py/EgAehzvLh0VXEdBbQ==", - "dev": true + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" }, - "core-js-compat": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.17.0.tgz", - "integrity": "sha512-haEcBrfU3hu83JXWpcLHzeg8Ypf05LGK4GIjzLiYgFJYXuxrkdN2MrDBeHt/t5/ZFmIzLcdsT2x8Xw654wXsuw==", - "dev": true, - "requires": { - "browserslist": "^4.16.8", - "semver": "7.0.0" - }, - "dependencies": { - "browserslist": { - "version": "4.16.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.8.tgz", - "integrity": "sha512-sc2m9ohR/49sWEbPj14ZSSZqp+kbi16aLao42Hmn3Z8FpjuMaq2xCA2l4zl9ITfyzvnvyE0hcg62YkIGKxgaNQ==", - "dev": true, - "requires": { - "caniuse-lite": "^1.0.30001251", - "colorette": "^1.3.0", - "electron-to-chromium": "^1.3.811", - "escalade": "^3.1.1", - "node-releases": "^1.1.75" - } - }, - "caniuse-lite": { - "version": "1.0.30001252", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001252.tgz", - "integrity": "sha512-I56jhWDGMtdILQORdusxBOH+Nl/KgQSdDmpJezYddnAkVOmnoU8zwjTV9xAjMIYxr0iPreEAVylCGcmHCjfaOw==", - "dev": true - }, - "colorette": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", - "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", - "dev": true - }, - "electron-to-chromium": { - "version": "1.3.826", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.826.tgz", - "integrity": "sha512-bpLc4QU4B8PYmdO4MSu2ZBTMD8lAaEXRS43C09lB31BvYwuk9UxgBRXbY5OJBw7VuMGcg2MZG5FyTaP9u4PQnw==", - "dev": true - }, - "node-releases": { - "version": "1.1.75", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.75.tgz", - "integrity": "sha512-Qe5OUajvqrqDSy6wrWFmMwfJ0jVgwiw4T3KqmbTcZ62qW0gQkheXYhcFM1+lOVcGUoRxcEcfyvFMAnDgaF1VWw==", - "dev": true - }, - "semver": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.0.0.tgz", - "integrity": "sha512-+GB6zVA9LWh6zovYQLALHwv5rb2PHGlJi3lfiqIHxR0uuwCgefcOJc59v9fv1w8GbStwxuuqqAjI9NMAOOgq1A==", - "dev": true - } + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" } }, - "core-js-pure": { - "version": "3.17.0", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.17.0.tgz", - "integrity": "sha512-O5RvMRWW+I0hfR227mrIwU+gPLVaa4kPEq+9b8FcjuFed4QckOvYc94c2KSI/X5dlvcsj/V1Sp5F5cecYpNQOQ==", - "dev": true + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } }, - "core-js-url-browser": { - "version": "3.6.4", - "resolved": "https://registry.npmjs.org/core-js-url-browser/-/core-js-url-browser-3.6.4.tgz", - "integrity": "sha512-VCMkPikOVp5JXftTj0E3gPZNKa0exQX837KxyPcnMAKvImWG8+RbXwEHEGMjiNz+9Vl2YgutkVYOpq7iaSOt/Q==" + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } }, - "core-util-is": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", - "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } }, - "cors": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", - "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", - "requires": { - "object-assign": "^4", - "vary": "^1" + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, - "cosmiconfig": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-6.0.0.tgz", - "integrity": "sha512-xb3ZL6+L8b9JLLCx3ZdoZy4+2ECphCMo2PwqgP1tlfVq6M6YReyzBJtvWWtbDSpNr9hn96pkCiZqUcFEc+54Qg==", - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.1.0", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.7.2" + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" } }, - "create-ecdh": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", - "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", "dev": true, - "requires": { - "bn.js": "^4.1.0", - "elliptic": "^6.5.3" + "engines": { + "node": ">=14.15.0" }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true } } }, - "create-emotion": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/create-emotion/-/create-emotion-10.0.27.tgz", - "integrity": "sha512-fIK73w82HPPn/RsAij7+Zt8eCE8SptcJ3WoRMfxMtjteYxud8GDTKKld7MYwAX2TVhrw29uR1N/bVGxeStHILg==", - "requires": { - "@emotion/cache": "^10.0.27", - "@emotion/serialize": "^0.11.15", - "@emotion/sheet": "0.9.4", - "@emotion/utils": "0.11.3" + "node_modules/@wordpress/babel-preset-default": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/babel-preset-default/-/babel-preset-default-8.42.0.tgz", + "integrity": "sha512-/WC38ZuGsLYF7yXyqzMwgcKBB49sE94SymXTmSwwHpglJ1CaLpcrI7LcUdqsz1M7YbkUHN/2UgvqWe/E95Fm/w==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/core": "7.25.7", + "@babel/plugin-syntax-import-attributes": "7.26.0", + "@babel/plugin-transform-react-jsx": "7.25.7", + "@babel/plugin-transform-runtime": "7.25.7", + "@babel/preset-env": "7.25.7", + "@babel/preset-typescript": "7.25.7", + "@wordpress/browserslist-config": "^6.42.0", + "@wordpress/warning": "^3.42.0", + "browserslist": "^4.21.10", + "core-js": "^3.31.0", + "react": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "create-hash": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", - "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "node_modules/@wordpress/base-styles": { + "version": "6.18.0", + "resolved": "https://registry.npmjs.org/@wordpress/base-styles/-/base-styles-6.18.0.tgz", + "integrity": "sha512-c9C8gE49uFsR6S8zmfhH8xFR8FrrkpO289sscv5jRABHeH21irwP/yGuEbkJiUqIqV9Rm2+HbQay4+F5M8DYfA==", "dev": true, - "requires": { - "cipher-base": "^1.0.1", - "inherits": "^2.0.1", - "md5.js": "^1.3.4", - "ripemd160": "^2.0.1", - "sha.js": "^2.4.0" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "create-hmac": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", - "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "node_modules/@wordpress/browserslist-config": { + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/browserslist-config/-/browserslist-config-6.42.0.tgz", + "integrity": "sha512-nof8KS4I8lqopdIaAa+Cqz6UtM3x09MpeAH2JWsP2GcczPudClCju67unQGVgsHKXJqAjYtFpx4GfVYn+Rtr/w==", "dev": true, - "requires": { - "cipher-base": "^1.0.3", - "create-hash": "^1.1.0", - "inherits": "^2.0.1", - "ripemd160": "^2.0.0", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "cross-spawn": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", - "integrity": "sha1-6L0O/uWPz/b4+UUQoKVUu/ojVEk=", + "node_modules/@wordpress/dependency-extraction-webpack-plugin": { + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/dependency-extraction-webpack-plugin/-/dependency-extraction-webpack-plugin-6.42.0.tgz", + "integrity": "sha512-C00CqmuCHbKRsh7zXD0jlSnPhuW6nVF02xxkqXXX9AEo1FkvYhaBdQ1Plas1V+fuk47+lIktPg04FiaX6J4Tlg==", "dev": true, - "requires": { - "lru-cache": "^4.0.1", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - }, + "license": "GPL-2.0-or-later", "dependencies": { - "lru-cache": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", - "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", - "dev": true, - "requires": { - "pseudomap": "^1.0.2", - "yallist": "^2.1.2" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yallist": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", - "integrity": "sha1-HBH5IY8HYImkfdUS+TxmmaaoHVI=", - "dev": true - } + "json2php": "^0.0.7" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "crypto-browserify": { - "version": "3.12.0", - "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.0.tgz", - "integrity": "sha512-fz4spIh+znjO2VjL+IdhEpRJ3YN6sMzITSBijk6FK2UvTqruSQW+/cCZTSNsMiZNvUeq0CqurF+dAbyiGOY6Wg==", + "node_modules/@wordpress/dependency-extraction-webpack-plugin/node_modules/json2php": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.7.tgz", + "integrity": "sha512-dnSoUiLAoVaMXxFsVi4CrPVYMKOuDBXTghXSmMINX44RZ8WM9cXlY7UqrQnlAcODCVO7FV3+8t/5nDKAjimLfg==", "dev": true, - "requires": { - "browserify-cipher": "^1.0.0", - "browserify-sign": "^4.0.0", - "create-ecdh": "^4.0.0", - "create-hash": "^1.1.0", - "create-hmac": "^1.1.0", - "diffie-hellman": "^5.0.0", - "inherits": "^2.0.1", - "pbkdf2": "^3.0.3", - "public-encrypt": "^4.0.0", - "randombytes": "^2.0.0", - "randomfill": "^1.0.3" + "license": "BSD" + }, + "node_modules/@wordpress/e2e-test-utils-playwright": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/e2e-test-utils-playwright/-/e2e-test-utils-playwright-1.42.0.tgz", + "integrity": "sha512-IN5OK4QTymZxnUzOswK52y/YfHecmiMh+09LcVglxfqHecFgRqd40j1BcNv/7oFIlah6jRO74zC0bqKXX5fw/w==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "change-case": "^4.1.2", + "get-port": "^5.1.1", + "lighthouse": "^12.2.2", + "mime": "^3.0.0", + "web-vitals": "^4.2.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": ">=1", + "@types/node": "^20.17.10" } }, - "cson-parser": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-1.3.5.tgz", - "integrity": "sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ=", + "node_modules/@wordpress/e2e-test-utils-playwright/node_modules/mime": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-3.0.0.tgz", + "integrity": "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A==", "dev": true, - "requires": { - "coffee-script": "^1.10.0" + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=10.0.0" } }, - "css-color-names": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/css-color-names/-/css-color-names-0.0.4.tgz", - "integrity": "sha1-gIrcLnnPhHOAabZGyyDsJ762KeA=", - "dev": true + "node_modules/@wordpress/element": { + "version": "6.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/element/-/element-6.42.0.tgz", + "integrity": "sha512-aSuifJL9MF0xrAynWSWxIuhgagJcVwSWrqIpLwX0DZasQ0LKsJe08SmuDe/z3sgOymGG6cOd/GHv0fLwQe8VFQ==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@types/react": "^18.3.27", + "@types/react-dom": "^18.3.1", + "@wordpress/escape-html": "^3.42.0", + "change-case": "^4.1.2", + "is-plain-object": "^5.0.0", + "react": "^18.3.0", + "react-dom": "^18.3.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + } }, - "css-declaration-sorter": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-4.0.1.tgz", - "integrity": "sha512-BcxQSKTSEEQUftYpBVnsH4SF05NTuBokb19/sBt6asXGKZ/6VP7PLG1CBCkFDYOnhXhPh0jMhO6xZ71oYHXHBA==", + "node_modules/@wordpress/escape-html": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/escape-html/-/escape-html-3.42.0.tgz", + "integrity": "sha512-dykrMeKAxhwfEImrXfTqKREYGJP2qVIU8q3daUNyNLzrOdwhulAlBzUWXH9zYyY5qEQWrWsnjq4M9f77dO0p4w==", "dev": true, - "requires": { - "postcss": "^7.0.1", - "timsort": "^0.3.0" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "css-loader": { - "version": "5.2.7", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-5.2.7.tgz", - "integrity": "sha512-Q7mOvpBNBG7YrVGMxRxcBJZFL75o+cH2abNASdibkj/fffYD8qWbInZrD0S9ccI6vZclF3DsHE7njGlLtaHbhg==", + "node_modules/@wordpress/eslint-plugin": { + "version": "24.4.0", + "resolved": "https://registry.npmjs.org/@wordpress/eslint-plugin/-/eslint-plugin-24.4.0.tgz", + "integrity": "sha512-qh/2CWsXNpnC4ROxajh6T50WwwV87fIBkKliuBO1G0DMdxVwiXSDaLRqSxn+mOLAyw4q5VdlL4A5R8a/0UMjQw==", "dev": true, - "requires": { - "icss-utils": "^5.1.0", - "loader-utils": "^2.0.0", - "postcss": "^8.2.15", - "postcss-modules-extract-imports": "^3.0.0", - "postcss-modules-local-by-default": "^4.0.0", - "postcss-modules-scope": "^3.0.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.1.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.5" - }, + "license": "GPL-2.0-or-later", "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "colorette": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.3.0.tgz", - "integrity": "sha512-ecORCqbSFP7Wm8Y6lyqMJjexBQqXSF7SSeaTyGGphogUjBlFP9m9o08wy86HL2uB7fMTxtOUzLMk7ogKcxMg1w==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "postcss": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.3.6.tgz", - "integrity": "sha512-wG1cc/JhRgdqB6WHEuyLTedf3KIRuD0hG6ldkFEZNCjRxiC+3i6kkWUUbiJQayP28iwG35cEmAbe98585BYV0A==", - "dev": true, - "requires": { - "colorette": "^1.2.2", - "nanoid": "^3.1.23", - "source-map-js": "^0.6.2" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } + "@babel/eslint-parser": "7.25.7", + "@typescript-eslint/eslint-plugin": "^6.4.1", + "@typescript-eslint/parser": "^6.4.1", + "@wordpress/babel-preset-default": "^8.42.0", + "@wordpress/prettier-config": "^4.42.0", + "@wordpress/theme": "^0.9.0", + "cosmiconfig": "^7.0.0", + "eslint-config-prettier": "^8.3.0", + "eslint-import-resolver-typescript": "^4.4.4", + "eslint-plugin-import": "^2.25.2", + "eslint-plugin-jest": "^27.4.3", + "eslint-plugin-jsdoc": "^46.4.6", + "eslint-plugin-jsx-a11y": "^6.5.1", + "eslint-plugin-playwright": "^0.15.3", + "eslint-plugin-prettier": "^5.0.0", + "eslint-plugin-react": "^7.27.0", + "eslint-plugin-react-hooks": "^4.3.0", + "globals": "^13.12.0", + "requireindex": "^1.2.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "eslint": ">=8", + "prettier": ">=3", + "typescript": ">=5" + }, + "peerDependenciesMeta": { + "prettier": { + "optional": true }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } + "typescript": { + "optional": true } } }, - "css-mediaquery": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/css-mediaquery/-/css-mediaquery-0.1.2.tgz", - "integrity": "sha1-aiw3NEkoYYYxxUvTPO3TAdoYvqA=" + "node_modules/@wordpress/eslint-plugin/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "css-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", - "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "node_modules/@wordpress/jest-console": { + "version": "8.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-console/-/jest-console-8.42.0.tgz", + "integrity": "sha512-eLU7HO5VMt5LEIL+fTkuF8mcbLVNtTa0WXWZDcTPnPv4PSDH/QUGEJZ7QccF+cJ0Xj8np6G/xZWRs7VXlJYycg==", "dev": true, - "requires": { - "boolbase": "^1.0.0", - "css-what": "^3.2.1", - "domutils": "^1.7.0", - "nth-check": "^1.0.2" + "license": "GPL-2.0-or-later", + "dependencies": { + "jest-matcher-utils": "^29.6.2", + "jest-mock": "^29.6.2" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "jest": ">=29" } }, - "css-select-base-adapter": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", - "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", - "dev": true + "node_modules/@wordpress/jest-preset-default": { + "version": "12.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/jest-preset-default/-/jest-preset-default-12.42.0.tgz", + "integrity": "sha512-yTFeblOQORtQ77T4l2LqWf2IO4j65rpX2ekaQTR7cWKCbA/HOpuwK3LYHPN8Pq2gfXrCoNC68QEgGoi4i2oHAw==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/jest-console": "^8.42.0", + "babel-jest": "29.7.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@babel/core": ">=7", + "jest": ">=29" + } }, - "css-tree": { - "version": "1.0.0-alpha.37", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", - "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", + "node_modules/@wordpress/npm-package-json-lint-config": { + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/npm-package-json-lint-config/-/npm-package-json-lint-config-5.42.0.tgz", + "integrity": "sha512-Kjvf5M0NNwuAwsxKRY2hB16QJ7BPfd+NjDyYMlSCpzUXtg94Eo8DdzNnKcyZXVIQynKdQCDGLx84DN2wr60K3A==", "dev": true, - "requires": { - "mdn-data": "2.0.4", - "source-map": "^0.6.1" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "peerDependencies": { + "npm-package-json-lint": ">=6.0.0" } }, - "css-what": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", - "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", - "dev": true - }, - "cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "dev": true - }, - "cssnano": { - "version": "4.1.11", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-4.1.11.tgz", - "integrity": "sha512-6gZm2htn7xIPJOHY824ERgj8cNPgPxyCSnkXc4v7YvNW+TdVfzgngHcEhy/8D11kUWRUMbke+tC+AUcUsnMz2g==", - "dev": true, - "requires": { - "cosmiconfig": "^5.0.0", - "cssnano-preset-default": "^4.0.8", - "is-resolvable": "^1.0.0", - "postcss": "^7.0.0" - }, - "dependencies": { - "cosmiconfig": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-5.2.1.tgz", - "integrity": "sha512-H65gsXo1SKjf8zmrJ67eJk8aIRKV5ff2D4uKZIBZShbhGSpEmsQOPW/SKMKYhSTrqR7ufy6RP69rPogdaPh/kA==", - "dev": true, - "requires": { - "import-fresh": "^2.0.0", - "is-directory": "^0.3.1", - "js-yaml": "^3.13.1", - "parse-json": "^4.0.0" - } - }, - "import-fresh": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-2.0.0.tgz", - "integrity": "sha1-2BNVwVYS04bGH53dOSLUMEgipUY=", - "dev": true, - "requires": { - "caller-path": "^2.0.0", - "resolve-from": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - } + "node_modules/@wordpress/postcss-plugins-preset": { + "version": "5.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/postcss-plugins-preset/-/postcss-plugins-preset-5.42.0.tgz", + "integrity": "sha512-hXCJdLX3R9bWp11DE90ZzTE+1jf/J9KlFkP3qHwZSY+5TP3hWb4HEvdnWSFkWDiBqlzFZK2SEvzheyMjPsq+3g==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@wordpress/base-styles": "^6.18.0", + "autoprefixer": "^10.4.20", + "postcss-import": "^16.1.1" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "postcss": "^8.0.0" } }, - "cssnano-preset-default": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-4.0.8.tgz", - "integrity": "sha512-LdAyHuq+VRyeVREFmuxUZR1TXjQm8QQU/ktoo/x7bz+SdOge1YKc5eMN6pRW7YWBmyq59CqYba1dJ5cUukEjLQ==", - "dev": true, - "requires": { - "css-declaration-sorter": "^4.0.1", - "cssnano-util-raw-cache": "^4.0.1", - "postcss": "^7.0.0", - "postcss-calc": "^7.0.1", - "postcss-colormin": "^4.0.3", - "postcss-convert-values": "^4.0.1", - "postcss-discard-comments": "^4.0.2", - "postcss-discard-duplicates": "^4.0.2", - "postcss-discard-empty": "^4.0.1", - "postcss-discard-overridden": "^4.0.1", - "postcss-merge-longhand": "^4.0.11", - "postcss-merge-rules": "^4.0.3", - "postcss-minify-font-values": "^4.0.2", - "postcss-minify-gradients": "^4.0.2", - "postcss-minify-params": "^4.0.2", - "postcss-minify-selectors": "^4.0.2", - "postcss-normalize-charset": "^4.0.1", - "postcss-normalize-display-values": "^4.0.2", - "postcss-normalize-positions": "^4.0.2", - "postcss-normalize-repeat-style": "^4.0.2", - "postcss-normalize-string": "^4.0.2", - "postcss-normalize-timing-functions": "^4.0.2", - "postcss-normalize-unicode": "^4.0.1", - "postcss-normalize-url": "^4.0.1", - "postcss-normalize-whitespace": "^4.0.2", - "postcss-ordered-values": "^4.1.2", - "postcss-reduce-initial": "^4.0.3", - "postcss-reduce-transforms": "^4.0.2", - "postcss-svgo": "^4.0.3", - "postcss-unique-selectors": "^4.0.1" - } - }, - "cssnano-util-get-arguments": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-arguments/-/cssnano-util-get-arguments-4.0.0.tgz", - "integrity": "sha1-7ToIKZ8h11dBsg87gfGU7UnMFQ8=", - "dev": true - }, - "cssnano-util-get-match": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/cssnano-util-get-match/-/cssnano-util-get-match-4.0.0.tgz", - "integrity": "sha1-wOTKB/U4a7F+xeUiULT1lhNlFW0=", - "dev": true + "node_modules/@wordpress/prettier-config": { + "version": "4.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/prettier-config/-/prettier-config-4.42.0.tgz", + "integrity": "sha512-YoOlVxDMZ02+Eg8N9OVItikOLnpLd6C4mi/QwJvlKS7b9sKAQe+ekBugj6y/9w1PkODhMM+Dvxj+dGWq/8TTyA==", + "dev": true, + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "prettier": ">=3" + } }, - "cssnano-util-raw-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-raw-cache/-/cssnano-util-raw-cache-4.0.1.tgz", - "integrity": "sha512-qLuYtWK2b2Dy55I8ZX3ky1Z16WYsx544Q0UWViebptpwn/xDBmog2TLg4f+DBMg1rJ6JDWtn96WHbOKDWt1WQA==", + "node_modules/@wordpress/private-apis": { + "version": "1.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/private-apis/-/private-apis-1.42.0.tgz", + "integrity": "sha512-IDpyCszdnBECvkejn2vyGPHn4aWtROFq0yFaVGPAvYCadnlpWsQC0oJodppBOE7sftYiIDnTw6/rnv+mp29/Kg==", "dev": true, - "requires": { - "postcss": "^7.0.0" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "cssnano-util-same-parent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/cssnano-util-same-parent/-/cssnano-util-same-parent-4.0.1.tgz", - "integrity": "sha512-WcKx5OY+KoSIAxBW6UBBRay1U6vkYheCdjyVNDm85zt5K9mHoGOfsOsqIszfAqrQQFIIKgjh2+FDgIj/zsl21Q==", - "dev": true + "node_modules/@wordpress/scripts": { + "version": "31.7.0", + "resolved": "https://registry.npmjs.org/@wordpress/scripts/-/scripts-31.7.0.tgz", + "integrity": "sha512-Gat1EFwIPPH3qvpWeip85fCRFP20sk7RTlmqhumbdWR+nIjHK41lz3k2W4zHbneloo96f6coWLJz1dAtwj+lSg==", + "dev": true, + "license": "GPL-2.0-or-later", + "dependencies": { + "@babel/core": "7.25.7", + "@pmmmwh/react-refresh-webpack-plugin": "^0.5.11", + "@svgr/webpack": "^8.0.1", + "@wordpress/babel-preset-default": "^8.42.0", + "@wordpress/browserslist-config": "^6.42.0", + "@wordpress/dependency-extraction-webpack-plugin": "^6.42.0", + "@wordpress/e2e-test-utils-playwright": "^1.42.0", + "@wordpress/eslint-plugin": "^24.4.0", + "@wordpress/jest-preset-default": "^12.42.0", + "@wordpress/npm-package-json-lint-config": "^5.42.0", + "@wordpress/postcss-plugins-preset": "^5.42.0", + "@wordpress/prettier-config": "^4.42.0", + "@wordpress/stylelint-config": "^23.34.0", + "adm-zip": "^0.5.9", + "babel-jest": "29.7.0", + "babel-loader": "9.2.1", + "browserslist": "^4.21.10", + "chalk": "^4.0.0", + "check-node-version": "^4.1.0", + "copy-webpack-plugin": "^10.2.0", + "cross-spawn": "^7.0.6", + "css-loader": "^6.2.0", + "cssnano": "^6.0.1", + "cwd": "^0.10.0", + "dir-glob": "^3.0.1", + "eslint": "^8.57.1", + "expect-puppeteer": "^4.4.0", + "fast-glob": "^3.2.7", + "filenamify": "^4.2.0", + "jest": "^29.6.2", + "jest-dev-server": "^10.1.4", + "jest-environment-jsdom": "^30.2.0", + "jest-environment-node": "^29.6.2", + "json2php": "^0.0.9", + "markdownlint-cli": "^0.31.1", + "merge-deep": "^3.0.3", + "mini-css-extract-plugin": "^2.9.2", + "minimist": "^1.2.0", + "npm-package-json-lint": "^6.4.0", + "npm-packlist": "^3.0.0", + "postcss": "^8.4.5", + "postcss-loader": "^6.2.1", + "prettier": "npm:wp-prettier@3.0.3", + "puppeteer-core": "^23.10.1", + "react-refresh": "^0.14.0", + "read-pkg-up": "^7.0.1", + "resolve-bin": "^0.4.0", + "rtlcss": "^4.3.0", + "sass": "^1.54.0", + "sass-loader": "^16.0.3", + "schema-utils": "^4.2.0", + "source-map-loader": "^3.0.0", + "stylelint": "^16.8.2", + "terser-webpack-plugin": "^5.3.10", + "url-loader": "^4.1.1", + "webpack": "^5.97.0", + "webpack-bundle-analyzer": "^4.9.1", + "webpack-cli": "^5.1.4", + "webpack-dev-server": "^4.15.1" + }, + "bin": { + "wp-scripts": "bin/wp-scripts.js" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "@playwright/test": "^1.58.2", + "@wordpress/env": ">=10.0.0", + "react": "^18.0.0", + "react-dom": "^18.0.0" + }, + "peerDependenciesMeta": { + "@wordpress/env": { + "optional": true + } + } }, - "csso": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", - "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "node_modules/@wordpress/scripts/node_modules/@pmmmwh/react-refresh-webpack-plugin": { + "version": "0.5.17", + "resolved": "https://registry.npmjs.org/@pmmmwh/react-refresh-webpack-plugin/-/react-refresh-webpack-plugin-0.5.17.tgz", + "integrity": "sha512-tXDyE1/jzFsHXjhRZQ3hMl0IVhYe5qula43LDWIhVfjp9G/nT5OQY5AORVOrkEGAUltBJOfOWeETbmhm6kHhuQ==", "dev": true, - "requires": { - "css-tree": "^1.1.2" - }, + "license": "MIT", "dependencies": { - "css-tree": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.2.tgz", - "integrity": "sha512-wCoWush5Aeo48GLhfHPbmvZs59Z+M7k5+B1xDnXbdWNcEF423DoFdqSWE0PM5aNk5nI5cp1q7ms36zGApY/sKQ==", - "dev": true, - "requires": { - "mdn-data": "2.0.14", - "source-map": "^0.6.1" - } + "ansi-html": "^0.0.9", + "core-js-pure": "^3.23.3", + "error-stack-parser": "^2.0.6", + "html-entities": "^2.1.0", + "loader-utils": "^2.0.4", + "schema-utils": "^4.2.0", + "source-map": "^0.7.3" + }, + "engines": { + "node": ">= 10.13" + }, + "peerDependencies": { + "@types/webpack": "4.x || 5.x", + "react-refresh": ">=0.10.0 <1.0.0", + "sockjs-client": "^1.4.0", + "type-fest": ">=0.17.0 <5.0.0", + "webpack": ">=4.43.0 <6.0.0", + "webpack-dev-server": "3.x || 4.x || 5.x", + "webpack-hot-middleware": "2.x", + "webpack-plugin-serve": "0.x || 1.x" + }, + "peerDependenciesMeta": { + "@types/webpack": { + "optional": true + }, + "sockjs-client": { + "optional": true + }, + "type-fest": { + "optional": true + }, + "webpack-dev-server": { + "optional": true }, - "mdn-data": { - "version": "2.0.14", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", - "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", - "dev": true + "webpack-hot-middleware": { + "optional": true }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "webpack-plugin-serve": { + "optional": true } } }, - "cssom": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.4.4.tgz", - "integrity": "sha512-p3pvU7r1MyyqbTk+WbNJIgJjG2VmTIaB10rI93LzVPrmDJKkzKYMtxxyAvQXR/NS6otuzveI7+7BBq3SjBS2mw==", - "dev": true - }, - "cssstyle": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-2.3.0.tgz", - "integrity": "sha512-AZL67abkUzIuvcHqk7c09cezpGNcxUxU4Ioi/05xHk4DQeTkWmGYftIE6ctU6AEt+Gn4n1lDStOtj7FKycP71A==", + "node_modules/@wordpress/scripts/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "cssom": "~0.3.6" - }, + "license": "MIT", "dependencies": { - "cssom": { - "version": "0.3.8", - "resolved": "https://registry.npmjs.org/cssom/-/cssom-0.3.8.tgz", - "integrity": "sha512-b0tGHbfegbhPJpxpiBPU2sCkigAqtM9O121le6bbOlgyV+NyGyCmVfJ6QW9eRjz8CpNfWEOYBIMIGRYkLwsIYg==", - "dev": true - } + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "csstype": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.0.8.tgz", - "integrity": "sha512-jXKhWqXPmlUeoQnF/EhTtTl4C9SnrxSH/jZUih3jmO6lBKr99rP3/+FmrMj4EFpOXzMtXHAZkd3x0E6h6Fgflw==" - }, - "current-script-polyfill": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/current-script-polyfill/-/current-script-polyfill-1.0.0.tgz", - "integrity": "sha1-8xz35PPiGLBybnOMqSoC00iO9hU=" - }, - "currently-unhandled": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", - "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "node_modules/@wordpress/scripts/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "optional": true, - "requires": { - "array-find-index": "^1.0.1" + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "custom-event": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", - "integrity": "sha1-XQKkaFCt8bSjF5RqOSj8y1v9BCU=" - }, - "cwd": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", - "integrity": "sha1-FyQAaUBXwioTsM8WFix+S3p/5Wc=", + "node_modules/@wordpress/scripts/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "find-pkg": "^0.1.2", - "fs-exists-sync": "^0.1.0" + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "cyclist": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/cyclist/-/cyclist-1.0.1.tgz", - "integrity": "sha1-WW6WmP0MgOEgOMK4LW6xs1tiJNk=", - "dev": true - }, - "damerau-levenshtein": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.7.tgz", - "integrity": "sha512-VvdQIPGdWP0SqFXghj79Wf/5LArmreyMsGLa6FG6iC4t3j7j5s71TrwWmT/4akbDQIqjfACkLZmjXhA7g2oUZw==", - "dev": true - }, - "dashdash": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", - "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", + "node_modules/@wordpress/scripts/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { - "assert-plus": "^1.0.0" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "data-urls": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-2.0.0.tgz", - "integrity": "sha512-X5eWTSXO/BJmpdIKCRuKUgSCgAN0OwliVK3yPKbwIWU1Tdw5BRajxlzMidvh+gwko9AfQ9zIj52pzF91Q3YAvQ==", + "node_modules/@wordpress/scripts/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "abab": "^2.0.3", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.0.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "date-format": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-3.0.0.tgz", - "integrity": "sha512-eyTcpKOcamdhWJXj56DpQMo1ylSQpcGtGKXcU0Tb97+K56/CF5amAqqqNj0+KvA0iw2ynxtHWFsPDSClCxe48w==" - }, - "date-now": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", - "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", - "dev": true - }, - "dateformat": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz", - "integrity": "sha512-jyCETtSl3VMZMWeRo7iY1FL19ges1t55hMo5yaam4Jrsm5EPL89UQkoQRyiI+Yf4k8r2ZpdngkV8hr1lIdjb3Q==", + "node_modules/@wordpress/scripts/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "debug": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", - "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", - "requires": { - "ms": "^2.1.1" + "node_modules/@wordpress/scripts/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" } }, - "decamelize": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", - "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=" - }, - "decamelize-keys": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.0.tgz", - "integrity": "sha1-0XGoeTMlKAfrPLYdwcFEXQeN8tk=", + "node_modules/@wordpress/scripts/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", "dev": true, - "requires": { - "decamelize": "^1.1.0", - "map-obj": "^1.0.0" + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" } }, - "decimal.js": { - "version": "10.3.1", - "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.3.1.tgz", - "integrity": "sha512-V0pfhfr8suzyPGOx3nmq4aHqabehUZn6Ch9kyFpV79TGDTWFmHqUqXdabR7QHqxzrYolF4+tVmJhUG4OURg5dQ==", - "dev": true - }, - "decode-uri-component": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz", - "integrity": "sha1-6zkTMzRYd1y4TNGh+uBiEGu4dUU=", - "dev": true - }, - "decompress": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", - "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "node_modules/@wordpress/scripts/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, - "optional": true, - "requires": { - "decompress-tar": "^4.0.0", - "decompress-tarbz2": "^4.0.0", - "decompress-targz": "^4.0.0", - "decompress-unzip": "^4.0.1", - "graceful-fs": "^4.1.10", - "make-dir": "^1.0.0", - "pify": "^2.3.0", - "strip-dirs": "^2.0.0" - }, + "license": "BSD-2-Clause", "dependencies": { - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "optional": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true - } + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "decompress-response": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", - "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "node_modules/@wordpress/scripts/node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", "dev": true, - "optional": true, - "requires": { - "mimic-response": "^1.0.0" + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "decompress-tar": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", - "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "node_modules/@wordpress/scripts/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "optional": true, - "requires": { - "file-type": "^5.2.0", - "is-stream": "^1.1.0", - "tar-stream": "^1.5.2" + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" }, - "dependencies": { - "bl": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", - "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", - "dev": true, - "optional": true, - "requires": { - "readable-stream": "^2.3.5", - "safe-buffer": "^5.1.1" - } - }, - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true - }, - "tar-stream": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", - "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", - "dev": true, - "optional": true, - "requires": { - "bl": "^1.0.0", - "buffer-alloc": "^1.2.0", - "end-of-stream": "^1.0.0", - "fs-constants": "^1.0.0", - "readable-stream": "^2.3.0", - "to-buffer": "^1.1.1", - "xtend": "^4.0.0" - } - } + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "decompress-tarbz2": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", - "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "node_modules/@wordpress/scripts/node_modules/cssnano": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", + "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", "dev": true, - "optional": true, - "requires": { - "decompress-tar": "^4.1.0", - "file-type": "^6.1.0", - "is-stream": "^1.1.0", - "seek-bzip": "^1.0.5", - "unbzip2-stream": "^1.0.9" - }, "dependencies": { - "file-type": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", - "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", - "dev": true, - "optional": true - } + "cssnano-preset-default": "^6.1.2", + "lilconfig": "^3.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "decompress-targz": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", - "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "node_modules/@wordpress/scripts/node_modules/cssnano-preset-default": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", + "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", "dev": true, - "optional": true, - "requires": { - "decompress-tar": "^4.1.1", - "file-type": "^5.2.0", - "is-stream": "^1.1.0" - }, "dependencies": { - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true - } + "browserslist": "^4.23.0", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^4.0.2", + "postcss-calc": "^9.0.1", + "postcss-colormin": "^6.1.0", + "postcss-convert-values": "^6.1.0", + "postcss-discard-comments": "^6.0.2", + "postcss-discard-duplicates": "^6.0.3", + "postcss-discard-empty": "^6.0.3", + "postcss-discard-overridden": "^6.0.2", + "postcss-merge-longhand": "^6.0.5", + "postcss-merge-rules": "^6.1.1", + "postcss-minify-font-values": "^6.1.0", + "postcss-minify-gradients": "^6.0.3", + "postcss-minify-params": "^6.1.0", + "postcss-minify-selectors": "^6.0.4", + "postcss-normalize-charset": "^6.0.2", + "postcss-normalize-display-values": "^6.0.2", + "postcss-normalize-positions": "^6.0.2", + "postcss-normalize-repeat-style": "^6.0.2", + "postcss-normalize-string": "^6.0.2", + "postcss-normalize-timing-functions": "^6.0.2", + "postcss-normalize-unicode": "^6.1.0", + "postcss-normalize-url": "^6.0.2", + "postcss-normalize-whitespace": "^6.0.2", + "postcss-ordered-values": "^6.0.2", + "postcss-reduce-initial": "^6.1.0", + "postcss-reduce-transforms": "^6.0.2", + "postcss-svgo": "^6.0.3", + "postcss-unique-selectors": "^6.0.4" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "decompress-unzip": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", - "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "node_modules/@wordpress/scripts/node_modules/cssnano-utils": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", + "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", "dev": true, - "optional": true, - "requires": { - "file-type": "^3.8.0", - "get-stream": "^2.2.0", - "pify": "^2.3.0", - "yauzl": "^2.4.2" + "engines": { + "node": "^14 || ^16 || >=18.0" }, - "dependencies": { - "file-type": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", - "integrity": "sha1-JXoHg4TR24CHvESdEH1SpSZyuek=", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", - "integrity": "sha1-Xzj5PzRgCWZu4BUKBUFn+Rvdld4=", - "dev": true, - "optional": true, - "requires": { - "object-assign": "^4.0.1", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true - } + "peerDependencies": { + "postcss": "^8.4.31" } }, - "dedent": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/dedent/-/dedent-0.7.0.tgz", - "integrity": "sha1-JJXduvbrh0q7Dhvp3yLS5aVEMmw=", - "dev": true + "node_modules/@wordpress/scripts/node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } }, - "deep-equal": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.1.1.tgz", - "integrity": "sha512-yd9c5AdiqVcR+JjcwUQb9DkhJc8ngNr0MahEBGvDiJw8puWab2yZlh+nkasOnZP+EGTAP6rRp2JzJhJZzvNF8g==", - "dev": true, - "requires": { - "is-arguments": "^1.0.4", - "is-date-object": "^1.0.1", - "is-regex": "^1.0.4", - "object-is": "^1.0.1", - "object-keys": "^1.1.1", - "regexp.prototype.flags": "^1.2.0" + "node_modules/@wordpress/scripts/node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "deep-extend": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.5.1.tgz", - "integrity": "sha512-N8vBdOa+DF7zkRrDCsaOXoCs/E2fJfx9B9MrKnnSiHNh4ws7eSys6YQE4KvT1cecKmOASYQBhbKjeuDD9lT81w==", + "node_modules/@wordpress/scripts/node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", "dev": true }, - "deep-for-each": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/deep-for-each/-/deep-for-each-3.0.0.tgz", - "integrity": "sha512-pPN+0f8jlnNP+z90qqOdxGghJU5XM6oBDhvAR+qdQzjCg5pk/7VPPvKK1GqoXEFkHza6ZS+Otzzvmr0g3VUaKw==", + "node_modules/@wordpress/scripts/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "requires": { - "lodash.isplainobject": "^4.0.6" + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "deep-is": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", - "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=" - }, - "deepmerge": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-1.5.2.tgz", - "integrity": "sha512-95k0GDqvBjZavkuvzx/YqVLv/6YYa17fz6ILMSf7neqQITCPbnfEnQvEgMPNjH4kgobe7+WIL0yJEHku+H3qtQ==" + "node_modules/@wordpress/scripts/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] }, - "default-gateway": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-4.2.0.tgz", - "integrity": "sha512-h6sMrVB1VMWVrW13mSc6ia/DwYYw5MN6+exNu1OaJeFac5aSAvwM7lZ0NVfTABuSkQelr4h5oebg3KB1XPdjgA==", + "node_modules/@wordpress/scripts/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, - "requires": { - "execa": "^1.0.0", - "ip-regex": "^2.1.0" + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" } }, - "define-properties": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.3.tgz", - "integrity": "sha512-3MqfYKj2lLzdMSf8ZIZE/V+Zuy+BgD6f164e8K2w7dgnpKArBDerGYpM46IYYcjnkdPNMjPk9A6VFB8+3SKlXQ==", - "requires": { - "object-keys": "^1.0.12" + "node_modules/@wordpress/scripts/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "define-property": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", - "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "node_modules/@wordpress/scripts/node_modules/filenamify": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-4.3.0.tgz", + "integrity": "sha512-hcFKyUG57yWGAzu1CMt/dPzYZuv+jAJUT85bL8mrXvNe6hWj6yEHEc4EdcgiA6Z3oi1/9wXJdZPXF2dZNgwgOg==", "dev": true, - "requires": { - "is-descriptor": "^1.0.2", - "isobject": "^3.0.1" - }, "dependencies": { - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.1", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "del": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/del/-/del-4.1.1.tgz", - "integrity": "sha512-QwGuEUouP2kVwQenAsOof5Fv8K9t3D8Ca8NxcXKrIpEHjTXK5J2nXLdP+ALI1cgv8wj7KuwBhTwBkOZSJKM5XQ==", - "dev": true, - "requires": { - "@types/glob": "^7.1.1", - "globby": "^6.1.0", - "is-path-cwd": "^2.0.0", - "is-path-in-cwd": "^2.0.0", - "p-map": "^2.0.0", - "pify": "^4.0.1", - "rimraf": "^2.6.3" + "node_modules/@wordpress/scripts/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "delayed-stream": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", - "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", - "dev": true - }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" - }, - "depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=" - }, - "des.js": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.0.1.tgz", - "integrity": "sha512-Q0I4pfFrv2VPd34/vfLrFOoRmlYj3OV50i7fskps1jZWK1kApMWWT9G6RRUeYedLcBDIhnSDaUvJMb3AhUlaEA==", + "node_modules/@wordpress/scripts/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "minimalistic-assert": "^1.0.0" + "engines": { + "node": ">=8" } }, - "desandro-matches-selector": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz", - "integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE=" - }, - "destroy": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.0.4.tgz", - "integrity": "sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=", - "dev": true - }, - "detect-file": { + "node_modules/@wordpress/scripts/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", - "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", - "dev": true - }, - "detect-newline": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", - "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", - "dev": true - }, - "detect-node": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.0.4.tgz", - "integrity": "sha512-ZIzRpLJrOj7jjP2miAtgqIfmzbxa4ZOr5jJc601zklsfEx9oTzmmj2nVpIPRpNlRTIh8lc1kyViIY7BWSGNmKw==", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "devtools-protocol": { - "version": "0.0.869402", - "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.869402.tgz", - "integrity": "sha512-VvlVYY+VDJe639yHs5PHISzdWTLL3Aw8rO4cvUtwvoxFd6FHbE4OpHHcde52M6096uYYazAmd4l0o5VuFRO2WA==", - "dev": true - }, - "di": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", - "integrity": "sha1-gGZJMmzqp8qjMG112YXqJ0i6kTw=" + "node_modules/@wordpress/scripts/node_modules/json2php": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.9.tgz", + "integrity": "sha512-fQMYwvPsQt8hxRnCGyg1r2JVi6yL8Um0DIIawiKiMK9yhAAkcRNj5UsBWoaFvFzPpcWbgw9L6wzj+UMYA702Mw==", + "dev": true, + "license": "BSD" }, - "diff": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.2.tgz", - "integrity": "sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==" + "node_modules/@wordpress/scripts/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } }, - "diff-sequences": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-26.6.2.tgz", - "integrity": "sha512-Mv/TDa3nZ9sbc5soK+OoA74BsS3mL37yixCvUAQkiuA4Wz6YtwP/K47n2rv2ovzHZvoiQeA5FTQOschKkEwB0Q==", + "node_modules/@wordpress/scripts/node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", "dev": true }, - "diffie-hellman": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", - "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "node_modules/@wordpress/scripts/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "requires": { - "bn.js": "^4.1.0", - "miller-rabin": "^4.0.0", - "randombytes": "^2.0.0" - }, "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "node_modules/@wordpress/scripts/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "requires": { - "path-type": "^4.0.0" + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "direction": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/direction/-/direction-1.0.4.tgz", - "integrity": "sha512-GYqKi1aH7PJXxdhTeZBFrg8vUBeKXi+cNprXsC1kpJcbcVnV9wBsrOu1cQEdG0WeQwlfHiy3XvnKfIrJ2R0NzQ==" - }, - "discontinuous-range": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/discontinuous-range/-/discontinuous-range-1.0.0.tgz", - "integrity": "sha1-44Mx8IRLukm5qctxx3FYWqsbxlo=", - "dev": true - }, - "dns-equal": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/dns-equal/-/dns-equal-1.0.0.tgz", - "integrity": "sha1-s55/HabrCnW6nBcySzR1PEfgZU0=", - "dev": true - }, - "dns-packet": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-1.3.4.tgz", - "integrity": "sha512-BQ6F4vycLXBvdrJZ6S3gZewt6rcrks9KBgM9vrhW+knGRqc8uEdT7fuCwloc7nny5xNoMJ17HGH0R/6fpo8ECA==", + "node_modules/@wordpress/scripts/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "requires": { - "ip": "^1.1.0", - "safe-buffer": "^5.0.1" + "engines": { + "node": ">=8" } }, - "dns-txt": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/dns-txt/-/dns-txt-2.0.2.tgz", - "integrity": "sha1-uR2Ab10nGI5Ks+fRB9iBocxGQrY=", + "node_modules/@wordpress/scripts/node_modules/postcss-calc": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", + "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", "dev": true, - "requires": { - "buffer-indexof": "^1.0.0" + "dependencies": { + "postcss-selector-parser": "^6.0.11", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.2.2" } }, - "doctrine": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", - "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "node_modules/@wordpress/scripts/node_modules/postcss-colormin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", + "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", "dev": true, - "requires": { - "esutils": "^2.0.2" + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "document.contains": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/document.contains/-/document.contains-1.0.2.tgz", - "integrity": "sha512-YcvYFs15mX8m3AO1QNQy3BlIpSMfNRj3Ujk2BEJxsZG+HZf7/hZ6jr7mDpXrF8q+ff95Vef5yjhiZxm8CGJr6Q==", - "requires": { - "define-properties": "^1.1.3" + "node_modules/@wordpress/scripts/node_modules/postcss-convert-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", + "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "dom-scroll-into-view": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/dom-scroll-into-view/-/dom-scroll-into-view-1.2.1.tgz", - "integrity": "sha1-6PNnMt0ImwIBqI14Fdw/iObWbH4=" + "node_modules/@wordpress/scripts/node_modules/postcss-discard-comments": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", + "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "dom-serialize": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", - "integrity": "sha1-ViromZ9Evl6jB29UGdzVnrQ6yVs=", - "requires": { - "custom-event": "~1.0.0", - "ent": "~2.2.0", - "extend": "^3.0.0", - "void-elements": "^2.0.0" + "node_modules/@wordpress/scripts/node_modules/postcss-discard-duplicates": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", + "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "dom-serializer": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", - "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "node_modules/@wordpress/scripts/node_modules/postcss-discard-empty": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", + "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "entities": "^2.0.0" + "engines": { + "node": "^14 || ^16 || >=18.0" }, - "dependencies": { - "domelementtype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", - "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==", - "dev": true - } + "peerDependencies": { + "postcss": "^8.4.31" } }, - "domain-browser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", - "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", - "dev": true + "node_modules/@wordpress/scripts/node_modules/postcss-discard-overridden": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", + "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "domelementtype": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", - "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", - "dev": true + "node_modules/@wordpress/scripts/node_modules/postcss-merge-longhand": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", + "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^6.1.1" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "domexception": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/domexception/-/domexception-2.0.1.tgz", - "integrity": "sha512-yxJ2mFy/sibVQlu5qHjOkf9J3K6zgmCxgJ94u2EdvDOV09H+32LtRswEcUsmUWN72pVLOEnTSRaIVVzVQgS0dg==", + "node_modules/@wordpress/scripts/node_modules/postcss-merge-rules": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", + "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", "dev": true, - "requires": { - "webidl-conversions": "^5.0.0" + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^4.0.2", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "domhandler": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.2.2.tgz", - "integrity": "sha512-PzE9aBMsdZO8TK4BnuJwH0QT41wgMbRzuZrHUcpYncEjmQazq8QEaBWgLG7ZyC/DAZKEgglpIA6j4Qn/HmxS3w==", + "node_modules/@wordpress/scripts/node_modules/postcss-minify-font-values": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", + "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", "dev": true, - "requires": { - "domelementtype": "^2.2.0" + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-minify-gradients": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", + "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", + "dev": true, "dependencies": { - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true - } + "colord": "^2.9.3", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "domutils": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", - "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "node_modules/@wordpress/scripts/node_modules/postcss-minify-params": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", + "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" + "dependencies": { + "browserslist": "^4.23.0", + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "dot-prop": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", - "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "node_modules/@wordpress/scripts/node_modules/postcss-minify-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", + "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", "dev": true, - "requires": { - "is-obj": "^2.0.0" + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "dotenv": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-10.0.0.tgz", - "integrity": "sha512-rlBi9d8jpv9Sf1klPjNfFAuWDjKLwTIJJ/VxtoTwIR6hnZxcEOQCZg2oIL3MWBYw5GpUDKOEnND7LXTbIpQ03Q==", - "dev": true + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-charset": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", + "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", + "dev": true, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "dotenv-expand": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-5.1.0.tgz", - "integrity": "sha512-YXQl1DSa4/PQyRfgrv6aoNjhasp/p4qs9FjJ4q4cQk+8m4r6k4ZSiEyytKG8f8W9gi8WsQtIObNmKd+tMzNTmA==", - "dev": true + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-display-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", + "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "download": { - "version": "6.2.5", - "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", - "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-positions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", + "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", "dev": true, - "optional": true, - "requires": { - "caw": "^2.0.0", - "content-disposition": "^0.5.2", - "decompress": "^4.0.0", - "ext-name": "^5.0.0", - "file-type": "5.2.0", - "filenamify": "^2.0.0", - "get-stream": "^3.0.0", - "got": "^7.0.0", - "make-dir": "^1.0.0", - "p-event": "^1.0.0", - "pify": "^3.0.0" + "dependencies": { + "postcss-value-parser": "^4.2.0" }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } + }, + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-repeat-style": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", + "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", + "dev": true, "dependencies": { - "file-type": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", - "integrity": "sha1-LdvqfHP/42No365J3DOMBYwritY=", - "dev": true, - "optional": true - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "optional": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "downshift": { - "version": "6.1.7", - "resolved": "https://registry.npmjs.org/downshift/-/downshift-6.1.7.tgz", - "integrity": "sha512-cVprZg/9Lvj/uhYRxELzlu1aezRcgPWBjTvspiGTVEU64gF5pRdSRKFVLcxqsZC637cLAGMbL40JavEfWnqgNg==", - "requires": { - "@babel/runtime": "^7.14.8", - "compute-scroll-into-view": "^1.0.17", - "prop-types": "^15.7.2", - "react-is": "^17.0.2", - "tslib": "^2.3.0" - }, - "dependencies": { - "@babel/runtime": { - "version": "7.15.3", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.15.3.tgz", - "integrity": "sha512-OvwMLqNXkCXSz1kSm58sEsNuhqOx/fKpnUnKnFB5v8uDda5bLNEHNgKPvhDN6IU0LDcnHQ90LlJ0Q6jnyBSIBA==", - "requires": { - "regenerator-runtime": "^0.13.4" - } - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==" - }, - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-string": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", + "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "dev": true + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-timing-functions": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", + "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "duplexer3": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", - "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-unicode": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", + "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", "dev": true, - "optional": true + "dependencies": { + "browserslist": "^4.23.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" + } }, - "duplexify": { - "version": "3.7.1", - "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", - "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-url": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", + "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", "dev": true, - "requires": { - "end-of-stream": "^1.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.0.0", - "stream-shift": "^1.0.0" + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "ecc-jsbn": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", - "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "node_modules/@wordpress/scripts/node_modules/postcss-normalize-whitespace": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", + "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", "dev": true, - "requires": { - "jsbn": "~0.1.0", - "safer-buffer": "^2.1.0" + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=" - }, - "electron-to-chromium": { - "version": "1.3.740", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.740.tgz", - "integrity": "sha512-Mi2m55JrX2BFbNZGKYR+2ItcGnR4O5HhrvgoRRyZQlaMGQULqDhoGkLWHzJoshSzi7k1PUofxcDbNhlFrDZNhg==", - "dev": true - }, - "element-closest": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-2.0.2.tgz", - "integrity": "sha1-cqdAoQdFM4LijfnOXbtajfD5Zuw=" - }, - "elliptic": { - "version": "6.5.4", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.4.tgz", - "integrity": "sha512-iLhC6ULemrljPZb+QutR5TQGB+pdW6KGD5RSegS+8sorOZT+rdQFbsQFJgvN3eRqNALqJer4oQ16YvJHlU8hzQ==", - "dev": true, - "requires": { - "bn.js": "^4.11.9", - "brorand": "^1.1.0", - "hash.js": "^1.0.0", - "hmac-drbg": "^1.0.1", - "inherits": "^2.0.4", - "minimalistic-assert": "^1.0.1", - "minimalistic-crypto-utils": "^1.0.1" - }, - "dependencies": { - "bn.js": { - "version": "4.12.0", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz", - "integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA==", - "dev": true - } + "node_modules/@wordpress/scripts/node_modules/postcss-ordered-values": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", + "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", + "dev": true, + "dependencies": { + "cssnano-utils": "^4.0.2", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "emittery": { - "version": "0.7.2", - "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.7.2.tgz", - "integrity": "sha512-A8OG5SR/ij3SsJdWDJdkkSYUjQdCUx6APQXem0SaEePBSRg4eymGYwBkKo1Y6DU+af/Jn2dBQqDBvjnr9Vi8nQ==", - "dev": true - }, - "emoji-regex": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", - "integrity": "sha512-CwBLREIQ7LvYFB0WyRvwhq5N5qPhc6PMjD6bYggFlI5YyDgl+0vxq5VHbMOFqLg7hfWzmu8T5Z1QofhmTIhItA==" - }, - "emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "dev": true - }, - "emotion": { - "version": "10.0.27", - "resolved": "https://registry.npmjs.org/emotion/-/emotion-10.0.27.tgz", - "integrity": "sha512-2xdDzdWWzue8R8lu4G76uWX5WhyQuzATon9LmNeCy/2BHVC6dsEpfhN1a0qhELgtDVdjyEA6J8Y/VlI5ZnaH0g==", - "requires": { - "babel-plugin-emotion": "^10.0.27", - "create-emotion": "^10.0.27" + "node_modules/@wordpress/scripts/node_modules/postcss-reduce-initial": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", + "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=" - }, - "encoding": { - "version": "0.1.13", - "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", - "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", - "requires": { - "iconv-lite": "^0.6.2" + "node_modules/@wordpress/scripts/node_modules/postcss-reduce-transforms": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", + "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", + "dev": true, + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "end-of-stream": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", - "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "node_modules/@wordpress/scripts/node_modules/postcss-svgo": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", + "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", "dev": true, - "requires": { - "once": "^1.4.0" + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^3.2.0" + }, + "engines": { + "node": "^14 || ^16 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "engine.io": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-4.1.1.tgz", - "integrity": "sha512-t2E9wLlssQjGw0nluF6aYyfX8LwYU8Jj0xct+pAhfWfv/YrBn6TSNtEYsgxHIfaMqfrLx07czcMg9bMN6di+3w==", - "requires": { - "accepts": "~1.3.4", - "base64id": "2.0.0", - "cookie": "~0.4.1", - "cors": "~2.8.5", - "debug": "~4.3.1", - "engine.io-parser": "~4.0.0", - "ws": "~7.4.2" - }, - "dependencies": { - "cookie": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", - "integrity": "sha512-ZwrFkGJxUR3EIoXtO+yVE69Eb7KlixbaeAWfBQB9vVsNn/o+Yw69gBWSSDK825hQNdN+wF8zELf3dFNl/kxkUA==" - }, - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "node_modules/@wordpress/scripts/node_modules/postcss-unique-selectors": { + "version": "6.0.4", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", + "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", + "dev": true, + "dependencies": { + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "engine.io-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-4.0.2.tgz", - "integrity": "sha512-sHfEQv6nmtJrq6TKuIz5kyEKH/qSdK56H/A+7DnAuUPWosnIZAS2NHNcPLmyjtY3cGS/MqJdZbUjW97JU72iYg==", - "requires": { - "base64-arraybuffer": "0.1.4" + "node_modules/@wordpress/scripts/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "enhanced-resolve": { + "node_modules/@wordpress/scripts/node_modules/rtlcss": { "version": "4.3.0", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-4.3.0.tgz", - "integrity": "sha512-3e87LvavsdxyoCfGusJnrZ5G8SLPOFeHSNpZI/ATL9a5leXo2k0w6MKnbqhdBad9qTobSfB20Ld7UmgoNbAZkQ==", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", + "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "memory-fs": "^0.5.0", - "tapable": "^1.0.0" - }, - "dependencies": { - "memory-fs": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.5.0.tgz", - "integrity": "sha512-jA0rdU5KoQMC0e6ppoNRtpp6vjFq6+NY7r8hywnC7V+1Xj/MtHwGIbB1QaK/dunyjWteJzmkpd7ooeWg10T7GA==", - "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" - } - } + "dependencies": { + "escalade": "^3.1.1", + "picocolors": "^1.0.0", + "postcss": "^8.4.21", + "strip-json-comments": "^3.1.1" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" + }, + "engines": { + "node": ">=12.0.0" } }, - "enquirer": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.3.6.tgz", - "integrity": "sha512-yjNnPr315/FjS4zIsUxYguYUPP2e1NK4d7E7ZOLiyYCcbFBiTMyID+2wvm2w6+pZ/odMA7cRkjhsPbltwBOrLg==", + "node_modules/@wordpress/scripts/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", "dev": true, - "requires": { - "ansi-colors": "^4.1.1" + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" } }, - "ent": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.0.tgz", - "integrity": "sha1-6WQhkyWiHQX0RGai9obtbOX13R0=" - }, - "entities": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", - "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", - "dev": true - }, - "enzyme": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/enzyme/-/enzyme-3.11.0.tgz", - "integrity": "sha512-Dw8/Gs4vRjxY6/6i9wU0V+utmQO9kvh9XLnz3LIudviOnVYDEe2ec+0k+NQoMamn1VrjKgCUOWj5jG/5M5M0Qw==", - "dev": true, - "requires": { - "array.prototype.flat": "^1.2.3", - "cheerio": "^1.0.0-rc.3", - "enzyme-shallow-equal": "^1.0.1", - "function.prototype.name": "^1.1.2", - "has": "^1.0.3", - "html-element-map": "^1.2.0", - "is-boolean-object": "^1.0.1", - "is-callable": "^1.1.5", - "is-number-object": "^1.0.4", - "is-regex": "^1.0.5", - "is-string": "^1.0.5", - "is-subset": "^0.1.1", - "lodash.escape": "^4.0.1", - "lodash.isequal": "^4.5.0", - "object-inspect": "^1.7.0", - "object-is": "^1.0.2", - "object.assign": "^4.1.0", - "object.entries": "^1.1.1", - "object.values": "^1.1.1", - "raf": "^3.4.1", - "rst-selector-parser": "^2.2.3", - "string.prototype.trim": "^1.2.1" - } - }, - "enzyme-shallow-equal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/enzyme-shallow-equal/-/enzyme-shallow-equal-1.0.4.tgz", - "integrity": "sha512-MttIwB8kKxypwHvRynuC3ahyNc+cFbR8mjVIltnmzQ0uKGqmsfO4bfBuLxb0beLNPhjblUEYvEbsg+VSygvF1Q==", + "node_modules/@wordpress/scripts/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", "dev": true, - "requires": { - "has": "^1.0.3", - "object-is": "^1.1.2" + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "enzyme-to-json": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/enzyme-to-json/-/enzyme-to-json-3.6.2.tgz", - "integrity": "sha512-Ynm6Z6R6iwQ0g2g1YToz6DWhxVnt8Dy1ijR2zynRKxTyBGA8rCDXU3rs2Qc4OKvUvc2Qoe1bcFK6bnPs20TrTg==", + "node_modules/@wordpress/scripts/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", "dev": true, - "requires": { - "@types/cheerio": "^0.22.22", - "lodash": "^4.17.21", - "react-is": "^16.12.0" + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" } }, - "equivalent-key-map": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/equivalent-key-map/-/equivalent-key-map-0.2.2.tgz", - "integrity": "sha512-xvHeyCDbZzkpN4VHQj/n+j2lOwL0VWszG30X4cOrc9Y7Tuo2qCdZK/0AMod23Z5dCtNUbaju6p0rwOhHUk05ew==" - }, - "errno": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/errno/-/errno-0.1.8.tgz", - "integrity": "sha512-dJ6oBr5SQ1VSd9qkk7ByRgb/1SH4JZjCHSW/mr63/QcXO9zLVxvJ6Oy13nio03rxpSnVDDjFor75SjVeZWPW/A==", + "node_modules/@wordpress/scripts/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", "dev": true, - "requires": { - "prr": "~1.0.1" + "engines": { + "node": ">=8" } }, - "error": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", - "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", + "node_modules/@wordpress/scripts/node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", "dev": true, - "requires": { - "string-template": "~0.2.1" - } - }, - "error-ex": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", - "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", - "requires": { - "is-arrayish": "^0.2.1" + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" } }, - "es-abstract": { - "version": "1.17.7", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.17.7.tgz", - "integrity": "sha512-VBl/gnfcJ7OercKA9MVaegWsBHFjV492syMudcnQZvt/Dw8ezpcOHYZXa/J96O8vx+g4x65YKhxOwDUh63aS5g==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - }, - "es-array-method-boxes-properly": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/es-array-method-boxes-properly/-/es-array-method-boxes-properly-1.0.0.tgz", - "integrity": "sha512-wd6JXUmyHmt8T5a2xreUwKcGPq6f1f+WwIJkijUqiGcJz1qqnZgP6XIK+QyIWU5lT7imeNxUll48bziG+TSYcA==", - "dev": true - }, - "es-to-primitive": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.2.1.tgz", - "integrity": "sha512-QCOllgZJtaUo9miYBcLChTUaHNjJF3PYs1VidD7AwiEj1kYxKeQTctLAezAOH5ZKRH0g2IgPn6KwB4IT8iRpvA==", - "requires": { - "is-callable": "^1.1.4", - "is-date-object": "^1.0.1", - "is-symbol": "^1.0.2" - } - }, - "escalade": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.1.tgz", - "integrity": "sha512-k0er2gUkLf8O0zKJiAhmkTnJlTvINGv7ygDNPbeIsX/TJjGJZHuh9B2UxbsaEkmlEo9MfhrSzmhIlhRlI2GXnw==" - }, - "escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=" - }, - "escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=" - }, - "escodegen": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.0.0.tgz", - "integrity": "sha512-mmHKys/C8BFUGI+MAWNcSYoORYLMdPzjrknd2Vc+bUsjN5bXcr8EhrNB+UTqfL1y3I9c4fw2ihgtMPQLBRiQxw==", + "node_modules/@wordpress/scripts/node_modules/source-map-loader": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-3.0.2.tgz", + "integrity": "sha512-BokxPoLjyl3iOrgkWaakaxqnelAJSS+0V+De0kKIq6lyWrXuiPgYTGp6z3iHmqljKAaLXwZa+ctD8GccRJeVvg==", "dev": true, - "requires": { - "esprima": "^4.0.1", - "estraverse": "^5.2.0", - "esutils": "^2.0.2", - "optionator": "^0.8.1", - "source-map": "~0.6.1" - }, "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - }, - "levn": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", - "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2" - } - }, - "optionator": { - "version": "0.8.3", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.3.tgz", - "integrity": "sha512-+IW9pACdk3XWmmTXG8m3upGUJst5XRGzxMRjXzAuJ1XnIFNvfhjjIuYkDvysnPQ7qzqVzLt78BCruntqRhWQbA==", - "dev": true, - "requires": { - "deep-is": "~0.1.3", - "fast-levenshtein": "~2.0.6", - "levn": "~0.3.0", - "prelude-ls": "~1.1.2", - "type-check": "~0.3.2", - "word-wrap": "~1.2.3" - } - }, - "prelude-ls": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", - "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - }, - "type-check": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", - "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", - "dev": true, - "requires": { - "prelude-ls": "~1.1.2" - } - } + "abab": "^2.0.5", + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" } }, - "eslint": { - "version": "7.32.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-7.32.0.tgz", - "integrity": "sha512-VHZ8gX+EDfz+97jGcgyGCyRia/dPOd6Xh9yPv8Bl1+SoaIwD+a/vlrOmGRUyOYu7MwUhc7CxqeaDZU13S4+EpA==", + "node_modules/@wordpress/scripts/node_modules/stylehacks": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", + "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", "dev": true, - "requires": { - "@babel/code-frame": "7.12.11", - "@eslint/eslintrc": "^0.4.3", - "@humanwhocodes/config-array": "^0.5.0", - "ajv": "^6.10.0", - "chalk": "^4.0.0", - "cross-spawn": "^7.0.2", - "debug": "^4.0.1", - "doctrine": "^3.0.0", - "enquirer": "^2.3.5", - "escape-string-regexp": "^4.0.0", - "eslint-scope": "^5.1.1", - "eslint-utils": "^2.1.0", - "eslint-visitor-keys": "^2.0.0", - "espree": "^7.3.1", - "esquery": "^1.4.0", - "esutils": "^2.0.2", - "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^6.0.1", - "functional-red-black-tree": "^1.0.1", - "glob-parent": "^5.1.2", - "globals": "^13.6.0", - "ignore": "^4.0.6", - "import-fresh": "^3.0.0", - "imurmurhash": "^0.1.4", - "is-glob": "^4.0.0", - "js-yaml": "^3.13.1", - "json-stable-stringify-without-jsonify": "^1.0.1", - "levn": "^0.4.1", - "lodash.merge": "^4.6.2", - "minimatch": "^3.0.4", - "natural-compare": "^1.4.0", - "optionator": "^0.9.1", - "progress": "^2.0.0", - "regexpp": "^3.1.0", - "semver": "^7.2.1", - "strip-ansi": "^6.0.0", - "strip-json-comments": "^3.1.0", - "table": "^6.0.9", - "text-table": "^0.2.0", - "v8-compile-cache": "^2.0.3" - }, - "dependencies": { - "@babel/code-frame": { - "version": "7.12.11", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.12.11.tgz", - "integrity": "sha512-Zt1yodBx1UcyiePMSkWnU4hPqhwq7hGi2nFL1LeA3EUl+q2LQx16MISgJ0+z7dnmgvP9QtIleuETGOiOH1RcIw==", - "dev": true, - "requires": { - "@babel/highlight": "^7.10.4" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "doctrine": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", - "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", - "dev": true, - "requires": { - "esutils": "^2.0.2" - } - }, - "escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "dev": true - }, - "eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "dev": true, - "requires": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-2.1.0.tgz", - "integrity": "sha512-w94dQYoauyvlDc43XnGB8lU3Zt713vNChgt4EWwhXAP2XkBvndfxF0AgIqKOOasjPIPzj9JqgwkwbCYD0/V3Zg==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^1.1.0" - }, - "dependencies": { - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } - } - }, - "globals": { - "version": "13.11.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-13.11.0.tgz", - "integrity": "sha512-08/xrJ7wQjK9kkkRoI3OFUBbLx4f+6x3SGwcPvQ0QH6goFDrOU2oyAWrmh3dJezu65buo+HBMzAMQy6rovVC3g==", - "dev": true, - "requires": { - "type-fest": "^0.20.2" - } - }, - "ignore": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-4.0.6.tgz", - "integrity": "sha512-cyFDKrqc/YdcWFniJhzI42+AzS+gNwmUzOSFcRCQYwySuBBBy/KjuxWLZ/FHEH6Moq1NizMOBWyTcv8O4OZIMg==", - "dev": true - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "type-fest": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", - "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", - "dev": true - } + "dependencies": { + "browserslist": "^4.23.0", + "postcss-selector-parser": "^6.0.16" + }, + "engines": { + "node": "^14 || ^16 || >=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "eslint-config-prettier": { + "node_modules/@wordpress/scripts/node_modules/supports-color": { "version": "7.2.0", - "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-7.2.0.tgz", - "integrity": "sha512-rV4Qu0C3nfJKPOAhFujFxB7RMP+URFyQqqOZW9DMRD7ZDTFyjaIlETU3xzHELt++4ugC0+Jm084HQYkkJe+Ivg==", - "dev": true - }, - "eslint-import-resolver-node": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.6.tgz", - "integrity": "sha512-0En0w03NRVMn9Uiyn8YRPDKvWjxCWkslUEhGNTdGx15RvPJYQ+lbOlqrlNI2vEAs4pDYK4f/HN2TbDmk5TP0iw==", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "debug": "^3.2.7", - "resolve": "^1.20.0" - }, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "eslint-module-utils": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.6.2.tgz", - "integrity": "sha512-QG8pcgThYOuqxupd06oYTZoNOGaUdTY1PqK+oS6ElF6vs4pBdk/aYxFVQQXzcrAqp9m7cl7lb2ubazX+g16k2Q==", + "node_modules/@wordpress/scripts/node_modules/svgo": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", + "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", "dev": true, - "requires": { - "debug": "^3.2.7", - "pkg-dir": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "pkg-dir": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-2.0.0.tgz", - "integrity": "sha1-9tXREJ4Z1j7fQo4L1X4Sd3YVM0s=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - } - } + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "eslint-plugin-import": { - "version": "2.24.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.24.2.tgz", - "integrity": "sha512-hNVtyhiEtZmpsabL4neEj+6M5DCLgpYyG9nzJY8lZQeQXEn5UPW1DpUdsMHMXsq98dbNm7nt1w9ZMSVpfJdi8Q==", + "node_modules/@wordpress/scripts/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", "dev": true, - "requires": { - "array-includes": "^3.1.3", - "array.prototype.flat": "^1.2.4", - "debug": "^2.6.9", - "doctrine": "^2.1.0", - "eslint-import-resolver-node": "^0.3.6", - "eslint-module-utils": "^2.6.2", - "find-up": "^2.0.0", - "has": "^1.0.3", - "is-core-module": "^2.6.0", - "minimatch": "^3.0.4", - "object.values": "^1.1.4", - "pkg-up": "^2.0.0", - "read-pkg-up": "^3.0.0", - "resolve": "^1.20.0", - "tsconfig-paths": "^3.11.0" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - }, - "read-pkg-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-3.0.0.tgz", - "integrity": "sha1-PtSWaF26D4/hGNBpHcUfSh/5bwc=", - "dev": true, - "requires": { - "find-up": "^2.0.0", - "read-pkg": "^3.0.0" - } - }, - "resolve": { - "version": "1.20.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.20.0.tgz", - "integrity": "sha512-wENBPt4ySzg4ybFQW2TT1zMQucPK95HSh/nq2CFTZVOGut2+pQvSsgtda4d26YrYcr067wjbmzOG8byDPBX63A==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "engines": { + "node": ">=8" } }, - "eslint-plugin-jest": { - "version": "24.4.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-24.4.0.tgz", - "integrity": "sha512-8qnt/hgtZ94E9dA6viqfViKBfkJwFHXgJmTWlMGDgunw1XJEGqm3eiPjDsTanM3/u/3Az82nyQM9GX7PM/QGmg==", + "node_modules/@wordpress/stylelint-config": { + "version": "23.34.0", + "resolved": "https://registry.npmjs.org/@wordpress/stylelint-config/-/stylelint-config-23.34.0.tgz", + "integrity": "sha512-4WetpbMeyq27h1233huk12Jv5xthc4KDKMOZfDXKQl8wS6FX1NUlhEOxbe7k1u20ZQd2AvR71ucp3hOjnSw74A==", "dev": true, - "requires": { - "@typescript-eslint/experimental-utils": "^4.0.1" + "license": "MIT", + "dependencies": { + "@stylistic/stylelint-plugin": "^3.0.1", + "@wordpress/theme": "^0.9.0", + "stylelint-config-recommended": "^14.0.1", + "stylelint-config-recommended-scss": "^14.1.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "stylelint": "^16.8.2", + "stylelint-scss": "^6.4.0" } }, - "eslint-plugin-jsdoc": { - "version": "34.8.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-34.8.2.tgz", - "integrity": "sha512-UOU9A40Cl806JMtla2vF+RM6sNqfLPbhLv9FZqhcC7+LmChD3DVaWqM7ADxpF0kMyZNWe1QKUnqGnXaA3NTn+w==", + "node_modules/@wordpress/theme": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/@wordpress/theme/-/theme-0.9.0.tgz", + "integrity": "sha512-jxskNZVvWHIswQvWvswaNIAkBpXwdFcocBYxTWQnYgvb0QAEYsKsnqYMulZPrz/Dk4c+GF7ptwdLxb3rry9tcg==", "dev": true, - "requires": { - "@es-joy/jsdoccomment": "^0.6.0", - "comment-parser": "1.1.5", - "debug": "^4.3.1", - "esquery": "^1.4.0", - "jsdoctypeparser": "^9.0.0", - "lodash": "^4.17.21", - "regextras": "^0.7.1", - "semver": "^7.3.5", - "spdx-expression-parse": "^3.0.1" - }, + "license": "GPL-2.0-or-later", "dependencies": { - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - } - } - }, - "eslint-plugin-jsx-a11y": { - "version": "6.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.4.1.tgz", - "integrity": "sha512-0rGPJBbwHoGNPU73/QCLP/vveMlM1b1Z9PponxO87jfr6tuH5ligXbDT6nHSSzBC8ovX2Z+BQu7Bk5D/Xgq9zg==", - "dev": true, - "requires": { - "@babel/runtime": "^7.11.2", - "aria-query": "^4.2.2", - "array-includes": "^3.1.1", - "ast-types-flow": "^0.0.7", - "axe-core": "^4.0.2", - "axobject-query": "^2.2.0", - "damerau-levenshtein": "^1.0.6", - "emoji-regex": "^9.0.0", - "has": "^1.0.3", - "jsx-ast-utils": "^3.1.0", - "language-tags": "^1.0.5" - }, - "dependencies": { - "emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "dev": true + "@wordpress/element": "^6.42.0", + "@wordpress/private-apis": "^1.42.0", + "colorjs.io": "^0.6.0", + "memize": "^2.1.0" + }, + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" + }, + "peerDependencies": { + "react": "^18.0.0", + "react-dom": "^18.0.0", + "stylelint": "^16.8.2" + }, + "peerDependenciesMeta": { + "stylelint": { + "optional": true } } }, - "eslint-plugin-markdown": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/eslint-plugin-markdown/-/eslint-plugin-markdown-1.0.2.tgz", - "integrity": "sha512-BfvXKsO0K+zvdarNc801jsE/NTLmig4oKhZ1U3aSUgTf2dB/US5+CrfGxMsCK2Ki1vS1R3HPok+uYpufFndhzw==", + "node_modules/@wordpress/warning": { + "version": "3.42.0", + "resolved": "https://registry.npmjs.org/@wordpress/warning/-/warning-3.42.0.tgz", + "integrity": "sha512-LMsbWI57IkVRoco+HTQezSzf3FW97AJH3QllwQdk+Ge5y2mJ2jkfIgwZP7uDeMozA1HVUAW+TgmybLloS9xHzg==", "dev": true, - "requires": { - "object-assign": "^4.0.1", - "remark-parse": "^5.0.0", - "unified": "^6.1.2" + "license": "GPL-2.0-or-later", + "engines": { + "node": ">=18.12.0", + "npm": ">=8.19.2" } }, - "eslint-plugin-prettier": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-3.4.1.tgz", - "integrity": "sha512-htg25EUYUeIhKHXjOinK4BgCcDwtLHjqaxCDsMy5nbnUMkKFvIhMVCp+5GFUXQ4Nr8lBsPqtGAqBenbpFqAA2g==", + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", "dev": true, - "requires": { - "prettier-linter-helpers": "^1.0.0" - } + "license": "BSD-3-Clause" }, - "eslint-plugin-react": { - "version": "7.25.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.25.1.tgz", - "integrity": "sha512-P4j9K1dHoFXxDNP05AtixcJEvIT6ht8FhYKsrkY0MPCPaUMYijhpWwNiRDZVtA8KFuZOkGSeft6QwH8KuVpJug==", + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", "dev": true, - "requires": { - "array-includes": "^3.1.3", - "array.prototype.flatmap": "^1.2.4", - "doctrine": "^2.1.0", - "estraverse": "^5.2.0", - "has": "^1.0.3", - "jsx-ast-utils": "^2.4.1 || ^3.0.0", - "minimatch": "^3.0.4", - "object.entries": "^1.1.4", - "object.fromentries": "^2.0.4", - "object.values": "^1.1.4", - "prop-types": "^15.7.2", - "resolve": "^2.0.0-next.3", - "string.prototype.matchall": "^4.0.5" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "object.values": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.4.tgz", - "integrity": "sha512-TnGo7j4XSnKQoK3MfvkzqKCi0nVe/D9I9IjwTNYdb/fxYHpjrluHVOgw0AF6jrRFGMPHdfuidR09tIDiIvnaSg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" - } - }, - "resolve": { - "version": "2.0.0-next.3", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.3.tgz", - "integrity": "sha512-W8LucSynKUIDu9ylraa7ueVZ7hc0uAgJBxVsQSKOXOyle8a93qXhcz+XAXZ8bIq2d6i4Ehddn6Evt+0/UwKk6Q==", - "dev": true, - "requires": { - "is-core-module": "^2.2.0", - "path-parse": "^1.0.6" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } - } + "license": "Apache-2.0" }, - "eslint-plugin-react-hooks": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.2.0.tgz", - "integrity": "sha512-623WEiZJqxR7VdxFCKLI6d6LLpwJkGPYKODnkH3D7WpOG5KM8yWueBd8TLsNAetEJNF5iJmolaAKO3F8yzyVBQ==", + "node_modules/abab": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.6.tgz", + "integrity": "sha512-j2afSsaIENvHZN2B8GOpF566vZ5WVk5opAiMTvWgaQT8DkbOqsTfvNAvHoRGU2zzP8cPoqys+xHTRDWW8L+/BA==", "dev": true }, - "eslint-scope": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-4.0.3.tgz", - "integrity": "sha512-p7VutNr1O/QrxysMo3E45FjYDTeXBy0iTltPFNSqKAIfjDSXC+4dj+qfyuD8bfAXrW/y6lW3O76VaYNPKfpKrg==", - "dev": true, - "requires": { - "esrecurse": "^4.1.0", - "estraverse": "^4.1.1" - } - }, - "eslint-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/eslint-utils/-/eslint-utils-3.0.0.tgz", - "integrity": "sha512-uuQC43IGctw68pJA1RgbQS8/NP7rch6Cwd4j3ZBtgo4/8Flj4eGE7ZYSZRN3iq5pVUv6GPdW5Z1RFleo84uLDA==", - "dev": true, - "requires": { - "eslint-visitor-keys": "^2.0.0" - } - }, - "eslint-visitor-keys": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", - "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", "dev": true }, - "espree": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/espree/-/espree-7.3.1.tgz", - "integrity": "sha512-v3JCNCE64umkFpmkFGqzVKsOT0tN1Zr+ueqLZfpV1Ob8e+CEgPWa+OxCoGH3tnhimMKIaBm4m/vaRpJ/krRz2g==", + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", "dev": true, - "requires": { - "acorn": "^7.4.0", - "acorn-jsx": "^5.3.1", - "eslint-visitor-keys": "^1.3.0" - }, "dependencies": { - "acorn": { - "version": "7.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-7.4.1.tgz", - "integrity": "sha512-nQyp0o1/mNdbTO1PO6kHkwSrmgZ0MT/jCCpNiwbUjGoRN4dlBhqJtoQuCnEOKzgTVwg0ZWiCoQy6SxMebQVh8A==", - "dev": true - }, - "eslint-visitor-keys": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-1.3.0.tgz", - "integrity": "sha512-6J72N8UNa462wa/KFODt/PJ3IU60SDpC3QXC1Hjc1BXXpfL2C9R5+AU7jhe0F6GREqVMh4Juu+NY7xn+6dipUQ==", - "dev": true - } + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" } }, - "esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "dev": true - }, - "esquery": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.4.0.tgz", - "integrity": "sha512-cCDispWt5vHHtwMY2YrAQ4ibFkAL8RbH5YGBnZBc90MolvvfkkQcJro/aZiAQUlQ3qgrYS6D6v8Gc5G5CQsc9w==", - "dev": true, - "requires": { - "estraverse": "^5.1.0" + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "license": "MIT", + "bin": { + "acorn": "bin/acorn" }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } + "engines": { + "node": ">=0.4.0" } }, - "esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", "dev": true, - "requires": { - "estraverse": "^5.2.0" + "license": "MIT", + "engines": { + "node": ">=10.13.0" }, - "dependencies": { - "estraverse": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.2.0.tgz", - "integrity": "sha512-BxbNGGNm0RyRYvUdHpIwv9IWzeM9XClbOxwoATuFdOE7ZE6wHL+HQ5T8hoPM+zHvmKzzsEqhgy0GrQ5X13afiQ==", - "dev": true - } + "peerDependencies": { + "acorn": "^8.14.0" } }, - "estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "dev": true - }, - "esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "dev": true - }, - "etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=", - "dev": true - }, - "ev-emitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz", - "integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==" - }, - "event-source-polyfill": { - "version": "1.0.24", - "resolved": "https://registry.npmjs.org/event-source-polyfill/-/event-source-polyfill-1.0.24.tgz", - "integrity": "sha512-aEtMhrH5ww3X6RgbsNcwu0whw8zjOoeRnwPqRKqKuxWS5KlAZhCY+rTm6wMlHOXbxmLGn8lW6Xox7rfpBExzGA==" - }, - "event-target-shim": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", - "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } }, - "eventemitter2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", - "integrity": "sha1-j2G3XN4BKy6esoTUVFWDtWQ7Yas=", - "dev": true + "node_modules/acorn-walk": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.2.0.tgz", + "integrity": "sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } }, - "eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + "node_modules/adm-zip": { + "version": "0.5.10", + "resolved": "https://registry.npmjs.org/adm-zip/-/adm-zip-0.5.10.tgz", + "integrity": "sha512-x0HvcHqVJNTPk/Bw8JbLWlWoo6Wwnsug0fnYYro1HBrjxZ3G7/AZk7Ahv8JwDe1uIcz8eBqvu86FuF1POiG7vQ==", + "dev": true, + "engines": { + "node": ">=6.0" + } }, - "events": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", - "integrity": "sha512-/46HWwbfCX2xTawVfkKLGxMifJYQBWMwY1mjywRtb4c9x8l5NP3KoJtnIOiL1hfdRkIuYhETxQlo62IF8tcnlg==", - "dev": true + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } }, - "eventsource": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-1.0.7.tgz", - "integrity": "sha512-4Ln17+vVT0k8aWq+t/bF5arcS3EpT9gYtW66EPacdj/mAFevznsnyoHLPy2BA8gbIQeIHoPsvwmfBftfcG//BQ==", + "node_modules/ajv": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, - "requires": { - "original": "^1.0.0" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "evp_bytestokey": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", - "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "node_modules/ajv-errors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/ajv-errors/-/ajv-errors-1.0.1.tgz", + "integrity": "sha512-DCRfO/4nQ+89p/RK43i8Ezd41EqdGIU4ld7nGF8OQ14oc/we5rEntLCUa7+jrn3nn83BosfwZA0wb4pon2o8iQ==", "dev": true, - "requires": { - "md5.js": "^1.3.4", - "safe-buffer": "^5.1.1" + "peerDependencies": { + "ajv": ">=5.0.0" } }, - "exec-buffer": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", - "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", "dev": true, - "optional": true, - "requires": { - "execa": "^0.7.0", - "p-finally": "^1.0.0", - "pify": "^3.0.0", - "rimraf": "^2.5.4", - "tempfile": "^2.0.0" - }, "dependencies": { - "execa": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", - "integrity": "sha1-lEvs00zEHuMqY6n68nrVpl/Fl3c=", - "dev": true, - "optional": true, - "requires": { - "cross-spawn": "^5.0.1", - "get-stream": "^3.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - } - }, - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { "optional": true } } }, - "exec-sh": { - "version": "0.3.6", - "resolved": "https://registry.npmjs.org/exec-sh/-/exec-sh-0.3.6.tgz", - "integrity": "sha512-nQn+hI3yp+oD0huYhKwvYI32+JFeq+XkNcD1GAo3Y/MjxsfVGmrrzrnzjWiNY6f+pUCP440fThsFh5gZrRAU/w==", - "dev": true + "node_modules/ajv-formats/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } }, - "execa": { + "node_modules/ajv-formats/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", - "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/ajv-keywords": { + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", + "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", "dev": true, - "requires": { - "cross-spawn": "^6.0.0", - "get-stream": "^4.0.0", - "is-stream": "^1.1.0", - "npm-run-path": "^2.0.0", - "p-finally": "^1.0.0", - "signal-exit": "^3.0.0", - "strip-eof": "^1.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "peerDependencies": { + "ajv": "^6.9.1" } }, - "execall": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/execall/-/execall-2.0.0.tgz", - "integrity": "sha512-0FU2hZ5Hh6iQnarpRtQurM/aAvp3RIbfvgLHrcqJYzhXyV2KFruhuChf9NC6waAhiUR7FFtlugkI4p7f2Fqlow==", + "node_modules/anser": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/anser/-/anser-2.3.5.tgz", + "integrity": "sha512-vcZjxvvVoxTeR5XBNJB38oTu/7eDCZlwdz32N1eNgpyPF7j/Z7Idf+CUwQOkKKpJ7RJyjxgLHCM7vdIK0iCNMQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ansi-colors": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.3.tgz", + "integrity": "sha512-/6w/C21Pm1A7aZitlI5Ni/2J6FFQN8i1Cvz3kHABAAbw93v/NlvKdVOqz7CCWz/3iv/JplRSEEZ83XION15ovw==", "dev": true, - "requires": { - "clone-regexp": "^2.1.0" + "engines": { + "node": ">=6" } }, - "executable": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", - "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "node_modules/ansi-escapes": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", + "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", "dev": true, - "optional": true, - "requires": { - "pify": "^2.2.0" - }, "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true, - "optional": true - } + "type-fest": "^0.21.3" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "exit": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", - "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", - "dev": true - }, - "expand-brackets": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", - "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "node_modules/ansi-escapes/node_modules/type-fest": { + "version": "0.21.3", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", + "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", "dev": true, - "requires": { - "debug": "^2.3.3", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "posix-character-classes": "^0.1.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" + "engines": { + "node": ">=10" }, - "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "expand-tilde": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", - "integrity": "sha1-C4HrqJflo9MdHD0QL48BRB5VlEk=", + "node_modules/ansi-html": { + "version": "0.0.9", + "resolved": "https://registry.npmjs.org/ansi-html/-/ansi-html-0.0.9.tgz", + "integrity": "sha512-ozbS3LuenHVxNRh/wdnN16QapUHzauqSomAl1jwwJRRsGwFwtj644lIhxfWu0Fy0acCij2+AEgHvjscq3dlVXg==", "dev": true, - "requires": { - "os-homedir": "^1.0.1" + "engines": [ + "node >= 0.8.0" + ], + "license": "Apache-2.0", + "bin": { + "ansi-html": "bin/ansi-html" } }, - "expect": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/expect/-/expect-26.6.2.tgz", - "integrity": "sha512-9/hlOBkQl2l/PLHJx6JjoDF6xPKcJEsUlWKb23rKE7KzeDqUZKXKNMW27KIue5JMdBV9HgmoJPcc8HtO85t9IA==", + "node_modules/ansi-html-community": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", + "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-styles": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-regex-util": "^26.0.0" - }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - } + "engines": [ + "node >= 0.8.0" + ], + "bin": { + "ansi-html": "bin/ansi-html" } }, - "expect-puppeteer": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", - "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", - "dev": true + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "express": { - "version": "4.17.1", - "resolved": "https://registry.npmjs.org/express/-/express-4.17.1.tgz", - "integrity": "sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==", + "node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", "dev": true, - "requires": { - "accepts": "~1.3.7", - "array-flatten": "1.1.1", - "body-parser": "1.19.0", - "content-disposition": "0.5.3", - "content-type": "~1.0.4", - "cookie": "0.4.0", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "~1.1.2", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "~1.1.2", - "fresh": "0.5.2", - "merge-descriptors": "1.0.1", - "methods": "~1.1.2", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.7", - "proxy-addr": "~2.0.5", - "qs": "6.7.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.1.2", - "send": "0.17.1", - "serve-static": "1.14.1", - "setprototypeof": "1.1.1", - "statuses": "~1.5.0", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" + "dependencies": { + "color-convert": "^1.9.0" }, + "engines": { + "node": ">=4" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "license": "ISC", "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/arch": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/arch/-/arch-2.2.0.tgz", + "integrity": "sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "qs": { - "version": "6.7.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.7.0.tgz", - "integrity": "sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==", - "dev": true + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "dev": true + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ], + "optional": true }, - "ext-list": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", - "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "node_modules/archive-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/archive-type/-/archive-type-4.0.0.tgz", + "integrity": "sha1-+S5yIzBW38aWlHJ0nCZ72wRrHXA=", "dev": true, "optional": true, - "requires": { - "mime-db": "^1.28.0" + "dependencies": { + "file-type": "^4.2.0" + }, + "engines": { + "node": ">=4" } }, - "ext-name": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", - "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "node_modules/archive-type/node_modules/file-type": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-4.4.0.tgz", + "integrity": "sha512-f2UbFQEk7LXgWpi5ntcO86OeA/cC80fuDDDaX/fZ2ZGel+AF7leRQqBBW1eJNiiQkrZlAoM6P+VYP5P6bOlDEQ==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "ext-list": "^2.0.0", - "sort-keys-length": "^1.0.0" + "engines": { + "node": ">=4" } }, - "extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" - }, - "extend-shallow": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", - "integrity": "sha1-Jqcarwc7OfshJxcnRhMcJwQCjbg=", + "node_modules/are-docs-informative": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/are-docs-informative/-/are-docs-informative-0.0.2.tgz", + "integrity": "sha512-ixiS0nLNNG5jNQzgZJNoUpBKdo9yTYZMGJ+QgT2jmjR7G7+QHRCc4v6LQ3NgE7EBJq+o0ams3waJwkrlBom8Ig==", "dev": true, - "requires": { - "assign-symbols": "^1.0.0", - "is-extendable": "^1.0.1" - }, - "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } + "license": "MIT", + "engines": { + "node": ">=14" } }, - "external-editor": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", - "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", "dev": true, - "requires": { - "chardet": "^0.4.0", - "iconv-lite": "^0.4.17", - "tmp": "^0.0.33" - }, "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "sprintf-js": "~1.0.2" } }, - "extglob": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", - "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "node_modules/argparse/node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true, - "requires": { - "array-unique": "^0.3.2", - "define-property": "^1.0.0", - "expand-brackets": "^2.1.4", - "extend-shallow": "^2.0.1", - "fragment-cache": "^0.2.1", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, - "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } - } + "license": "BSD-3-Clause" }, - "extract-zip": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", - "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "node_modules/aria-query": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.2.tgz", + "integrity": "sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==", "dev": true, - "requires": { - "@types/yauzl": "^2.9.1", - "debug": "^4.1.1", - "get-stream": "^5.1.0", - "yauzl": "^2.10.0" - }, - "dependencies": { - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - } + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "extsprintf": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", - "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", - "dev": true - }, - "fast-average-color": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/fast-average-color/-/fast-average-color-4.3.0.tgz", - "integrity": "sha512-k8FXd6+JeXoItmdNqB3hMwFgArryjdYBLuzEM8fRY/oztd/051yhSHU6GUrMOfIQU9dDHyFDcIAkGrQKlYtpDA==" + "node_modules/arr-diff": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/arr-diff/-/arr-diff-4.0.0.tgz", + "integrity": "sha1-1kYQdP6/7HHn4VI1dhoyml3HxSA=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "dev": true + "node_modules/arr-flatten": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/arr-flatten/-/arr-flatten-1.1.0.tgz", + "integrity": "sha512-L3hKV5R/p5o81R7O02IGnwpDmkp6E982XhtbuwSe3O4qOtMMMtodicASA1Cny2U+aCXcNpml+m4dPsvsJ3jatg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "fast-diff": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.2.0.tgz", - "integrity": "sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==", - "dev": true + "node_modules/arr-union": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/arr-union/-/arr-union-3.1.0.tgz", + "integrity": "sha1-45sJrqne+Gao8gbiiK9jkZuuOcQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "fast-glob": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.2.7.tgz", - "integrity": "sha512-rYGMRwip6lUMvYD3BTScMwT1HtAs2d71SMv66Vrxs0IekGZEjhM0pcMfjQPnknBt2zeCwQMEupiN02ZP4DiT1Q==", + "node_modules/array-buffer-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-buffer-byte-length/-/array-buffer-byte-length-1.0.2.tgz", + "integrity": "sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==", "dev": true, - "requires": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.4" - }, "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "call-bound": "^1.0.3", + "is-array-buffer": "^3.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "dev": true - }, - "fast-levenshtein": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", - "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", - "dev": true + "node_modules/array-each": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/array-each/-/array-each-1.0.1.tgz", + "integrity": "sha1-p5SvDAWrF1KEbudTofIRoFugxE8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "fast-memoize": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/fast-memoize/-/fast-memoize-2.5.2.tgz", - "integrity": "sha512-Ue0LwpDYErFbmNnZSF0UH6eImUwDmogUO1jyE+JbN2gsQz/jICm1Ve7t9QT0rNSsfJt+Hs4/S3GnsDVjL4HVrw==" + "node_modules/array-find-index": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-find-index/-/array-find-index-1.0.2.tgz", + "integrity": "sha1-3wEKoSh+Fku9pvlyOwqWoexBh6E=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } }, - "fastest-levenshtein": { - "version": "1.0.12", - "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.12.tgz", - "integrity": "sha512-On2N+BpYJ15xIC974QNVuYGMOlEVt4s0EOI3wwMqOmK1fdDY+FN/zltPV8vosq4ad4c/gJ1KHScUn/6AWIgiow==", + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", "dev": true }, - "fastestsmallesttextencoderdecoder": { - "version": "1.0.22", - "resolved": "https://registry.npmjs.org/fastestsmallesttextencoderdecoder/-/fastestsmallesttextencoderdecoder-1.0.22.tgz", - "integrity": "sha512-Pb8d48e+oIuY4MaM64Cd7OW1gt4nxCHs7/ddPPZ/Ic3sg8yVGM7O9wDvZ7us6ScaUupzM+pfBolwtYhN1IxBIw==" - }, - "fastq": { - "version": "1.12.0", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.12.0.tgz", - "integrity": "sha512-VNX0QkHK3RsXVKr9KrlUv/FoTa0NdbYoHHl7uXHv2rzyHSlxjdNAKug2twd9luJxpcyNeAgf5iPPMutJO67Dfg==", + "node_modules/array-includes": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/array-includes/-/array-includes-3.1.9.tgz", + "integrity": "sha512-FmeCCAenzH0KH381SPT5FZmiA/TmpndpcaShhfgEN9eCVjnFBqq3l1xrI42y8+PPLI6hypzou4GXw00WHmPBLQ==", "dev": true, - "requires": { - "reusify": "^1.0.4" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.0", + "es-object-atoms": "^1.1.1", + "get-intrinsic": "^1.3.0", + "is-string": "^1.1.1", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "faye-websocket": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", - "integrity": "sha1-TkkvjQTftviQA1B/btvy1QHnxvQ=", + "node_modules/array-slice": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/array-slice/-/array-slice-1.1.0.tgz", + "integrity": "sha512-B1qMD3RBP7O8o0H2KbrXDyB0IccejMF15+87Lvlor12ONPRHP6gTjXMNkt/d3ZuOGbAe66hFmaCfECI24Ufp6w==", "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" + "engines": { + "node": ">=0.10.0" } }, - "fb-watchman": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.1.tgz", - "integrity": "sha512-DkPJKQeY6kKwmuMretBhr7G6Vodr7bFwDYTXIkfG1gjvNpaxBTQV3PbXg6bR1c1UP4jPOX0jHUbbHANL9vRjVg==", + "node_modules/array-union": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", + "integrity": "sha512-Dxr6QJj/RdU/hCaBjOfxW+q6lyuVE6JFWIrAUpuOOhoJJoQ99cUn3igRaHVB5P9WrgFVN0FfArM3x0cueOU8ng==", "dev": true, - "requires": { - "bser": "2.1.1" + "license": "MIT", + "dependencies": { + "array-uniq": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "node_modules/array-uniq": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", + "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", "dev": true, - "requires": { - "pend": "~1.2.0" + "engines": { + "node": ">=0.10.0" } }, - "fetch-blob": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-2.1.2.tgz", - "integrity": "sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==" - }, - "figgy-pudding": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.2.tgz", - "integrity": "sha512-0btnI/H8f2pavGMN8w40mlSKOfTK2SVJmBfBeVIj3kNw0swwgzyRq0d5TJVOwodFmtvpPeWPN/MCcfuWF0Ezbw==", - "dev": true - }, - "figures": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", - "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", + "node_modules/array-unique": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/array-unique/-/array-unique-0.3.2.tgz", + "integrity": "sha1-qJS3XUvE9s1nnvMkSp/Y9Gri1Cg=", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5", - "object-assign": "^4.1.0" + "engines": { + "node": ">=0.10.0" } }, - "file-entry-cache": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", - "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "node_modules/array.prototype.findlast": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", + "integrity": "sha512-CVvd6FHg1Z3POpBLxO6E6zr+rSKEQ9L6rZHAaY7lLfhKsWYUBBOuMs0e9o24oopj6H+geRCX0YJ+TJLBK2eHyQ==", "dev": true, - "requires": { - "flat-cache": "^3.0.4" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", + "node_modules/array.prototype.findlastindex": { + "version": "1.2.6", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.6.tgz", + "integrity": "sha512-F/TKATkzseUExPlfvmwQKGITM3DGTK+vkAsCZoDc5daVygbJBnjEUCbgkAvVFsgfXfX4YIqZ/27G3k3tdXrTxQ==", "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-shim-unscopables": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "file-sync-cmp": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", - "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", - "dev": true - }, - "file-type": { - "version": "10.11.0", - "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", - "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", - "dev": true - }, - "file-uri-to-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/file-uri-to-path/-/file-uri-to-path-1.0.0.tgz", - "integrity": "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==", + "node_modules/array.prototype.flat": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.3.tgz", + "integrity": "sha512-rwG/ja1neyLqCuGZ5YYrznA62D4mZXg0i1cIskIUKSiqF3Cje9/wXAls9B9s1Wa2fomMsIv8czB8jZcPmxCXFg==", "dev": true, - "optional": true - }, - "filename-reserved-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", - "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", - "dev": true + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "filenamify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", - "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "node_modules/array.prototype.flatmap": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/array.prototype.flatmap/-/array.prototype.flatmap-1.3.3.tgz", + "integrity": "sha512-Y7Wt51eKJSyi80hFrJCePGGNo5ktJCslFuboqJsbf57CCPcm5zztluPlc4/aD8sWsKvlwatezpV4U1efk8kpjg==", "dev": true, - "optional": true, - "requires": { - "filename-reserved-regex": "^2.0.0", - "strip-outer": "^1.0.0", - "trim-repeated": "^1.0.0" + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "fill-range": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", - "integrity": "sha1-1USBHUKPmOsGpj3EAtJAPDKMOPc=", + "node_modules/array.prototype.tosorted": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/array.prototype.tosorted/-/array.prototype.tosorted-1.1.4.tgz", + "integrity": "sha512-p6Fx8B7b7ZhL/gmUsAy0D15WhvDccw3mnGNbZpi3pmeJdxtWsj2jEaI4Y6oo3XiHfzuSgPwKc04MYt6KgvC/wA==", "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-number": "^3.0.0", - "repeat-string": "^1.6.1", - "to-regex-range": "^2.1.0" - }, + "license": "MIT", "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3", + "es-errors": "^1.3.0", + "es-shim-unscopables": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "finalhandler": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", - "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", - "requires": { - "debug": "2.6.9", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "on-finished": "~2.3.0", - "parseurl": "~1.3.3", - "statuses": "~1.5.0", - "unpipe": "~1.0.0" - }, + "node_modules/arraybuffer.prototype.slice": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/arraybuffer.prototype.slice/-/arraybuffer.prototype.slice-1.0.4.tgz", + "integrity": "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==", + "dev": true, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "requires": { - "ms": "2.0.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=" - } + "array-buffer-byte-length": "^1.0.1", + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "is-array-buffer": "^3.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "find-cache-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-2.1.0.tgz", - "integrity": "sha512-Tq6PixE0w/VMFfCgbONnkiQIVol/JJL7nRMi20fqzA4NRs9AfeqMGeRdPi3wIhYkxjeBaWh2rxwapn5Tu3IqOQ==", + "node_modules/arrify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", + "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", "dev": true, - "requires": { - "commondir": "^1.0.1", - "make-dir": "^2.0.0", - "pkg-dir": "^3.0.0" + "engines": { + "node": ">=0.10.0" } }, - "find-file-up": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", - "integrity": "sha1-z2gJG8+fMApA2kEbN9pczlovvqA=", + "node_modules/asn1": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.4.tgz", + "integrity": "sha512-jxwzQpLQjSmWXgwaCZE9Nz+glAG01yF1QnWgbhGwHI5A6FRIEY6IVqtHhIepHqI7/kyEyQEagBC5mBEFlIYvdg==", "dev": true, - "requires": { - "fs-exists-sync": "^0.1.0", - "resolve-dir": "^0.1.0" + "dependencies": { + "safer-buffer": "~2.1.0" } }, - "find-parent-dir": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", - "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", - "dev": true + "node_modules/assert-plus": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", + "integrity": "sha1-8S4PPF13sLHN2RRpQuTpbB5N1SU=", + "dev": true, + "engines": { + "node": ">=0.8" + } }, - "find-pkg": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", - "integrity": "sha1-G9wiwG42NlUy4qJIBGhUuXiNpVc=", + "node_modules/assign-symbols": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz", + "integrity": "sha1-WWZ/QfrdTyDMvCu5a41Pf3jsA2c=", "dev": true, - "requires": { - "find-file-up": "^0.1.2" + "engines": { + "node": ">=0.10.0" } }, - "find-process": { - "version": "1.4.4", - "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.4.tgz", - "integrity": "sha512-rRSuT1LE4b+BFK588D2V8/VG9liW0Ark1XJgroxZXI0LtwmQJOb490DvDYvbm+Hek9ETFzTutGfJ90gumITPhQ==", + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", "dev": true, - "requires": { - "chalk": "^4.0.0", - "commander": "^5.1.0", - "debug": "^4.1.1" - }, "dependencies": { - "commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "dev": true - } + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" } }, - "find-root": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/find-root/-/find-root-1.1.0.tgz", - "integrity": "sha512-NKfW6bec6GfKc0SGx1e07QZY9PE99u0Bft/0rzSD5k3sO/vwkVUpDUKVm5Gpp5Ue3YfShPFTX2070tDs5kB9Ng==" + "node_modules/ast-types-flow": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/ast-types-flow/-/ast-types-flow-0.0.8.tgz", + "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", + "dev": true, + "license": "MIT" }, - "find-up": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", - "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", - "requires": { - "locate-path": "^3.0.0" - } - }, - "find-versions": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", - "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "node_modules/astral-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/astral-regex/-/astral-regex-2.0.0.tgz", + "integrity": "sha512-Z7tMw1ytTXt5jqMcOP+OQteU1VuNK9Y02uuJtKQ1Sv69jXQKKg5cibLwGJow8yzZP+eAc18EmLGPal0bp36rvQ==", "dev": true, - "optional": true, - "requires": { - "semver-regex": "^2.0.0" + "license": "MIT", + "engines": { + "node": ">=8" } }, - "findup-sync": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-3.0.0.tgz", - "integrity": "sha512-YbffarhcicEhOrm4CtrwdKBdCuz576RLdhJDsIfvNtxUuhdRet1qZcsMjqbePtAseKdAnDyM/IyXbu7PRPRLYg==", + "node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - }, "dependencies": { - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "lodash": "^4.17.14" } }, - "fined": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", - "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "node_modules/async-function": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/async-function/-/async-function-1.0.0.tgz", + "integrity": "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==", "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "is-plain-object": "^2.0.3", - "object.defaults": "^1.1.0", - "object.pick": "^1.2.0", - "parse-filepath": "^1.0.1" - }, - "dependencies": { - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - } - } - }, - "fizzy-ui-utils": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz", - "integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==", - "requires": { - "desandro-matches-selector": "^2.0.0" + "engines": { + "node": ">= 0.4" } }, - "flagged-respawn": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", - "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=", "dev": true }, - "flat-cache": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.0.4.tgz", - "integrity": "sha512-dm9s5Pw7Jc0GvMYbshN6zchCA9RgQlzzEZX3vylR9IqFfS8XciblUXOKfW6SiuJ0e13eDYZoZV5wdrev7P3Nwg==", + "node_modules/atob": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/atob/-/atob-2.1.2.tgz", + "integrity": "sha512-Wm6ukoaOGJi/73p/cl2GvLjTI5JM1k/O14isD73YML8StrH/7/lRFgmg8nICZgD3bZZvjwCGxtMOD3wWNAu8cg==", "dev": true, - "requires": { - "flatted": "^3.1.0", - "rimraf": "^3.0.2" + "bin": { + "atob": "bin/atob.js" }, - "dependencies": { - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "engines": { + "node": ">= 4.5.0" } }, - "flatted": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.2.2.tgz", - "integrity": "sha512-JaTY/wtrcSyvXJl4IMFHPKyFur1sE9AUqc0QnhOaJ0CxHtAoIV8pYDzeEfAaNEtGkOfq4gr3LBFmdXW5mOQFnA==", - "dev": true + "node_modules/autoprefixer": { + "version": "10.4.27", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.27.tgz", + "integrity": "sha512-NP9APE+tO+LuJGn7/9+cohklunJsXWiaWEfV3si4Gi/XHDwVNgkwr1J3RQYFIvPy76GmJ9/bW8vyoU1LcxwKHA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/autoprefixer" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-lite": "^1.0.30001774", + "fraction.js": "^5.3.4", + "picocolors": "^1.1.1", + "postcss-value-parser": "^4.2.0" + }, + "bin": { + "autoprefixer": "bin/autoprefixer" + }, + "engines": { + "node": "^10 || ^12 || >=14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } }, - "flush-write-stream": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/flush-write-stream/-/flush-write-stream-1.1.1.tgz", - "integrity": "sha512-3Z4XhFZ3992uIq0XOqb9AreonueSYphE6oYbpt5+3u06JWklbsPkNv3ZKkP9Bz/r+1MWCaMoSQ28P85+1Yc77w==", + "node_modules/available-typed-arrays": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", + "integrity": "sha512-wvUjBtSGN7+7SjNpq/9M2Tg350UZD3q62IFZLbRAR1bSMlCo1ZaeW+BJ+D090e4hIIZLBcTDWe4Mh4jvUDajzQ==", "dev": true, - "requires": { - "inherits": "^2.0.3", - "readable-stream": "^2.3.6" + "dependencies": { + "possible-typed-array-names": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "follow-redirects": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.13.1.tgz", - "integrity": "sha512-SSG5xmZh1mkPGyKzjZP8zLjltIfpW32Y5QpdNJyjcfGxK3qo3NDDkZOZSFiGn1A6SclQxY9GzEwAHQ3dmYRWpg==" + "node_modules/aws-sign2": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", + "integrity": "sha1-tG6JCTSpWR8tL2+G1+ap8bP+dqg=", + "dev": true, + "engines": { + "node": "*" + } }, - "for-in": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", - "integrity": "sha1-gQaNKVqBQuwKxybG4iAMMPttXoA=", + "node_modules/aws4": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz", + "integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA==", "dev": true }, - "for-own": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", - "integrity": "sha1-UmXGgaTylNq78XyVCbZ2OqhFEM4=", + "node_modules/axe-core": { + "version": "4.10.2", + "resolved": "https://registry.npmjs.org/axe-core/-/axe-core-4.10.2.tgz", + "integrity": "sha512-RE3mdQ7P3FRSe7eqCWoeQ/Z9QXrtniSjp1wUjt5nRC3WIpz5rSCve6o3fsZ2aCpJtrZjSZgjwXAoTO5k4tEI0w==", "dev": true, - "requires": { - "for-in": "^1.0.1" + "engines": { + "node": ">=4" } }, - "forever-agent": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", - "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", - "dev": true + "node_modules/axios": { + "version": "1.13.6", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.13.6.tgz", + "integrity": "sha512-ChTCHMouEe2kn713WHbQGcuYrr6fXTBiu460OTwWrWob16g1bXn4vtz07Ope7ewMozJAnEquLk5lWQWtBig9DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "follow-redirects": "^1.15.11", + "form-data": "^4.0.5", + "proxy-from-env": "^1.1.0" + } }, - "form-data": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", - "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "node_modules/axios/node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "asynckit": "^0.4.0", - "combined-stream": "^1.0.6", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" } }, - "formdata-polyfill": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.0.tgz", - "integrity": "sha512-sBVj9czlZu7nOjbHDZa3IqNT/OCs5JR45G5FW4B7ZthDpcfqIl9CCFbLXYSEh/5YDIr0cZaFEBzHaGs1o2hCgA==", - "requires": { - "fetch-blob": "*", - "karma": "^6.3.2" + "node_modules/axobject-query": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/axobject-query/-/axobject-query-4.1.0.tgz", + "integrity": "sha512-qIj0G9wZbMGNLjLmg1PT6v2mE9AH2zlnADJD/2tC6E00hgmhUOfEB6greHPAfLRSufHqROIUTkw6E+M3lH0PTQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">= 0.4" } }, - "forwarded": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.1.2.tgz", - "integrity": "sha1-mMI9qxF1ZXuMBXPozszZGw/xjIQ=", - "dev": true - }, - "fraction.js": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.1.1.tgz", - "integrity": "sha512-MHOhvvxHTfRFpF1geTK9czMIZ6xclsEor2wkIGYYq+PxcQqT7vStJqjhe6S1TenZrMZzo+wlqOufBDVepUEgPg==", + "node_modules/b4a": { + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz", + "integrity": "sha512-fpWrvyVHEKyeEvbKZTVOeZF3VSKKWtJxFIxX/jaVPf+cLbGUSitjb49pHLqPV2BUNNZ0LcoeEGfE/YCpyDYHIw==", "dev": true }, - "fragment-cache": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", - "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "node_modules/babel-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.7.0.tgz", + "integrity": "sha512-BrvGY3xZSwEcCzKvKsCi2GgHqDqsYkOP4/by5xCgIwGXQxIEh+8ew3gmrE1y7XRR6LHZIj6yLYnUi/mm2KXKBg==", "dev": true, - "requires": { - "map-cache": "^0.2.2" + "dependencies": { + "@jest/transform": "^29.7.0", + "@types/babel__core": "^7.1.14", + "babel-plugin-istanbul": "^6.1.1", + "babel-preset-jest": "^29.6.3", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.8.0" } }, - "framer-motion": { - "version": "4.1.17", - "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-4.1.17.tgz", - "integrity": "sha512-thx1wvKzblzbs0XaK2X0G1JuwIdARcoNOW7VVwjO8BUltzXPyONGAElLu6CiCScsOQRI7FIk/45YTFtJw5Yozw==", - "requires": { - "@emotion/is-prop-valid": "^0.8.2", - "framesync": "5.3.0", - "hey-listen": "^1.0.8", - "popmotion": "9.3.6", - "style-value-types": "4.1.4", - "tslib": "^2.1.0" - }, + "node_modules/babel-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "framesync": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/framesync/-/framesync-5.3.0.tgz", - "integrity": "sha512-oc5m68HDO/tuK2blj7ZcdEBRx3p1PjrgHazL8GYEpvULhrtGIFbQArN6cQS2QhW8mitffaB+VYzMjDqBxxQeoA==", - "requires": { - "tslib": "^2.1.0" + "node_modules/babel-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/babel-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=", + "node_modules/babel-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", "dev": true }, - "from2": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", - "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", - "requires": { - "inherits": "^2.0.1", - "readable-stream": "^2.0.0" + "node_modules/babel-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "from2-string": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz", - "integrity": "sha1-GCgrJ9CKJnyzAwzSuLSw8hKvdSo=", - "requires": { - "from2": "^2.0.3" + "node_modules/babel-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "fs-constants": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", - "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "dev": true - }, - "fs-exists-sync": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", - "integrity": "sha1-mC1ok6+RjnLQjeyehnP/K1qNat0=", - "dev": true - }, - "fs-extra": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", - "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", - "requires": { - "graceful-fs": "^4.2.0", - "jsonfile": "^4.0.0", - "universalify": "^0.1.0" - }, + "node_modules/babel-loader": { + "version": "9.2.1", + "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", + "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", + "dev": true, "dependencies": { - "jsonfile": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", - "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", - "requires": { - "graceful-fs": "^4.1.6" - } - } + "find-cache-dir": "^4.0.0", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 14.15.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0", + "webpack": ">=5" } }, - "fs-minipass": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", - "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "node_modules/babel-loader/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "minipass": "^3.0.0" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "fs-write-stream-atomic": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/fs-write-stream-atomic/-/fs-write-stream-atomic-1.0.10.tgz", - "integrity": "sha1-tH31NJPvkR33VzHnCp3tAYnbQMk=", + "node_modules/babel-loader/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "iferr": "^0.1.5", - "imurmurhash": "^0.1.4", - "readable-stream": "1 || 2" + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "fs.realpath": { + "node_modules/babel-loader/node_modules/json-schema-traverse": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" - }, - "fsevents": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.1.tgz", - "integrity": "sha512-YR47Eg4hChJGAB1O3yEAOkGO+rlzutoICGqGo9EZ4lKWokzZRSyIW1QmTzqjtw8MJdj9srP869CuWw/hyzSiBw==", - "optional": true - }, - "function-bind": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", - "integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A==" - }, - "function.prototype.name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.4.tgz", - "integrity": "sha512-iqy1pIotY/RmhdFZygSSlW0wko2yxkSCKqsuv4pr8QESohpYyG/Z7B/XXvPRKTJS//960rgguE5mSRUsDdaJrQ==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "functions-have-names": "^1.2.2" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } - } - }, - "functional-red-black-tree": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/functional-red-black-tree/-/functional-red-black-tree-1.0.1.tgz", - "integrity": "sha1-GwqzvVU7Kg1jmdKcDj6gslIHgyc=", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true }, - "functions-have-names": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.2.tgz", - "integrity": "sha512-bLgc3asbWdwPbx2mNk2S49kmJCuQeu0nfmaOgbs8WIyzzkw3r4htszdIi9Q9EMezDPTYuJx2wvjZ/EwgAthpnA==" + "node_modules/babel-loader/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, - "gaze": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", - "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "node_modules/babel-plugin-istanbul": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/babel-plugin-istanbul/-/babel-plugin-istanbul-6.1.1.tgz", + "integrity": "sha512-Y1IQok9821cC9onCx5otgFfRm7Lm+I+wwxOx738M/WLPZ9Q42m4IG5W0FNX8WLL2gYMZo3JkuXIH2DOpWM+qwA==", "dev": true, - "requires": { - "globule": "^1.0.0" + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@istanbuljs/load-nyc-config": "^1.0.0", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-instrument": "^5.0.4", + "test-exclude": "^6.0.0" + }, + "engines": { + "node": ">=8" } }, - "gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "dev": true + "node_modules/babel-plugin-jest-hoist": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.6.3.tgz", + "integrity": "sha512-ESAc/RJvGTFEzRwOTT4+lNDk/GNHMkKbNzsvT0qKRfDyyYTskxB5rnU2njIDYVxXCBHHEI1c0YwHob3WaYujOg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.3.3", + "@babel/types": "^7.3.3", + "@types/babel__core": "^7.1.14", + "@types/babel__traverse": "^7.0.6" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } }, - "get-caller-file": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", - "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==" + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.12", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", + "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.3", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } }, - "get-intrinsic": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.0.2.tgz", - "integrity": "sha512-aeX0vrFm21ILl3+JpFFRNe9aUvp6VFZb2/CTbgLb8j75kOhvoNYjt9d8KA/tJG4gSo8nzEDedRl0h7vDmBYRVg==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, - "get-package-type": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", - "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", - "dev": true + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.6", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", + "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2", + "core-js-compat": "^3.38.0" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } }, - "get-proxy": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", - "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", + "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", "dev": true, - "optional": true, - "requires": { - "npm-conf": "^1.1.0" + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.3" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" } }, - "get-size": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz", - "integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==" - }, - "get-stdin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-5.0.1.tgz", - "integrity": "sha1-Ei4WFZHiH/TFJTAwVpPyDmOTo5g=", - "dev": true - }, - "get-stream": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", - "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "node_modules/babel-preset-current-node-syntax": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/babel-preset-current-node-syntax/-/babel-preset-current-node-syntax-1.0.1.tgz", + "integrity": "sha512-M7LQ0bxarkxQoN+vz5aJPsLBn77n8QgTFmo8WK0/44auK2xlCXrYcUxHFxgU7qW5Yzw/CjmLRK2uJzaCd7LvqQ==", "dev": true, - "requires": { - "pump": "^3.0.0" + "dependencies": { + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-bigint": "^7.8.3", + "@babel/plugin-syntax-class-properties": "^7.8.3", + "@babel/plugin-syntax-import-meta": "^7.8.3", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.8.3", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.8.3", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-top-level-await": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "get-value": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", - "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", - "dev": true - }, - "getobject": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.0.tgz", - "integrity": "sha512-tbUz6AKKKr2YiMB+fLWIgq5ZeBOobop9YMMAU9dC54/ot2ksMXt3DOFyBuhZw6ptcVszEykgByK20j7W9jHFag==", - "dev": true - }, - "getpass": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", - "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "node_modules/babel-preset-jest": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.6.3.tgz", + "integrity": "sha512-0B3bhxR6snWXJZtR/RliHTDPRgn1sNHOR0yVtq/IiQFyuOVjFS+wuio/R4gSNkyYmKmJB4wGZv2NZanmKmTnNA==", "dev": true, - "requires": { - "assert-plus": "^1.0.0" + "dependencies": { + "babel-plugin-jest-hoist": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" } }, - "gettext-parser": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/gettext-parser/-/gettext-parser-1.4.0.tgz", - "integrity": "sha512-sedZYLHlHeBop/gZ1jdg59hlUEcpcZJofLq2JFwJT1zTqAU3l2wFv6IsuwFHGqbiT9DWzMUW4/em2+hspnmMMA==", - "requires": { - "encoding": "^0.1.12", - "safe-buffer": "^5.1.1" + "node_modules/backbone": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/backbone/-/backbone-1.6.1.tgz", + "integrity": "sha512-YQzWxOrIgL6BoFnZjThVN99smKYhyEXXFyJJ2lsF1wJLyo4t+QjmkLrH8/fN22FZ4ykF70Xq7PgTugJVR4zS9Q==", + "dependencies": { + "underscore": ">=1.8.3" } }, - "gifsicle": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-4.0.1.tgz", - "integrity": "sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg==", - "dev": true, - "optional": true, - "requires": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "execa": "^1.0.0", - "logalot": "^2.0.0" - } + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "license": "MIT" }, - "glob": { - "version": "7.1.6", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.6.tgz", - "integrity": "sha512-LwaxwyZ72Lk7vZINtNNrywX0ZuLyStrdDtabefZKAY5ZGJhVtgdznluResxNmPitE0SAO+O26sWTHeKSI2wMBA==", - "requires": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.0.4", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" + "node_modules/bare-events": { + "version": "2.8.2", + "resolved": "https://registry.npmjs.org/bare-events/-/bare-events-2.8.2.tgz", + "integrity": "sha512-riJjyv1/mHLIPX4RwiK+oW9/4c3TEUeORHKefKAKnZ5kyslbN+HXowtbaVEqt4IMUB7OXlfixcs6gsFeo/jhiQ==", + "dev": true, + "license": "Apache-2.0", + "peerDependencies": { + "bare-abort-controller": "*" + }, + "peerDependenciesMeta": { + "bare-abort-controller": { + "optional": true + } } }, - "glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "requires": { - "is-glob": "^4.0.1" + "node_modules/bare-fs": { + "version": "4.5.5", + "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.5.5.tgz", + "integrity": "sha512-XvwYM6VZqKoqDll8BmSww5luA5eflDzY0uEFfBJtFKe4PAAtxBjU3YIxzIBzhyaEQBy1VXEQBto4cpN5RZJw+w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.5.4", + "bare-path": "^3.0.0", + "bare-stream": "^2.6.4", + "bare-url": "^2.2.2", + "fast-fifo": "^1.3.2" + }, + "engines": { + "bare": ">=1.16.0" + }, + "peerDependencies": { + "bare-buffer": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true + } } }, - "glob-to-regexp": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", - "integrity": "sha1-jFoUlNIGbFcMw7/kSWF1rMTVAqs=", - "dev": true - }, - "global-cache": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/global-cache/-/global-cache-1.2.1.tgz", - "integrity": "sha512-EOeUaup5DgWKlCMhA9YFqNRIlZwoxt731jCh47WBV9fQqHgXhr3Fa55hfgIUqilIcPsfdNKN7LHjrNY+Km40KA==", - "requires": { - "define-properties": "^1.1.2", - "is-symbol": "^1.0.1" + "node_modules/bare-os": { + "version": "3.8.0", + "resolved": "https://registry.npmjs.org/bare-os/-/bare-os-3.8.0.tgz", + "integrity": "sha512-Dc9/SlwfxkXIGYhvMQNUtKaXCaGkZYGcd1vuNUUADVqzu4/vQfvnMkYYOUnt2VwQ2AqKr/8qAVFRtwETljgeFg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "bare": ">=1.14.0" } }, - "global-modules": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", - "integrity": "sha1-6lo77ULG1s6ZWk+KEmm12uIjgo0=", + "node_modules/bare-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bare-path/-/bare-path-3.0.0.tgz", + "integrity": "sha512-tyfW2cQcB5NN8Saijrhqn0Zh7AnFNsnczRcuWODH0eYAXBsJ5gVxAUuNr7tsHSC6IZ77cA0SitzT+s47kot8Mw==", "dev": true, - "requires": { - "global-prefix": "^0.1.4", - "is-windows": "^0.2.0" - }, + "license": "Apache-2.0", "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true - } + "bare-os": "^3.0.1" } }, - "global-prefix": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", - "integrity": "sha1-jTvGuNo8qBEqFg2NSW/wRiv+948=", + "node_modules/bare-stream": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/bare-stream/-/bare-stream-2.8.1.tgz", + "integrity": "sha512-bSeR8RfvbRwDpD7HWZvn8M3uYNDrk7m9DQjYOFkENZlXW8Ju/MPaqUPQq5LqJ3kyjEm07siTaAQ7wBKCU59oHg==", "dev": true, - "requires": { - "homedir-polyfill": "^1.0.0", - "ini": "^1.3.4", - "is-windows": "^0.2.0", - "which": "^1.2.12" - }, + "license": "Apache-2.0", "dependencies": { - "is-windows": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", - "integrity": "sha1-3hqm1j6indJIc3tp8f+LgALSEIw=", - "dev": true + "streamx": "^2.21.0", + "teex": "^1.0.1" + }, + "peerDependencies": { + "bare-buffer": "*", + "bare-events": "*" + }, + "peerDependenciesMeta": { + "bare-buffer": { + "optional": true }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } + "bare-events": { + "optional": true } } }, - "globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true - }, - "globalyzer": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", - "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", - "dev": true - }, - "globby": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-6.1.0.tgz", - "integrity": "sha1-9abXDoOV4hyFj7BInWTfAkJNUGw=", + "node_modules/bare-url": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/bare-url/-/bare-url-2.3.2.tgz", + "integrity": "sha512-ZMq4gd9ngV5aTMa5p9+UfY0b3skwhHELaDkhEHetMdX0LRkW9kzaym4oo/Eh+Ghm0CCDuMTsRIGM/ytUc1ZYmw==", "dev": true, - "requires": { - "array-union": "^1.0.1", - "glob": "^7.0.3", - "object-assign": "^4.0.1", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - }, + "license": "Apache-2.0", "dependencies": { - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - } + "bare-path": "^3.0.0" } }, - "globjoin": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", - "integrity": "sha1-L0SUrIkZ43Z8XLtpHp9GMyQoXUM=", - "dev": true - }, - "globrex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", - "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", - "dev": true - }, - "globule": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.2.tgz", - "integrity": "sha512-7IDTQTIu2xzXkT+6mlluidnWo+BypnbSoEVVQCGfzqnl5Ik8d3e1d4wycb8Rj9tWW+Z39uPWsdlquqiqPCd/pA==", + "node_modules/base": { + "version": "0.11.2", + "resolved": "https://registry.npmjs.org/base/-/base-0.11.2.tgz", + "integrity": "sha512-5T6P4xPgpp0YDFvSWwEZ4NoE3aM4QBQXDzmVbraCkFj8zHM+mba8SyqB5DbZWyR7mYHo6Y7BdQo3MoA4m0TeQg==", "dev": true, - "requires": { - "glob": "~7.1.1", - "lodash": "~4.17.10", - "minimatch": "~3.0.2" + "dependencies": { + "cache-base": "^1.0.1", + "class-utils": "^0.3.5", + "component-emitter": "^1.2.1", + "define-property": "^1.0.0", + "isobject": "^3.0.1", + "mixin-deep": "^1.2.0", + "pascalcase": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "glur": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/glur/-/glur-1.1.2.tgz", - "integrity": "sha1-8g6jbbEDv8KSNDkh8fkeg8NGdok=", - "dev": true - }, - "gonzales-pe": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/gonzales-pe/-/gonzales-pe-4.3.0.tgz", - "integrity": "sha512-otgSPpUmdWJ43VXyiNgEYE4luzHCL2pz4wQ0OnDluC6Eg4Ko3Vexy/SrSynglw/eR+OhkzmqFCZa/OFa/RgAOQ==", + "node_modules/base/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "requires": { - "delegate": "^3.1.2" + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "got": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", - "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "node_modules/base/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, - "optional": true, - "requires": { - "decompress-response": "^3.2.0", - "duplexer3": "^0.1.4", - "get-stream": "^3.0.0", - "is-plain-obj": "^1.1.0", - "is-retry-allowed": "^1.0.0", - "is-stream": "^1.0.0", - "isurl": "^1.0.0-alpha5", - "lowercase-keys": "^1.0.0", - "p-cancelable": "^0.3.0", - "p-timeout": "^1.1.1", - "safe-buffer": "^5.0.1", - "timed-out": "^4.0.0", - "url-parse-lax": "^1.0.0", - "url-to-options": "^1.0.1" - }, + "license": "MIT", "dependencies": { - "get-stream": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", - "integrity": "sha1-jpQ9E1jcN1VQVOy+LtsFqhdO3hQ=", - "dev": true, - "optional": true - } + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" } }, - "graceful-fs": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.4.tgz", - "integrity": "sha512-WjKPNJF79dtJAVniUlGGWHYGz2jWxT6VhN/4m1NdkbZ2nOsEF+cI1Edgql5zCRhs/VsQYRvrXctxktVXZUkixw==" - }, - "graceful-readlink": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz", - "integrity": "sha1-TK+tdrxi8C+gObL5Tpo906ORpyU=", - "dev": true - }, - "gradient-parser": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/gradient-parser/-/gradient-parser-0.1.5.tgz", - "integrity": "sha1-DH4heVWeXOfY1x9EI6+TcQCyJIw=" - }, - "growly": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", - "integrity": "sha1-8QdIy+dq+WS3yWyTxrzCivEgwIE=", - "dev": true, - "optional": true - }, - "grunt": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.4.1.tgz", - "integrity": "sha512-ZXIYXTsAVrA7sM+jZxjQdrBOAg7DyMUplOMhTaspMRExei+fD0BTwdWXnn0W5SXqhb/Q/nlkzXclSi3IH55PIA==", + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", "dev": true, - "requires": { - "dateformat": "~3.0.3", - "eventemitter2": "~0.4.13", - "exit": "~0.1.2", - "findup-sync": "~0.3.0", - "glob": "~7.1.6", - "grunt-cli": "~1.4.2", - "grunt-known-options": "~2.0.0", - "grunt-legacy-log": "~3.0.0", - "grunt-legacy-util": "~2.0.1", - "iconv-lite": "~0.4.13", - "js-yaml": "~3.14.0", - "minimatch": "~3.0.4", - "mkdirp": "~1.0.4", - "nopt": "~3.0.6", - "rimraf": "~3.0.2" - }, - "dependencies": { - "findup-sync": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-0.3.0.tgz", - "integrity": "sha1-N5MKpdgWt3fANEXhlmzGeQpMCxY=", - "dev": true, - "requires": { - "glob": "~5.0.0" - }, - "dependencies": { - "glob": { - "version": "5.0.15", - "resolved": "https://registry.npmjs.org/glob/-/glob-5.0.15.tgz", - "integrity": "sha1-G8k2ueAvSmA/zCIuz3Yz0wuLk7E=", - "dev": true, - "requires": { - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "2 || 3", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - } - } - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "grunt-cli": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", - "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", - "dev": true, - "requires": { - "grunt-known-options": "~2.0.0", - "interpret": "~1.1.0", - "liftup": "~3.0.1", - "nopt": "~4.0.1", - "v8flags": "~3.2.0" - }, - "dependencies": { - "nopt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", - "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", - "dev": true, - "requires": { - "abbrev": "1", - "osenv": "^0.1.4" - } - } - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - }, - "interpret": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", - "integrity": "sha1-ftGxQQxqDg94z5XTuEQMY/eLhhQ=", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + { + "type": "consulting", + "url": "https://feross.org/support" } - } + ] }, - "grunt-banner": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/grunt-banner/-/grunt-banner-0.6.0.tgz", - "integrity": "sha1-P4eQIdEj+linuloLb7a+QStYhaw=", + "node_modules/baseline-browser-mapping": { + "version": "2.10.8", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.8.tgz", + "integrity": "sha512-PCLz/LXGBsNTErbtB6i5u4eLpHeMfi93aUv5duMmj6caNu6IphS4q6UevDnL36sZQv9lrP11dbPKGMaXPwMKfQ==", "dev": true, - "requires": { - "chalk": "^1.1.0" + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "engines": { + "node": ">=6.0.0" } }, - "grunt-contrib-clean": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.0.tgz", - "integrity": "sha512-g5ZD3ORk6gMa5ugZosLDQl3dZO7cI3R14U75hTM+dVLVxdMNJCPVmwf9OUt4v4eWgpKKWWoVK9DZc1amJp4nQw==", + "node_modules/basic-ftp": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.0.tgz", + "integrity": "sha512-VoMINM2rqJwJgfdHq6RiUudKt2BV+FY5ZFezP/ypmwayk68+NzzAQy4XXLlqsGD4MCzq3DrmNFD/uUmBJuGoXw==", "dev": true, - "requires": { - "async": "^2.6.1", - "rimraf": "^2.6.2" + "license": "MIT", + "engines": { + "node": ">=10.0.0" } }, - "grunt-contrib-concat": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-1.0.1.tgz", - "integrity": "sha1-YVCYYwhOhx1+ht5IwBUlntl3Rb0=", + "node_modules/batch": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", + "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", + "dev": true + }, + "node_modules/bcrypt-pbkdf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", + "integrity": "sha1-pDAdOJtqQ/m2f/PKEaP2Y342Dp4=", "dev": true, - "requires": { - "chalk": "^1.0.0", - "source-map": "^0.5.3" - }, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "tweetnacl": "^0.14.3" } }, - "grunt-contrib-copy": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", - "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "node_modules/big.js": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", + "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", "dev": true, - "requires": { - "chalk": "^1.1.1", - "file-sync-cmp": "^0.1.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "engines": { + "node": "*" } }, - "grunt-contrib-cssmin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-4.0.0.tgz", - "integrity": "sha512-jXU+Zlk8Q8XztOGNGpjYlD/BDQ0n95IHKrQKtFR7Gd8hZrzgqiG1Ra7cGYc8h2DD9vkSFGNlweb9Q00rBxOK2w==", + "node_modules/bin-build": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/bin-build/-/bin-build-3.0.0.tgz", + "integrity": "sha512-jcUOof71/TNAI2uM5uoUaDq2ePcVBQ3R/qhxAz1rX7UfvduAL/RXD3jXzvn8cVcDJdGVkiR1shal3OH0ImpuhA==", "dev": true, - "requires": { - "chalk": "^4.1.0", - "clean-css": "^5.0.1", - "maxmin": "^3.0.0" - }, + "optional": true, "dependencies": { - "figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "gzip-size": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", - "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", - "dev": true, - "requires": { - "duplexer": "^0.1.1", - "pify": "^4.0.1" - } - }, - "maxmin": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz", - "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==", - "dev": true, - "requires": { - "chalk": "^4.1.0", - "figures": "^3.2.0", - "gzip-size": "^5.1.1", - "pretty-bytes": "^5.3.0" - } - }, - "pretty-bytes": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", - "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", - "dev": true - } + "decompress": "^4.0.0", + "download": "^6.2.2", + "execa": "^0.7.0", + "p-map-series": "^1.0.0", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "grunt-contrib-imagemin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-imagemin/-/grunt-contrib-imagemin-4.0.0.tgz", - "integrity": "sha512-2GYQBQFfJLjeTThJ8E7+vLgvgfOh78u0bgieIK85c2Rv9V6ssd2AvBvuF7T26mK261EN/SlNefpW5+zGWzfrVw==", + "node_modules/bin-build/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "dev": true, - "requires": { - "chalk": "^2.4.1", - "imagemin": "^6.0.0", - "imagemin-gifsicle": "^6.0.1", - "imagemin-jpegtran": "^6.0.0", - "imagemin-optipng": "^6.0.0", - "imagemin-svgo": "^7.0.0", - "p-map": "^1.2.0", - "plur": "^3.0.1", - "pretty-bytes": "^5.1.0" - }, + "license": "MIT", + "optional": true, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "irregular-plurals": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", - "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", - "dev": true - }, - "p-map": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", - "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", - "dev": true - }, - "plur": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", - "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", - "dev": true, - "requires": { - "irregular-plurals": "^2.0.0" - } - }, - "pretty-bytes": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.5.0.tgz", - "integrity": "sha512-p+T744ZyjjiaFlMUZZv6YPC5JrkNj8maRmPaQCWFJFplUAzpIUTRaTcS+7wmZtUoFXHtESJb23ISliaWyz3SHA==", - "dev": true - } + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "grunt-contrib-jshint": { + "node_modules/bin-build/node_modules/get-stream": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-3.0.0.tgz", - "integrity": "sha512-o0V3HNK54+w2Lss/AP0LsAUCEmPDQIcgsDFvTy0sE8sdPXq/8vHdNdMEitK9Wcfoq7H6v02v6soiiwJ0wavT7A==", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, - "requires": { - "chalk": "^4.1.0", - "hooker": "^0.2.3", - "jshint": "~2.12.0" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "grunt-contrib-qunit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-qunit/-/grunt-contrib-qunit-4.0.0.tgz", - "integrity": "sha512-XP9Ks+uoSQzic0eic6koD8kYAKQnSYfu2G1HBqvrvUyXaDDnSSXOKELND8j7dwudnJj4N6KgW6OU7AHeM5PGKA==", + "node_modules/bin-check": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-check/-/bin-check-4.1.0.tgz", + "integrity": "sha512-b6weQyEUKsDGFlACWSIOfveEnImkJyK/FGW6FAG42loyoquvjdtOIqO6yBFzHyqyVVhNgNkQxxx09SFLK28YnA==", "dev": true, - "requires": { - "eventemitter2": "^6.4.2", - "p-each-series": "^2.1.0", - "puppeteer": "^4.0.0" - }, + "optional": true, "dependencies": { - "eventemitter2": { - "version": "6.4.3", - "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.3.tgz", - "integrity": "sha512-t0A2msp6BzOf+QAcI6z9XMktLj52OjGQg+8SJH6v5+3uxNpWYRR3wQmfA+6xtMU9kOC59qk9licus5dYcrYkMQ==", - "dev": true - }, - "puppeteer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-4.0.1.tgz", - "integrity": "sha512-LIiSWTRqpTnnm3R2yAoMBx1inSeKwVZy66RFSkgSTDINzheJZPd5z5mMbPM0FkvwWAZ27a+69j5nZf+Fpyhn3Q==", - "dev": true, - "requires": { - "debug": "^4.1.0", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^4.0.0", - "mime": "^2.0.3", - "mitt": "^2.0.1", - "progress": "^2.0.1", - "proxy-from-env": "^1.0.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" - } - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } - } + "execa": "^0.7.0", + "executable": "^4.1.0" + }, + "engines": { + "node": ">=4" } }, - "grunt-contrib-uglify": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-5.0.1.tgz", - "integrity": "sha512-T/aXZ4WIpAtoswZqb6HROKg7uq9QbKwl+lUuOwK4eoFj3tFv9/a/oMyd3/qvetV29Pbf8P1YYda1gDwZppr60A==", + "node_modules/bin-check/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", "dev": true, - "requires": { - "chalk": "^2.4.1", - "maxmin": "^2.1.0", - "uglify-js": "^3.13.3", - "uri-path": "^1.0.0" - }, + "license": "MIT", + "optional": true, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "grunt-contrib-watch": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", - "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", + "node_modules/bin-check/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, - "requires": { - "async": "^2.6.0", - "gaze": "^1.1.0", - "lodash": "^4.17.10", - "tiny-lr": "^1.1.1" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "grunt-file-append": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/grunt-file-append/-/grunt-file-append-0.0.7.tgz", - "integrity": "sha1-P376M2lvoFdwsoCU9EUIyvxdLto=", - "dev": true - }, - "grunt-includes": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-includes/-/grunt-includes-1.1.0.tgz", - "integrity": "sha512-aZQfL+fiAonPI173QUjGyuCkaUTJci7+a5SkmSAbezUikwLban7Jp6W+vbA/Mnacmy+EPipnuK5kAF6O0SvrDw==", - "dev": true - }, - "grunt-jsdoc": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/grunt-jsdoc/-/grunt-jsdoc-2.4.1.tgz", - "integrity": "sha512-S0zxU0wDewRu7z+vijEItOWe/UttxWVmvz0qz2ZVcAYR2GpXjsiski2CAVN0b18t2qeVLdmxZkJaEWCOsKzcAw==", + "node_modules/bin-version": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/bin-version/-/bin-version-3.1.0.tgz", + "integrity": "sha512-Mkfm4iE1VFt4xd4vH+gx+0/71esbfus2LsnCGe8Pi4mndSPyT+NGES/Eg99jx8/lUGWfu3z2yuB/bt5UB+iVbQ==", "dev": true, - "requires": { - "cross-spawn": "^7.0.1", - "jsdoc": "^3.6.3" - }, + "optional": true, "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - } + "execa": "^1.0.0", + "find-versions": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "grunt-jsvalidate": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/grunt-jsvalidate/-/grunt-jsvalidate-0.2.2.tgz", - "integrity": "sha1-/QlEJYiNbmPfqgbPsJ7gUrjrvo8=", + "node_modules/bin-version-check": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/bin-version-check/-/bin-version-check-4.0.0.tgz", + "integrity": "sha512-sR631OrhC+1f8Cvs8WyVWOA33Y8tgwjETNPyyD/myRBXLkfS/vl74FmH/lFcRl9KY3zwGh7jFhvyk9vV3/3ilQ==", "dev": true, - "requires": { - "esprima": "~1.0.0" + "optional": true, + "dependencies": { + "bin-version": "^3.0.0", + "semver": "^5.6.0", + "semver-truncate": "^1.1.2" }, + "engines": { + "node": ">=6" + } + }, + "node_modules/bin-version-check/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/bin-wrapper": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bin-wrapper/-/bin-wrapper-4.1.0.tgz", + "integrity": "sha512-hfRmo7hWIXPkbpi0ZltboCMVrU+0ClXR/JgbCKKjlDjQf6igXa7OwdqNcFWQZPZTgiY7ZpzE3+LjjkLiTN2T7Q==", + "dev": true, + "optional": true, "dependencies": { - "esprima": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz", - "integrity": "sha1-n1V+CPw7TSbs6d00+Pv0drYlha0=", - "dev": true - } + "bin-check": "^4.1.0", + "bin-version-check": "^4.0.0", + "download": "^7.1.0", + "import-lazy": "^3.1.0", + "os-filter-obj": "^2.0.0", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" } }, - "grunt-known-options": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", - "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", - "dev": true + "node_modules/bin-wrapper/node_modules/download": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/download/-/download-7.1.0.tgz", + "integrity": "sha512-xqnBTVd/E+GxJVrX5/eUJiLYjCGPwMpdL+jGhGU57BvtcA7wwhtHVbXBeUk51kOpW3S7Jn3BQbN9Q1R1Km2qDQ==", + "dev": true, + "optional": true, + "dependencies": { + "archive-type": "^4.0.0", + "caw": "^2.0.1", + "content-disposition": "^0.5.2", + "decompress": "^4.2.0", + "ext-name": "^5.0.0", + "file-type": "^8.1.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^8.3.1", + "make-dir": "^1.2.0", + "p-event": "^2.1.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=6" + } }, - "grunt-legacy-log": { + "node_modules/bin-wrapper/node_modules/download/node_modules/pify": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", - "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", "dev": true, - "requires": { - "colors": "~1.1.2", - "grunt-legacy-log-utils": "~2.1.0", - "hooker": "~0.2.3", - "lodash": "~4.17.19" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "grunt-legacy-log-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", - "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "node_modules/bin-wrapper/node_modules/file-type": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-8.1.0.tgz", + "integrity": "sha512-qyQ0pzAy78gVoJsmYeNgl8uH8yKhr1lVhW7JbzJmnlRi0I4R2eEDEJZVKG8agpDnLpacwNbDhLNG/LMdxHD2YQ==", "dev": true, - "requires": { - "chalk": "~4.1.0", - "lodash": "~4.17.19" + "optional": true, + "engines": { + "node": ">=6" } }, - "grunt-legacy-util": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", - "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "node_modules/bin-wrapper/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, - "requires": { - "async": "~3.2.0", - "exit": "~0.1.2", - "getobject": "~1.0.0", - "hooker": "~0.2.3", - "lodash": "~4.17.21", - "underscore.string": "~3.3.5", - "which": "~2.0.2" - }, - "dependencies": { - "async": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", - "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==", - "dev": true - } + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "grunt-patch-wordpress": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/grunt-patch-wordpress/-/grunt-patch-wordpress-3.0.1.tgz", - "integrity": "sha512-jdeqk/r0hSPez2Afgj+Zz8R6tFYDyhNKTNZvJIE5lAqmkms2ox4a+UfbZWhf9ObFb+Q4JYKIXqJruGzujzmYgA==", + "node_modules/bin-wrapper/node_modules/got": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/got/-/got-8.3.2.tgz", + "integrity": "sha512-qjUJ5U/hawxosMryILofZCkm3C84PLJS/0grRIpjAwu+Lkxxj5cxeCU25BG0/3mDSpXKTyZr8oh8wIgLaH0QCw==", "dev": true, - "requires": { - "grunt": "^1.0.3", - "inquirer": "^5.1.0", - "request": "^2.83.0", - "xmlrpc": "^1.3.1" + "optional": true, + "dependencies": { + "@sindresorhus/is": "^0.7.0", + "cacheable-request": "^2.1.1", + "decompress-response": "^3.3.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "into-stream": "^3.1.0", + "is-retry-allowed": "^1.1.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "mimic-response": "^1.0.0", + "p-cancelable": "^0.4.0", + "p-timeout": "^2.0.1", + "pify": "^3.0.0", + "safe-buffer": "^5.1.1", + "timed-out": "^4.0.1", + "url-parse-lax": "^3.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" } }, - "grunt-postcss": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/grunt-postcss/-/grunt-postcss-0.9.0.tgz", - "integrity": "sha512-lglLcVaoOIqH0sFv7RqwUKkEFGQwnlqyAKbatxZderwZGV1nDyKHN7gZS9LUiTx1t5GOvRBx0BEalHMyVwFAIA==", - "dev": true, - "requires": { - "chalk": "^2.1.0", - "diff": "^3.0.0", - "postcss": "^6.0.11" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "diff": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-3.5.0.tgz", - "integrity": "sha512-A46qtFgd+g7pDZinpnwiRJtxbC1hpgf0uzP3iG89scHk0AUC7A1TGxf5OiiOUv/JMZR8GOt8hL900hV0bOy5xA==", - "dev": true - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "node_modules/bin-wrapper/node_modules/got/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "grunt-replace-lts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/grunt-replace-lts/-/grunt-replace-lts-1.1.0.tgz", - "integrity": "sha512-YCLFHDM7/mEb+7tzdstb756ZYEUTSiyiEj5XhfLIxmVrDKShXQ8STD9f0s7HZXwwHwxFgPr4zELSP7J3kYra7w==", + "node_modules/bin-wrapper/node_modules/import-lazy": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-3.1.0.tgz", + "integrity": "sha512-8/gvXvX2JMn0F+CDlSC4l6kOmVaLOO3XLkksI7CI3Ud95KDYJuYur2b9P/PUt/i/pDAMd/DulQsNbbbmRRsDIQ==", "dev": true, - "requires": { - "chalk": "^1.1.0", - "file-sync-cmp": "^0.1.0", - "lodash": "^4.17.15", - "next-applause": "^2.2.4" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "optional": true, + "engines": { + "node": ">=6" } }, - "grunt-rtlcss": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/grunt-rtlcss/-/grunt-rtlcss-2.0.2.tgz", - "integrity": "sha512-WbI2thnwlF04N+xvJu+NxqEaCyPuLyar196SYhEQFZ2EJHkOS8YYE+Zkh+X9cWhwAtKp7ZEpR/IKXcyQggOIsQ==", + "node_modules/bin-wrapper/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", "dev": true, - "requires": { - "chalk": "^1.0.0", - "rtlcss": "^2.0.0" - }, + "optional": true, "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "grunt-sass": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-3.1.0.tgz", - "integrity": "sha512-90s27H7FoCDcA8C8+R0GwC+ntYD3lG6S/jqcavWm3bn9RiJTmSfOvfbFa1PXx4NbBWuiGQMLfQTj/JvvqT5w6A==", - "dev": true + "node_modules/bin-wrapper/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } }, - "grunt-webpack": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/grunt-webpack/-/grunt-webpack-4.0.3.tgz", - "integrity": "sha512-hRnTf7y9pe4K+M/AKUJFgHykZeyIOUHhZSMVD0/jF/uXphMCen7txPIz8IOnJoa6bX0JrpoueOwo7FgS/OtC2Q==", + "node_modules/bin-wrapper/node_modules/p-cancelable": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.4.1.tgz", + "integrity": "sha512-HNa1A8LvB1kie7cERyy21VNeHb2CWJJYqyyC2o3klWFfMGlFmWv2Z7sFgZH8ZiaYL95ydToKTFVXgMV/Os0bBQ==", "dev": true, - "requires": { - "deep-for-each": "^3.0.0", - "lodash": "^4.17.19" + "optional": true, + "engines": { + "node": ">=4" } }, - "gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "node_modules/bin-wrapper/node_modules/p-event": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-2.3.1.tgz", + "integrity": "sha512-NQCqOFhbpVTMX4qMe8PF8lbGtzZ+LCiN7pcNrb/413Na7+TRoe1xkKUzuWa/YEJdGQ0FvKtj35EEbDoVPO2kbA==", "dev": true, - "requires": { - "duplexer": "^0.1.2" + "optional": true, + "dependencies": { + "p-timeout": "^2.0.1" + }, + "engines": { + "node": ">=6" } }, - "handle-thing": { + "node_modules/bin-wrapper/node_modules/p-timeout": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "dev": true - }, - "har-schema": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", - "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", - "dev": true - }, - "har-validator": { - "version": "5.1.5", - "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", - "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-2.0.1.tgz", + "integrity": "sha512-88em58dDVB/KzPEx1X0N3LwFfYZPyDc4B6eF38M1rk9VTZMbxXXgjugz8mmwpS9Ox4BDZ+t6t3QP5+/gazweIA==", "dev": true, - "requires": { - "ajv": "^6.12.3", - "har-schema": "^2.0.0" - } - }, - "hard-rejection": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", - "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", - "dev": true - }, - "has": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz", - "integrity": "sha512-f2dvO0VU6Oej7RkWJGrehjbzMAjFp5/VKPp5tTpWIV4JHHZK1/BxbFRtf/siA2SWTe09caDmVtYYzWEIbBS4zw==", - "requires": { - "function-bind": "^1.1.1" + "optional": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "has-ansi": { + "node_modules/bin-wrapper/node_modules/prepend-http": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", - "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - } + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "has-bigints": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.1.tgz", - "integrity": "sha512-LSBS2LjbNBTf6287JEbEzvJgftkF5qFkmCo9hDRpAzKhUOlJ+hx8dd4USs00SgsUNwc4617J9ki5YtEClM2ffA==" - }, - "has-flag": { + "node_modules/bin-wrapper/node_modules/url-parse-lax": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", - "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" - }, - "has-symbol-support-x": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", - "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", - "dev": true, - "optional": true - }, - "has-symbols": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.1.tgz", - "integrity": "sha512-PLcsoqu++dmEIZB+6totNFKq/7Do+Z0u4oT0zKOJNl3lYK6vGwwu2hjHs+68OEZbTjiUE9bgOABXbP/GvrS0Kg==" - }, - "has-to-string-tag-x": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", - "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-3.0.0.tgz", + "integrity": "sha512-NjFKA0DidqPa5ciFcSrXnAltTtzz84ogy+NebPvfEgAck0+TNg4UJ4IN+fB7zRZfbgUf0syOo9MDxFkDSMuFaQ==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "has-symbol-support-x": "^1.4.1" - } - }, - "has-tostringtag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.0.tgz", - "integrity": "sha512-kFjcSNhnlGV1kyoGk7OXKSawH5JOb/LzUc5w9B02hOTO0dfFRjbHQKvg1d6cf3HbeUmtU9VbbV3qzZ2Teh97WQ==", - "requires": { - "has-symbols": "^1.0.2" - }, "dependencies": { - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - } + "prepend-http": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "has-value": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", - "integrity": "sha1-GLKB2lhbHFxR3vJMkw7SmgvmsXc=", + "node_modules/binary-extensions": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.1.0.tgz", + "integrity": "sha512-1Yj8h9Q+QDF5FzhMs/c9+6UntbD5MkRfRwac8DoEm9ZfUBZ7tZ55YcGVAzEe4bXsdQHEk+s9S5wsOKVdZrw0tQ==", "dev": true, - "requires": { - "get-value": "^2.0.6", - "has-values": "^1.0.0", - "isobject": "^3.0.0" + "engines": { + "node": ">=8" } }, - "has-values": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", - "integrity": "sha1-lbC2P+whRmGab+V/51Yo1aOe/k8=", + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "dev": true + }, + "node_modules/body": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/body/-/body-5.1.0.tgz", + "integrity": "sha1-5LoM5BCkaTYyM2dgnstOZVMSUGk=", "dev": true, - "requires": { - "is-number": "^3.0.0", - "kind-of": "^4.0.0" - }, "dependencies": { - "kind-of": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", - "integrity": "sha1-IIE989cSkosgc3hpGkUGb65y3Vc=", - "dev": true, - "requires": { - "is-buffer": "^1.1.5" - } - } + "continuable-cache": "^0.3.1", + "error": "^7.0.0", + "raw-body": "~1.1.0", + "safe-json-parse": "~1.0.1" } }, - "hash-base": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.1.0.tgz", - "integrity": "sha512-1nmYp/rhMDiE7AYkDw+lLwlAzz0AntGIe51F3RfFfEqyQ3feY2eI/NcwC6umIQVOASPMsWJLJScWKSSvzL9IVA==", - "dev": true, - "requires": { - "inherits": "^2.0.4", - "readable-stream": "^3.6.0", - "safe-buffer": "^5.2.0" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "node_modules/body-parser": { + "version": "1.20.4", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.4.tgz", + "integrity": "sha512-ZTgYYLMOXY9qKU/57FAo8F+HA2dGX7bqGc71txDRC1rS4frdFI5R7NhluHxH6M0YItAP0sHB4uqAOcYKxO6uGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "~1.2.0", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "on-finished": "~2.4.1", + "qs": "~6.14.0", + "raw-body": "~2.5.3", + "type-is": "~1.6.18", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" } }, - "hash.js": { - "version": "1.1.7", - "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", - "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "node_modules/body-parser/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "requires": { - "inherits": "^2.0.3", - "minimalistic-assert": "^1.0.1" + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, - "hex-color-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/hex-color-regex/-/hex-color-regex-1.1.0.tgz", - "integrity": "sha512-l9sfDFsuqtOqKDsQdqrMRk0U85RZc0RtOR9yPI7mRVOa4FsR/BVnZ0shmQRM96Ji99kYZP/7hn1cedc1+ApsTQ==", - "dev": true - }, - "hey-listen": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/hey-listen/-/hey-listen-1.0.8.tgz", - "integrity": "sha512-COpmrF2NOg4TBWUJ5UVyaCU2A88wEMkUPK4hNqyCkqHbxT92BbvfjoSozkAIIm6XhicGlJHhFdullInrdhwU8Q==" - }, - "highlight-words-core": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/highlight-words-core/-/highlight-words-core-1.2.2.tgz", - "integrity": "sha512-BXUKIkUuh6cmmxzi5OIbUJxrG8OAk2MqoL1DtO3Wo9D2faJg2ph5ntyuQeLqaHJmzER6H5tllCDA9ZnNe9BVGg==" - }, - "hmac-drbg": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", - "integrity": "sha1-0nRXAQJabHdabFRXk+1QL8DGSaE=", + "node_modules/body-parser/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, - "requires": { - "hash.js": "^1.0.3", - "minimalistic-assert": "^1.0.0", - "minimalistic-crypto-utils": "^1.0.1" + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "requires": { - "react-is": "^16.7.0" + "node_modules/body-parser/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "homedir-polyfill": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", - "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "node_modules/body-parser/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "requires": { - "parse-passwd": "^1.0.0" - } + "license": "MIT" }, - "hooker": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", - "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", - "dev": true + "node_modules/body-parser/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "node_modules/body/node_modules/bytes": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-1.0.0.tgz", + "integrity": "sha512-/x68VkHLeTl3/Ll8IvxdwzhrT+IyKc52e/oyHhA2RwqPqswSnjVbSddfPRwAsJtbilMAPSRWwAlpxdYsSWOTKQ==", "dev": true }, - "hoverintent": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/hoverintent/-/hoverintent-2.2.1.tgz", - "integrity": "sha512-VyU54L1xW5rSqpsv/LJ6ecymGXsXXeGs9iVEKot4kKBCq5UodSAuy3DqX686LZxEpaMEfeCHPu4LndsMX5Q9eQ==" - }, - "hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha1-h3dMCUnlE/QuhFdbPEVoH63ioLI=", + "node_modules/body/node_modules/raw-body": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-1.1.7.tgz", + "integrity": "sha512-WmJJU2e9Y6M5UzTOkHaM7xJGAPQD8PNzx3bAd2+uhZAim6wDk6dAZxPVYLF67XhbR4hmKGh33Lpmh4XWrCH5Mg==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" + "license": "MIT", + "dependencies": { + "bytes": "1", + "string_decoder": "0.10" + }, + "engines": { + "node": ">= 0.8.0" } }, - "hpq": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/hpq/-/hpq-1.3.0.tgz", - "integrity": "sha512-fvYTvdCFOWQupGxqkahrkA+ERBuMdzkxwtUdKrxR6rmMd4Pfl+iZ1QiQYoaZ0B/v0y59MOMnz3XFUWbT50/NWA==" + "node_modules/body/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" }, - "hsl-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsl-regex/-/hsl-regex-1.0.0.tgz", - "integrity": "sha1-1JMwx4ntgZ4nakwNJy3/owsY/m4=", - "dev": true + "node_modules/bonjour-service": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.2.1.tgz", + "integrity": "sha512-oSzCS2zV14bh2kji6vNe7vrpJYCHGvcZnlffFQ1MEoX/WOeQ/teD8SYWKR942OI3INjq8OMNJlbPK5LLLUxFDw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "multicast-dns": "^7.2.5" + } }, - "hsla-regex": { + "node_modules/boolbase": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/hsla-regex/-/hsla-regex-1.0.0.tgz", - "integrity": "sha1-wc56MWjIxmFAM6S194d/OyJfnDg=", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha1-aN/1++YMUes3cl6p4+0xDcwed24=", "dev": true }, - "html-comment-regex": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/html-comment-regex/-/html-comment-regex-1.1.2.tgz", - "integrity": "sha512-P+M65QY2JQ5Y0G9KKdlDpo0zK+/OHptU5AaBwUfAIDJZk1MYf32Frm84EcOytfJE0t5JvkAnKlmjsXDnWzCJmQ==", + "node_modules/brace-expansion": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", + "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } }, - "html-element-map": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/html-element-map/-/html-element-map-1.3.1.tgz", - "integrity": "sha512-6XMlxrAFX4UEEGxctfFnmrFaaZFNf9i5fNuV5wZ3WWQ4FVaNP1aX1LkX9j2mfEx1NpjeE/rL3nmgEn23GdFmrg==", - "dev": true, - "requires": { - "array.prototype.filter": "^1.0.0", - "call-bind": "^1.0.2" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } + "node_modules/braces": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-2.3.2.tgz", + "integrity": "sha512-aNdbnj9P8PjdXU4ybaWLK2IF3jc/EoDYbC7AazW6to3TRsfXxscC9UXOB5iDiEQrkyIbWp2SLQda4+QAa7nc3w==", + "dev": true, + "dependencies": { + "arr-flatten": "^1.1.0", + "array-unique": "^0.3.2", + "extend-shallow": "^2.0.1", + "fill-range": "^4.0.0", + "isobject": "^3.0.1", + "repeat-element": "^1.1.2", + "snapdragon": "^0.8.1", + "snapdragon-node": "^2.0.1", + "split-string": "^3.0.2", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "html-encoding-sniffer": { + "node_modules/braces/node_modules/extend-shallow": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-2.0.1.tgz", - "integrity": "sha512-D5JbOMBIR/TVZkubHT+OyT2705QvogUW4IBn6nHd756OwieSF9aDYFj4dv6HHEVGYbHaLETa3WggZYWWMyy3ZQ==", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", "dev": true, - "requires": { - "whatwg-encoding": "^1.0.5" + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "html-entities": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-1.4.0.tgz", - "integrity": "sha512-8nxjcBcd8wovbeKx7h3wTji4e6+rhaVuPNpMqwWgnHh+N9ToqsCs6XztWRBPQ+UtzsoMAdKZtUENoVzU/EMtZA==", - "dev": true - }, - "html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "dev": true - }, - "html-tags": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.1.0.tgz", - "integrity": "sha512-1qYz89hW3lFDEazhjW0yVAV87lw8lVkrJocr72XmBkMKsoSVJCQx3W8BXsC7hO2qAt8BoVjYjtAcZ9perqGnNg==", - "dev": true - }, - "html5shiv": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/html5shiv/-/html5shiv-3.7.3.tgz", - "integrity": "sha1-14qEo2e8uacQEA1XgCw4ewhGMdI=" - }, - "htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - }, - "dependencies": { - "dom-serializer": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", - "integrity": "sha512-5c54Bk5Dw4qAxNOI1pFEizPSjVsx5+bpJKmL2kPn8JhBUq2q09tTCa3mjijun2NfK78NMouDYNMBkOrPZiS+ig==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" }, - "domelementtype": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.2.0.tgz", - "integrity": "sha512-DtBMo82pv1dFtUmHyr48beiuq792Sxohr+8Hm9zoxklYPfa6n0Z3Byjj2IV7bmr2IyqClnqEQhfgHJJ5QF0R5A==", - "dev": true + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" }, - "domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - } + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, - "http-cache-semantics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", - "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "node_modules/bser": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz", + "integrity": "sha512-gQxTNE/GAfIIrmHLUE3oJyp5FO6HRBfhjnw4/wMmA63ZGDJnWBmgY/lyQBpnDUkGmAhbSe39tx2d/iTOAfglwQ==", "dev": true, - "optional": true - }, - "http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha1-+nFolEq5pRnTN8sL7HKE3D5yPYc=", - "dev": true - }, - "http-errors": { - "version": "1.7.2", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.7.2.tgz", - "integrity": "sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==", - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.1", - "statuses": ">= 1.5.0 < 2", - "toidentifier": "1.0.0" - }, "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=" - } + "node-int64": "^0.4.0" } }, - "http-parser-js": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", - "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", - "dev": true - }, - "http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "requires": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" + "node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" } }, - "http-proxy-agent": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-4.0.1.tgz", - "integrity": "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg==", + "node_modules/buffer-alloc": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/buffer-alloc/-/buffer-alloc-1.2.0.tgz", + "integrity": "sha512-CFsHQgjtW1UChdXgbyJGtnm+O/uLQeZdtbDo8mfUgYXCHSM1wgrVxXm6bSyrUuErEb+4sYVGCzASBRot7zyrow==", "dev": true, - "requires": { - "@tootallnate/once": "1", - "agent-base": "6", - "debug": "4" - }, + "optional": true, "dependencies": { - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - } + "buffer-alloc-unsafe": "^1.1.0", + "buffer-fill": "^1.0.0" } }, - "http-proxy-middleware": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-0.19.1.tgz", - "integrity": "sha512-yHYTgWMQO8VvwNS22eLLloAkvungsKdKTLO8AJlftYIKNfJr3GK3zK0ZCfzDDGUBttdGc8xFy1mCitvNKQtC3Q==", + "node_modules/buffer-alloc-unsafe": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/buffer-alloc-unsafe/-/buffer-alloc-unsafe-1.1.0.tgz", + "integrity": "sha512-TEM2iMIEQdJ2yjPJoSIsldnleVaAk1oW3DBVUykyOLsEsFmEc9kn+SFFPz+gl54KQNxlDnAwCXosOS9Okx2xAg==", "dev": true, - "requires": { - "http-proxy": "^1.17.0", - "is-glob": "^4.0.0", - "lodash": "^4.17.11", - "micromatch": "^3.1.10" - } + "optional": true }, - "http-signature": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", - "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha1-DTM+PwDqxQqhRUq9MO+MKl2ackI=", "dev": true, - "requires": { - "assert-plus": "^1.0.0", - "jsprim": "^1.2.2", - "sshpk": "^1.7.0" + "engines": { + "node": "*" } }, - "https-browserify": { + "node_modules/buffer-fill": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", - "integrity": "sha1-7AbBDgo0wPL68Zn3/X/Hj//QPHM=", - "dev": true - }, - "https-proxy-agent": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-4.0.0.tgz", - "integrity": "sha512-zoDhWrkR3of1l9QAL8/scJZyLu8j/gBkcwcaQOZh7Gyh/+uJQzGVETdgT30akuwkpL8HTRfssqI3BZuV18teDg==", + "resolved": "https://registry.npmjs.org/buffer-fill/-/buffer-fill-1.0.0.tgz", + "integrity": "sha1-+PeLdniYiO858gXNY39o5wISKyw=", "dev": true, - "requires": { - "agent-base": "5", - "debug": "4" - } + "optional": true }, - "human-signals": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-1.1.1.tgz", - "integrity": "sha512-SEQu7vl8KjNL2eoGBLF3+wAjpsNfA9XMlXAYj/3EdaNfAlxKthD1xjEQfGOUhllCGGJVNY34bRr6lPINhNjyZw==", + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, - "iconv-lite": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.2.tgz", - "integrity": "sha512-2y91h5OpQlolefMPmUlivelittSWy0rP+oYVpn6A7GwVHNE8AWzoYOBNmlwks3LobaJxgHCYZAnyNo2GgpNRNQ==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3.0.0" + "node_modules/builtin-modules": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", + "integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "dev": true - }, - "ieee754": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", - "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==" + "node_modules/builtins": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/builtins/-/builtins-5.0.1.tgz", + "integrity": "sha512-qwVpFEHNfhYJIzNRBvd2C1kyo6jz3ZSMPyyuR47OPdiKWlbYnZNyDWuyR175qDnAJLiCo5fBBqPb3RiXgWlkOQ==", + "dev": true, + "dependencies": { + "semver": "^7.0.0" + } }, - "iferr": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/iferr/-/iferr-0.1.5.tgz", - "integrity": "sha1-xg7taebY/bazEEofy8ocGS3FtQE=", - "dev": true + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "ignore": { - "version": "5.1.8", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.1.8.tgz", - "integrity": "sha512-BMpfD7PpiETpBl/A6S498BaIJ6Y/ABT93ETbby2fP00v4EbvPBXWEoaR1UBPKs3iR53pJY7EtZk5KACI57i1Uw==", - "dev": true + "node_modules/cache-base": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/cache-base/-/cache-base-1.0.1.tgz", + "integrity": "sha512-AKcdTnFSWATd5/GCPRxr2ChwIJ85CeyrEyjRHlKxQ56d4XJMGym0uAiKn0xbLOGOl3+yRpOTi484dVCEc5AUzQ==", + "dev": true, + "dependencies": { + "collection-visit": "^1.0.0", + "component-emitter": "^1.2.1", + "get-value": "^2.0.6", + "has-value": "^1.0.0", + "isobject": "^3.0.1", + "set-value": "^2.0.0", + "to-object-path": "^0.3.0", + "union-value": "^1.0.0", + "unset-value": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "ignore-emit-webpack-plugin": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/ignore-emit-webpack-plugin/-/ignore-emit-webpack-plugin-2.0.6.tgz", - "integrity": "sha512-/zC18RWCC2wz4ZwnS4UoujGWzvSKy28DLjtE+jrGBOXej6YdmityhBDzE8E0NlktEqi4tgdNbydX8B6G4haHSQ==", - "dev": true + "node_modules/cacheable": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.3.4.tgz", + "integrity": "sha512-djgxybDbw9fL/ZWMI3+CE8ZilNxcwFkVtDc1gJ+IlOSSWkSMPQabhV/XCHTQ6pwwN6aivXPZ43omTooZiX06Ew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.0.8", + "@cacheable/utils": "^2.4.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.9.0" + } }, - "imagemin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-6.1.0.tgz", - "integrity": "sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A==", + "node_modules/cacheable-request": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-2.1.4.tgz", + "integrity": "sha512-vag0O2LKZ/najSoUwDbVlnlCFvhBE/7mGTY2B5FgCBDcRD+oVV1HYTOwM6JZfMg/hIcM6IwnTZ1uQQL5/X3xIQ==", "dev": true, - "requires": { - "file-type": "^10.7.0", - "globby": "^8.0.1", - "make-dir": "^1.0.0", - "p-pipe": "^1.1.0", - "pify": "^4.0.1", - "replace-ext": "^1.0.0" - }, + "optional": true, "dependencies": { - "@nodelib/fs.stat": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", - "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", - "dev": true - }, - "dir-glob": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", - "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", - "dev": true, - "requires": { - "arrify": "^1.0.1", - "path-type": "^3.0.0" - } - }, - "fast-glob": { - "version": "2.2.7", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", - "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", - "dev": true, - "requires": { - "@mrmlnc/readdir-enhanced": "^2.2.1", - "@nodelib/fs.stat": "^1.1.2", - "glob-parent": "^3.1.0", - "is-glob": "^4.0.0", - "merge2": "^1.2.3", - "micromatch": "^3.1.10" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "globby": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", - "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", - "dev": true, - "requires": { - "array-union": "^1.0.1", - "dir-glob": "2.0.0", - "fast-glob": "^2.0.2", - "glob": "^7.1.2", - "ignore": "^3.3.5", - "pify": "^3.0.0", - "slash": "^1.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "ignore": { - "version": "3.3.10", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", - "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", - "dev": true - }, - "make-dir": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", - "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - }, - "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } - } - }, - "slash": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", - "integrity": "sha1-xB8vbDn8FtHNF61LXYlhFK5HDVU=", - "dev": true - } + "clone-response": "1.0.2", + "get-stream": "3.0.0", + "http-cache-semantics": "3.8.1", + "keyv": "3.0.0", + "lowercase-keys": "1.0.0", + "normalize-url": "2.0.1", + "responselike": "1.0.2" } }, - "imagemin-gifsicle": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz", - "integrity": "sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng==", + "node_modules/cacheable-request/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "exec-buffer": "^3.0.0", - "gifsicle": "^4.0.0", - "is-gif": "^3.0.0" + "engines": { + "node": ">=4" } }, - "imagemin-jpegtran": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz", - "integrity": "sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g==", + "node_modules/cacheable-request/node_modules/normalize-url": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-2.0.1.tgz", + "integrity": "sha512-D6MUW4K/VzoJ4rJ01JFKxDrtY1v9wrgzCX5f2qj/lzH1m/lW6MhUZFKerVsnyjOhOsYzI9Kqqak+10l4LvLpMw==", "dev": true, "optional": true, - "requires": { - "exec-buffer": "^3.0.0", - "is-jpg": "^2.0.0", - "jpegtran-bin": "^4.0.0" + "dependencies": { + "prepend-http": "^2.0.0", + "query-string": "^5.0.1", + "sort-keys": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "imagemin-optipng": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-6.0.0.tgz", - "integrity": "sha512-FoD2sMXvmoNm/zKPOWdhKpWdFdF9qiJmKC17MxZJPH42VMAp17/QENI/lIuP7LCUnLVAloO3AUoTSNzfhpyd8A==", + "node_modules/cacheable-request/node_modules/prepend-http": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-2.0.0.tgz", + "integrity": "sha512-ravE6m9Atw9Z/jjttRUZ+clIXogdghyZAuWJ3qEzjT+jI/dL1ifAqhZeC5VHzQp1MSt1+jxKkFNemj/iO7tVUA==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "exec-buffer": "^3.0.0", - "is-png": "^1.0.0", - "optipng-bin": "^5.0.0" + "engines": { + "node": ">=4" } }, - "imagemin-svgo": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-7.1.0.tgz", - "integrity": "sha512-0JlIZNWP0Luasn1HT82uB9nU9aa+vUj6kpT+MjPW11LbprXC+iC4HDwn1r4Q2/91qj4iy9tRZNsFySMlEpLdpg==", + "node_modules/cacheable-request/node_modules/query-string": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/query-string/-/query-string-5.1.1.tgz", + "integrity": "sha512-gjWOsm2SoGlgLEdAGt7a6slVOk9mGiXmPFMqrEhLQ68rhQuBnpfs3+EmlvqKyxnCo9/PPlF+9MtY02S1aFg+Jw==", "dev": true, "optional": true, - "requires": { - "is-svg": "^4.2.1", - "svgo": "^1.3.2" - }, "dependencies": { - "is-svg": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.2.1.tgz", - "integrity": "sha512-PHx3ANecKsKNl5y5+Jvt53Y4J7MfMpbNZkv384QNiswMKAWIbvcqbPz+sYbFKJI8Xv3be01GSFniPmoaP+Ai5A==", - "dev": true, - "optional": true, - "requires": { - "html-comment-regex": "^1.1.2" - } - } + "decode-uri-component": "^0.2.0", + "object-assign": "^4.1.0", + "strict-uri-encode": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "imagesloaded": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-4.1.4.tgz", - "integrity": "sha512-ltiBVcYpc/TYTF5nolkMNsnREHW+ICvfQ3Yla2Sgr71YFwQ86bDwV9hgpFhFtrGPuwEx5+LqOHIrdXBdoWwwsA==", - "requires": { - "ev-emitter": "^1.0.0" + "node_modules/cacheable-request/node_modules/sort-keys": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-2.0.0.tgz", + "integrity": "sha512-/dPCrG1s3ePpWm6yBbxZq5Be1dXGLyLn9Z791chDC3NFrpkVbWGzkBwPN1knaciexFXgRJ7hzdnwZ4stHSDmjg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "import-fresh": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", - "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", - "requires": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" + "node_modules/cacheable/node_modules/keyv": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@keyv/serialize": "^1.1.1" } }, - "import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "dev": true - }, - "import-local": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.0.2.tgz", - "integrity": "sha512-vjL3+w0oulAVZ0hBHnxa/Nm5TAurf9YLQJDhqRZyqb+VKGOB6LU8t9H1Nr5CIo16vh9XfJTOoHwU0B71S557gA==", + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", "dev": true, - "requires": { - "pkg-dir": "^4.2.0", - "resolve-cwd": "^3.0.0" - }, "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } - } + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", - "dev": true + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } }, - "indent-string": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", - "integrity": "sha1-ji1INIdCEhtKghi3oTfppSBJ3IA=", + "node_modules/call-bound": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", + "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", "dev": true, - "optional": true, - "requires": { - "repeating": "^2.0.0" + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "get-intrinsic": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "indexes-of": { + "node_modules/call-me-maybe": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz", - "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=", - "dev": true - }, - "infer-owner": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/infer-owner/-/infer-owner-1.0.4.tgz", - "integrity": "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==", + "resolved": "https://registry.npmjs.org/call-me-maybe/-/call-me-maybe-1.0.1.tgz", + "integrity": "sha1-JtII6onje1y95gJQoV8DHBak1ms=", "dev": true }, - "inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", - "requires": { - "once": "^1.3.0", - "wrappy": "1" + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "engines": { + "node": ">=6" } }, - "inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" - }, - "ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "dev": true + "node_modules/camel-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", + "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", + "dev": true, + "dependencies": { + "pascal-case": "^3.1.2", + "tslib": "^2.0.3" + } }, - "ink-docstrap": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/ink-docstrap/-/ink-docstrap-1.3.2.tgz", - "integrity": "sha512-STx5orGQU1gfrkoI/fMU7lX6CSP7LBGO10gXNgOZhwKhUqbtNjCkYSewJtNnLmWP1tAGN6oyEpG1HFPw5vpa5Q==", + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true, - "requires": { - "moment": "^2.14.1", - "sanitize-html": "^1.13.0" + "engines": { + "node": ">=6" } }, - "inquirer": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", - "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "node_modules/camelcase-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-2.1.0.tgz", + "integrity": "sha512-bA/Z/DERHKqoEOrp+qeGKw1QlvEQkGZSc0XaY6VnTxZr+Kv1G5zFwttpjv8qxZ/sBPT4nthwZaAcsAZTJlSKXQ==", "dev": true, - "requires": { - "ansi-escapes": "^3.0.0", - "chalk": "^2.0.0", - "cli-cursor": "^2.1.0", - "cli-width": "^2.0.0", - "external-editor": "^2.1.0", - "figures": "^2.0.0", - "lodash": "^4.3.0", - "mute-stream": "0.0.7", - "run-async": "^2.2.0", - "rxjs": "^5.5.2", - "string-width": "^2.1.0", - "strip-ansi": "^4.0.0", - "through": "^2.3.6" - }, + "license": "MIT", + "optional": true, "dependencies": { - "ansi-escapes": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", - "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", - "dev": true - }, - "ansi-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", - "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", - "dev": true - }, - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "figures": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", - "integrity": "sha1-OrGi0qYsi/tDGgyUy3l6L84nyWI=", - "dev": true, - "requires": { - "escape-string-regexp": "^1.0.5" - } - }, - "string-width": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", - "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", - "dev": true, - "requires": { - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^4.0.0" - } - }, - "strip-ansi": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", - "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", - "dev": true, - "requires": { - "ansi-regex": "^3.0.0" - } - } + "camelcase": "^2.0.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "install-changed": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/install-changed/-/install-changed-1.1.0.tgz", - "integrity": "sha512-APUWMdQnwGcyv9bmuvgxCcrR6qtD996+hofEEAPGSjsvGIZuBpBF0yrYAKOl9tmhm2AzxJC4EXUbxZ/SId4NIA==", - "dev": true - }, - "internal-ip": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/internal-ip/-/internal-ip-4.3.0.tgz", - "integrity": "sha512-S1zBo1D6zcsyuC6PMmY5+55YMILQ9av8lotMx447Bq6SAgo/sDK6y6uUKmuYhW7eacnIhFfsPmCNYdDzsnnDCg==", + "node_modules/camelcase-keys/node_modules/camelcase": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-2.1.1.tgz", + "integrity": "sha512-DLIsRzJVBQu72meAKPkWQOLcujdXT32hwdfnkI1frSiSRMK1MofjKHf+MEx0SB6fjEFXL8fBDv1dKymBlOp4Qw==", "dev": true, - "requires": { - "default-gateway": "^4.2.0", - "ipaddr.js": "^1.9.0" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "internal-slot": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.0.3.tgz", - "integrity": "sha512-O0DB1JC/sPyZl7cIo78n5dR7eUSwwpYPiXRhTzNxZVAMUuB8vlnRFyLxdrVToks6XPLVnFfbzaVd5WLjhgg+vA==", - "requires": { - "get-intrinsic": "^1.1.0", - "has": "^1.0.3", - "side-channel": "^1.0.4" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } + "node_modules/caniuse-api": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", + "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", + "dev": true, + "dependencies": { + "browserslist": "^4.0.0", + "caniuse-lite": "^1.0.0", + "lodash.memoize": "^4.1.2", + "lodash.uniq": "^4.5.0" } }, - "interpret": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.4.0.tgz", - "integrity": "sha512-agE4QfB2Lkp9uICn7BAqoscw4SZP9kTE2hxiFI3jBPmXJfdqiahTbUuKGsMoN2GtqL9AxhYioAcVvgsb1HvRbA==", - "dev": true + "node_modules/caniuse-lite": { + "version": "1.0.30001780", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001780.tgz", + "integrity": "sha512-llngX0E7nQci5BPJDqoZSbuZ5Bcs9F5db7EtgfwBerX9XGtkkiO4NwfDDIRzHTTwcYC8vC7bmeUEPGrKlR/TkQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" }, - "into-stream": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", - "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "node_modules/capital-case": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/capital-case/-/capital-case-1.0.4.tgz", + "integrity": "sha512-ds37W8CytHgwnhGGTi88pcPyR15qoNkOpYwmMMfnWqqWgESapLqvDx6huFjQ5vqWSn2Z06173XNA7LtMOeUh1A==", "dev": true, - "optional": true, - "requires": { - "from2": "^2.1.1", - "p-is-promise": "^1.1.0" + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" } }, - "ip": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.5.tgz", - "integrity": "sha1-vd7XARQpCCjAoDnnLvJfWq7ENUo=", + "node_modules/caseless": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", + "integrity": "sha1-G2gcIf+EAzyCZUMJBolCDRhxUdw=", "dev": true }, - "ip-regex": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ip-regex/-/ip-regex-2.1.0.tgz", - "integrity": "sha1-+ni/XS5pE8kRzp+BnuUUa7bYROk=" - }, - "ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true - }, - "irregular-plurals": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.3.0.tgz", - "integrity": "sha512-MVBLKUTangM3EfRPFROhmWQQKRDsrgI83J8GS3jXy+OwYqiR2/aoWndYQ5416jLE3uaGgLH7ncme3X9y09gZ3g==", - "dev": true - }, - "is-absolute": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", - "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "node_modules/catharsis": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/catharsis/-/catharsis-0.9.0.tgz", + "integrity": "sha512-prMTQVpcns/tzFgFVkVp6ak6RykZyWb3gu8ckUpd6YkTlacOd3DXGJjIpD4Q6zJirizvaiAjSSHlOsA+6sNh2A==", "dev": true, - "requires": { - "is-relative": "^1.0.0", - "is-windows": "^1.0.1" + "dependencies": { + "lodash": "^4.17.15" + }, + "engines": { + "node": ">= 10" } }, - "is-absolute-url": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-2.1.0.tgz", - "integrity": "sha1-UFMN+4T8yap9vnhS6Do3uTufKqY=", - "dev": true - }, - "is-accessor-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-0.1.6.tgz", - "integrity": "sha1-qeEss66Nh2cn7u84Q/igiXtcmNY=", + "node_modules/caw": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/caw/-/caw-2.0.1.tgz", + "integrity": "sha512-Cg8/ZSBEa8ZVY9HspcGUYaK63d/bN7rqS3CYCzEGUxuYv6UlmcjzDUz2fCFFHyTvUW5Pk0I+3hkA3iXlIj6guA==", "dev": true, - "requires": { - "kind-of": "^3.0.2" + "optional": true, + "dependencies": { + "get-proxy": "^2.0.0", + "isurl": "^1.0.0-alpha5", + "tunnel-agent": "^0.6.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" } }, - "is-alphabetical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-1.0.4.tgz", - "integrity": "sha512-DwzsA04LQ10FHTZuL0/grVDk4rFoVH1pjAToYwBrHSxcrBIGQuXrQMtD5U1b0U2XVgKZCTLLP8u2Qxqhy3l2Vg==", - "dev": true + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } }, - "is-alphanumerical": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-1.0.4.tgz", - "integrity": "sha512-UzoZUr+XfVz3t3v4KyGEniVL9BDRoQtY7tOyrRybkVNjDFWyo1yhXNGrrBTQxp3ib9BLAWs7k2YKBQsFRkZG9A==", + "node_modules/change-case": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/change-case/-/change-case-4.1.2.tgz", + "integrity": "sha512-bSxY2ws9OtviILG1EiY5K7NNxkqg/JnRnFxLtKQ96JaviiIxi7djMrSd0ECT9AC+lttClmYwKw53BWpOMblo7A==", "dev": true, - "requires": { - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0" + "dependencies": { + "camel-case": "^4.1.2", + "capital-case": "^1.0.4", + "constant-case": "^3.0.4", + "dot-case": "^3.0.4", + "header-case": "^2.0.4", + "no-case": "^3.0.4", + "param-case": "^3.0.4", + "pascal-case": "^3.1.2", + "path-case": "^3.0.4", + "sentence-case": "^3.0.4", + "snake-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "is-arguments": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.1.0.tgz", - "integrity": "sha512-1Ij4lOMPl/xB5kBDn7I+b2ttPMKa8szhEIrXDuXQD/oe3HJLTLhqhgGspwgyGd6MOywBUqVvYicF72lkgDnIHg==", + "node_modules/char-regex": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", + "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", "dev": true, - "requires": { - "call-bind": "^1.0.0" + "engines": { + "node": ">=10" } }, - "is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=" + "node_modules/chardet": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/chardet/-/chardet-0.4.2.tgz", + "integrity": "sha1-tUc7M9yXxCTl2Y3IfVXU2KKci/I=", + "dev": true }, - "is-bigint": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.0.4.tgz", - "integrity": "sha512-zB9CruMamjym81i2JZ3UMn54PKGsQzsJeo6xvN3HJJ4CAsQNB6iRutp2To77OfCNuoxspsIhzaPoO1zyCEhFOg==", - "requires": { - "has-bigints": "^1.0.1" + "node_modules/check-node-version": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/check-node-version/-/check-node-version-4.2.1.tgz", + "integrity": "sha512-YYmFYHV/X7kSJhuN/QYHUu998n/TRuDe8UenM3+m5NrkiH670lb9ILqHIvBencvJc4SDh+XcbXMR4b+TtubJiw==", + "dev": true, + "dependencies": { + "chalk": "^3.0.0", + "map-values": "^1.0.1", + "minimist": "^1.2.0", + "object-filter": "^1.0.2", + "run-parallel": "^1.1.4", + "semver": "^6.3.0" + }, + "bin": { + "check-node-version": "bin.js" + }, + "engines": { + "node": ">=8.3.0" } }, - "is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "requires": { - "binary-extensions": "^2.0.0" + "node_modules/check-node-version/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "is-boolean-object": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.2.tgz", - "integrity": "sha512-gDYaKHJmnj4aWxyj6YHyXVpdQawtVLHU5cb+eztPGczf6cjuTdwve5ZIEfgXqH4e57An1D1AKf8CZ3kYrQRqYA==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - } + "node_modules/check-node-version/node_modules/chalk": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", + "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=8" } }, - "is-buffer": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", - "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==" - }, - "is-callable": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.2.tgz", - "integrity": "sha512-dnMqspv5nU3LoewK2N/y7KLtxtakvTuaCsU9FU50/QDmdbHNy/4/JuRtMHqRU22o3q+W89YQndQEeCVwK+3qrA==" - }, - "is-ci": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-2.0.0.tgz", - "integrity": "sha512-YfJT7rkpQB0updsdHLGWrvhBJfcfzNNawYDNIyQXJz0IViGf75O8EBPKSdvw2rF+LGCsX4FZ8tcr3b19LcZq4w==", + "node_modules/check-node-version/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "ci-info": "^2.0.0" + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "is-color-stop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-color-stop/-/is-color-stop-1.1.0.tgz", - "integrity": "sha1-z/9HGu5N1cnhWFmPvhKWe1za00U=", + "node_modules/check-node-version/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/check-node-version/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "css-color-names": "^0.0.4", - "hex-color-regex": "^1.1.0", - "hsl-regex": "^1.0.0", - "hsla-regex": "^1.0.0", - "rgb-regex": "^1.0.1", - "rgba-regex": "^1.0.0" + "engines": { + "node": ">=8" } }, - "is-core-module": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.2.0.tgz", - "integrity": "sha512-XRAfAdyyY5F5cOXn7hYQDqh2Xmii+DEfIcQGxK/uNwMHhIkPWO0g8msXcbzLe+MpGoR951MlqM/2iIlU4vKDdQ==", - "requires": { - "has": "^1.0.3" + "node_modules/check-node-version/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" } }, - "is-data-descriptor": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-0.1.4.tgz", - "integrity": "sha1-C17mSDiOLIYCgueT8YVv7D8wG1Y=", + "node_modules/check-node-version/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "kind-of": "^3.0.2" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "is-date-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.2.tgz", - "integrity": "sha512-USlDT524woQ08aoZFzh3/Z6ch9Y/EWXEHQ/AaRN0SkKq4t2Jw2R2339tSXmwuVoY7LLlBCbOIlx2myP/L5zk0g==" - }, - "is-decimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-1.0.4.tgz", - "integrity": "sha512-RGdriMmQQvZ2aqaQq3awNA6dCGtKpiDFcOzrTWrDAT2MiWrKQVPmxLGHl7Y2nNu6led0kEyoX0enY0qXYsv9zw==", - "dev": true - }, - "is-descriptor": { - "version": "0.1.6", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.6.tgz", - "integrity": "sha512-avDYr0SB3DwO9zsMov0gKCESFYqCnE4hq/4z3TdUlukEy5t9C0YRq7HLrsN52NAcqXKaepeCD0n+B0arnVG3Hg==", + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", "dev": true, - "requires": { - "is-accessor-descriptor": "^0.1.6", - "is-data-descriptor": "^0.1.4", - "kind-of": "^5.0.0" - }, + "license": "MIT", "dependencies": { - "kind-of": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-5.1.0.tgz", - "integrity": "sha512-NGEErnH6F2vUuXDh+OlbcKW7/wOcfdRHaZ7VWtqCztfHri/++YKmP51OdWeGPuqCOba6kk2OTe5d02VmTB80Pw==", - "dev": true - } + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" } }, - "is-directory": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/is-directory/-/is-directory-0.3.1.tgz", - "integrity": "sha1-YTObbyR1/Hcv2cnYP1yFddwVSuE=", - "dev": true - }, - "is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "node_modules/chokidar/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "optional": true - }, - "is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha1-YrEQ4omkcUGOPsNqYX1HLjAd/Ik=", - "dev": true - }, - "is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=" + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } }, - "is-finite": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", - "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "node_modules/chokidar/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "optional": true - }, - "is-fullwidth-code-point": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", - "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=" - }, - "is-generator-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", - "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", - "dev": true + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, - "is-gif": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz", - "integrity": "sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw==", + "node_modules/chokidar/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", "dev": true, - "optional": true, - "requires": { - "file-type": "^10.4.0" + "engines": { + "node": ">=0.12.0" } }, - "is-glob": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.1.tgz", - "integrity": "sha512-5G0tKtBTFImOqDnLB2hG6Bp2qcKEFduo4tZu9MT/H6NQv/ghhy30o55ufafxJ/LdH79LLs2Kfrn85TLKyA7BUg==", - "requires": { - "is-extglob": "^2.1.1" + "node_modules/chokidar/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "is-hexadecimal": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-1.0.4.tgz", - "integrity": "sha512-gyPJuv83bHMpocVYoqof5VDiZveEoGoFL8m3BXNb2VW8Xs+rz9kqO8LOQ5DH6EsuvilT1ApazU0pyl+ytbPtlw==", - "dev": true - }, - "is-jpg": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", - "integrity": "sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=", + "node_modules/chrome-launcher": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/chrome-launcher/-/chrome-launcher-1.1.2.tgz", + "integrity": "sha512-YclTJey34KUm5jB1aEJCq807bSievi7Nb/TU4Gu504fUYi3jw3KCIaH6L7nFWQhdEgH3V+wCh+kKD1P5cXnfxw==", "dev": true, - "optional": true + "dependencies": { + "@types/node": "*", + "escape-string-regexp": "^4.0.0", + "is-wsl": "^2.2.0", + "lighthouse-logger": "^2.0.1" + }, + "bin": { + "print-chrome-path": "bin/print-chrome-path.js" + }, + "engines": { + "node": ">=12.13.0" + } }, - "is-natural-number": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", - "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "node_modules/chrome-launcher/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "optional": true - }, - "is-negative-zero": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.1.tgz", - "integrity": "sha512-2z6JzQvZRa9A2Y7xC6dQQm4FSTSTNWjKIYYTt4246eMTJmIo0Q+ZyOsU66X8lxK1AbB92dFeglPLrhwpeRKO6w==" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "is-number": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", - "integrity": "sha1-JP1iAaR4LPUFYcgQJ2r8fRLXEZU=", + "node_modules/chrome-trace-event": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.3.tgz", + "integrity": "sha512-p3KULyQg4S7NIHixdwbGX+nFHkoBiA4YQmyWtjb8XngSKV124nJmRysgAeujbUVb15vh+RvFUfCPqU7rXk+hZg==", "dev": true, - "requires": { - "kind-of": "^3.0.2" + "engines": { + "node": ">=6.0" } }, - "is-number-object": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.0.6.tgz", - "integrity": "sha512-bEVOqiRcvo3zO1+G2lVMy+gkkEm9Yh7cDMRusKKu5ZJKPUYSJwICTKZrNKHA2EbSP0Tu0+6B/emsYNHZyn6K8g==", - "requires": { - "has-tostringtag": "^1.0.0" + "node_modules/chromium-bidi": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.6.3.tgz", + "integrity": "sha512-qXlsCmpCZJAnoTYI83Iu6EdYQpMYdVkCfq08KDh2pmlVqK5t5IA9mGs4/LwCwp4fqisSOMXZxP3HIh8w8aRn0A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "mitt": "3.0.1", + "urlpattern-polyfill": "10.0.0", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" } }, - "is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "dev": true - }, - "is-object": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", - "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "node_modules/ci-info": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", + "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", "dev": true, - "optional": true + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "is-path-cwd": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-2.2.0.tgz", - "integrity": "sha512-w942bTcih8fdJPJmQHFzkS76NEP8Kzzvmw92cXsazb8intwLqPibPPdXf4ANdKV3rYMuuQYGIWtvz9JilB3NFQ==", + "node_modules/cjs-module-lexer": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cjs-module-lexer/-/cjs-module-lexer-1.2.3.tgz", + "integrity": "sha512-0TNiGstbQmCFwt4akjjBg5pLRTSyj/PkWQ1ZoO2zntmg9yLqSRxwEa4iCfQLGjqhiqBfOJa7W/E8wfGrTDmlZQ==", "dev": true }, - "is-path-in-cwd": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-2.1.0.tgz", - "integrity": "sha512-rNocXHgipO+rvnP6dk3zI20RpOtrAM/kzbB258Uw5BWr3TpXi861yzjo16Dn4hUox07iw5AyeMLHWsujkjzvRQ==", + "node_modules/class-utils": { + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/class-utils/-/class-utils-0.3.6.tgz", + "integrity": "sha512-qOhPa/Fj7s6TY8H8esGu5QNpMMQxz79h+urzrNYN6mn+9BnxlDGf5QZ+XeCDsxSjPqsSR56XOZOJmpeurnLMeg==", "dev": true, - "requires": { - "is-path-inside": "^2.1.0" + "dependencies": { + "arr-union": "^3.1.0", + "define-property": "^0.2.5", + "isobject": "^3.0.0", + "static-extend": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-path-inside": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-2.1.0.tgz", - "integrity": "sha512-wiyhTzfDWsvwAW53OBWF5zuvaOGlZ6PwYxAbPVDhpm+gM09xKQGjBq/8uYN12aDvMxnAnq3dxTyoSoRNmg5YFg==", + "node_modules/class-utils/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, - "requires": { - "path-is-inside": "^1.0.2" + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-plain-obj": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", - "integrity": "sha1-caUMhCnfync8kqOQpKA7OfzVHT4=", - "dev": true - }, - "is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "node_modules/clean-css": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.2.tgz", + "integrity": "sha512-JVJbM+f3d3Q704rF4bqQ5UUyTtuJ0JRKNbTKVEeujCCBoMdkEi+V+e8oktO9qGQNSvHrFTM6JZRXrUvGR1czww==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "dependencies": { + "source-map": "~0.6.0" + }, + "engines": { + "node": ">= 10.0" } }, - "is-png": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz", - "integrity": "sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=", + "node_modules/clean-css/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "optional": true + "engines": { + "node": ">=0.10.0" + } }, - "is-potential-custom-element-name": { + "node_modules/cli": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", - "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", - "dev": true - }, - "is-promise": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz", - "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==" - }, - "is-regex": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.1.tgz", - "integrity": "sha512-1+QkEcxiLlB7VEyFtyBg94e08OAsvq7FUBgApTq/w2ymCLyKJgDPsybBENVtA7XCQEgEXxKPonG+mvYRxh/LIg==", - "requires": { - "has-symbols": "^1.0.1" + "resolved": "https://registry.npmjs.org/cli/-/cli-1.0.1.tgz", + "integrity": "sha1-IoF1NPJL+klQw01TLUjsvGIbjBQ=", + "dev": true, + "dependencies": { + "exit": "0.1.2", + "glob": "^7.1.1" + }, + "engines": { + "node": ">=0.2.5" } }, - "is-regexp": { + "node_modules/cli-cursor": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-2.1.0.tgz", - "integrity": "sha512-OZ4IlER3zmRIoB9AqNhEggVxqIH4ofDns5nRrPS6yQxXE1TPCUpFznBfRQmQa8uC+pXqjMnukiJBxCisIxiLGA==", - "dev": true - }, - "is-relative": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", - "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-2.1.0.tgz", + "integrity": "sha1-s12sN2R5+sw+lHR9QdDQ9SOP/LU=", "dev": true, - "requires": { - "is-unc-path": "^1.0.0" + "dependencies": { + "restore-cursor": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "is-resolvable": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.1.0.tgz", - "integrity": "sha512-qgDYXFSR5WvEfuS5dMj6oTMEbrrSaM0CrFk2Yiq/gXnBvD9pMa2jGXxyhGLfvhZpuMZe18CJpFxAt3CRs42NMg==", + "node_modules/cli-width": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.2.1.tgz", + "integrity": "sha512-GRMWDxpOB6Dgk2E5Uo+3eEBvtOOlimMmpbFiKuLFnQzYDavtLFY3K5ona41jgN/WdRZtG7utuVSVTL4HbZHGkw==", "dev": true }, - "is-retry-allowed": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", - "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "node_modules/clipboard": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.11.tgz", + "integrity": "sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==", + "dependencies": { + "good-listener": "^1.2.2", + "select": "^1.1.2", + "tiny-emitter": "^2.0.0" + } + }, + "node_modules/cliui": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-8.0.1.tgz", + "integrity": "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==", "dev": true, - "optional": true + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.1", + "wrap-ansi": "^7.0.0" + }, + "engines": { + "node": ">=12" + } }, - "is-stream": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", - "integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=", - "dev": true + "node_modules/cliui/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "is-string": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.0.7.tgz", - "integrity": "sha512-tE2UXzivje6ofPW7l23cjDOMa09gb7xlAqG6jG5ej6uPV32TlWP3NKPigtaGeHNu9fohccRYvIiZMfOOnOYUtg==", - "requires": { - "has-tostringtag": "^1.0.0" + "node_modules/cliui/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "is-subset": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-subset/-/is-subset-0.1.1.tgz", - "integrity": "sha1-ilkRfZMt4d4A8kX83TnOQ/HpOaY=", - "dev": true + "node_modules/cliui/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } }, - "is-symbol": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.3.tgz", - "integrity": "sha512-OwijhaRSgqvhm/0ZdAcXNZt9lYdKFpcRDT5ULUuYXPoT794UNOdU+gpT6Rzo7b4V2HUl/op6GqY894AZwv9faQ==", - "requires": { - "has-symbols": "^1.0.1" + "node_modules/cliui/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "is-touch-device": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-touch-device/-/is-touch-device-1.0.1.tgz", - "integrity": "sha512-LAYzo9kMT1b2p19L/1ATGt2XcSilnzNlyvq6c0pbPRVisLbAPpLqr53tIJS00kvrTkj0HtR8U7+u8X0yR8lPSw==" + "node_modules/clone": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/clone/-/clone-2.1.2.tgz", + "integrity": "sha512-3Pe/CF1Nn94hyhIYpjtiLhdCoEoz0DqQ+988E9gmeEdQZlojxnOb74wctFyuwWQHzqyf9X7C7MG8juUpqBJT8w==", + "license": "MIT", + "engines": { + "node": ">=0.8" + } }, - "is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", - "dev": true + "node_modules/clone-deep": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-0.2.4.tgz", + "integrity": "sha512-we+NuQo2DHhSl+DP6jlUiAhyAjBQrYnpOk15rN6c6JSPScjiCLh8IbSU+VTcph6YS3o7mASE8a0+gbZ7ChLpgg==", + "dev": true, + "dependencies": { + "for-own": "^0.1.3", + "is-plain-object": "^2.0.1", + "kind-of": "^3.0.2", + "lazy-cache": "^1.0.3", + "shallow-clone": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } }, - "is-unc-path": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", - "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "node_modules/clone-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "requires": { - "unc-path-regex": "^0.1.2" + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "is-unicode-supported": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", - "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", - "dev": true + "node_modules/clone-response": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/clone-response/-/clone-response-1.0.2.tgz", + "integrity": "sha1-0dyXOSAxTfZ/vrlCI7TuNQI56Ws=", + "dev": true, + "optional": true, + "dependencies": { + "mimic-response": "^1.0.0" + } }, - "is-url": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-url/-/is-url-1.2.4.tgz", - "integrity": "sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==" + "node_modules/co": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", + "integrity": "sha512-QVb0dM5HvG+uaxitm8wONl7jltx8dqhfU33DcqtOZcLSVIKSDDLDi7+0LbAKiyI8hD9u42m2YxXSkMGWThaecQ==", + "dev": true, + "engines": { + "iojs": ">= 1.0.0", + "node": ">= 0.12.0" + } }, - "is-utf8": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", - "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", - "dev": true + "node_modules/coa": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/coa/-/coa-2.0.2.tgz", + "integrity": "sha512-q5/jG+YQnSy4nRTV4F7lPepBJZ8qBNJJDBuJdoejDyLXgmL7IEo+Le2JDZudFTFt7mrCqIRaSjws4ygRCTCAXA==", + "dev": true, + "optional": true, + "dependencies": { + "@types/q": "^1.5.1", + "chalk": "^2.4.1", + "q": "^1.1.2" + }, + "engines": { + "node": ">= 4.0" + } }, - "is-whitespace-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-whitespace-character/-/is-whitespace-character-1.0.4.tgz", - "integrity": "sha512-SDweEzfIZM0SJV0EUga669UTKlmL0Pq8Lno0QDQsPnvECB3IM2aP0gdx5TrU0A01MAPfViaZiI2V1QMZLaKK5w==", - "dev": true + "node_modules/coa/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "optional": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } }, - "is-windows": { + "node_modules/codemirror": { + "version": "5.65.20", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-5.65.20.tgz", + "integrity": "sha512-i5dLDDxwkFCbhjvL2pNjShsojoL3XHyDwsGv1jqETUoW+lzpBKKqNTUWgQwVAOa0tUm4BwekT455ujafi8payA==", + "license": "MIT" + }, + "node_modules/coffee-script": { + "version": "1.12.7", + "resolved": "https://registry.npmjs.org/coffee-script/-/coffee-script-1.12.7.tgz", + "integrity": "sha512-fLeEhqwymYat/MpTPUjSKHVYYl0ec2mOyALEMLmzr5i1isuG+6jfI2j2d5oBO3VIzgUXgBVIcOT9uH1TFxBckw==", + "deprecated": "CoffeeScript on NPM has moved to \"coffeescript\" (no hyphen)", + "dev": true, + "bin": { + "cake": "bin/cake", + "coffee": "bin/coffee" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/collect-v8-coverage": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", - "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", + "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==", "dev": true }, - "is-word-character": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/is-word-character/-/is-word-character-1.0.4.tgz", - "integrity": "sha512-5SMO8RVennx3nZrqtKwCGyyetPE9VDba5ugvKLaD4KopPG5kR4mQ7tNt/r7feL5yt5h3lpuBbIUmCOG2eSzXHA==", + "node_modules/collection-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/collection-visit/-/collection-visit-1.0.0.tgz", + "integrity": "sha1-S8A3PBZLwykbTTaMgpzxqApZ3KA=", + "dev": true, + "dependencies": { + "map-visit": "^1.0.0", + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true, + "license": "MIT" + }, + "node_modules/colord": { + "version": "2.9.3", + "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", + "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", "dev": true }, - "is-wsl": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-1.1.0.tgz", - "integrity": "sha1-HxbkqiKwTRM2tmGIpmrzxgDDpm0=", + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", "dev": true }, - "is2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is2/-/is2-2.0.1.tgz", - "integrity": "sha512-+WaJvnaA7aJySz2q/8sLjMb2Mw14KTplHmSwcSpZ/fWJPkUmqw3YTzSWbPJ7OAwRvdYTWF2Wg+yYJ1AdP5Z8CA==", - "requires": { - "deep-is": "^0.1.3", - "ip-regex": "^2.1.0", - "is-url": "^1.2.2" + "node_modules/colorjs.io": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/colorjs.io/-/colorjs.io-0.6.1.tgz", + "integrity": "sha512-8lyR2wHzuIykCpqHKgluGsqQi5iDm3/a2IgP2GBZrasn2sBRkE4NOGsglZxWLs/jZQoNkmA/KM/8NV16rLUdBg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/color" } }, - "isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=" + "node_modules/colors": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/colors/-/colors-1.1.2.tgz", + "integrity": "sha1-FopHAXVran9RoSzgyXv6KMCE7WM=", + "dev": true, + "engines": { + "node": ">=0.1.90" + } }, - "isbinaryfile": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.8.tgz", - "integrity": "sha512-53h6XFniq77YdW+spoRrebh0mnmTxRPTlcuIArO57lmMdq4uBKFKaeTjnb92oYWrSn/LVL+LT+Hap2tFQj8V+w==" + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } }, - "isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "dev": true }, - "isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=", - "dev": true + "node_modules/comment-parser": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/comment-parser/-/comment-parser-1.4.1.tgz", + "integrity": "sha512-buhp5kePrmda3vhc5B9t7pUQXAb2Tnd0qgpkIhPhkHXxJpiPJ11H0ZEU0oBpJ2QztSbzG/ZxMj/CHsYJqRHmyg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.0.0" + } }, - "isstream": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", - "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "node_modules/common-path-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", + "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", "dev": true }, - "istanbul-lib-coverage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.0.0.tgz", - "integrity": "sha512-UiUIqxMgRDET6eR+o5HbfRYP1l0hqkWOs7vNxC/mggutCMUIhWMm8gAHb8tHlyfD3/l6rlgNA5cKdDzEAf6hEg==", + "node_modules/component-emitter": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz", + "integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==", "dev": true }, - "istanbul-lib-instrument": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-4.0.3.tgz", - "integrity": "sha512-BXgQl9kf4WTCPCCpmFGoJkz/+uhvm7h7PFKUYxh7qarQd3ER33vHG//qaE8eN25l07YqZPpHXU9I09l/RD5aGQ==", + "node_modules/compressible": { + "version": "2.0.18", + "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", + "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", "dev": true, - "requires": { - "@babel/core": "^7.7.5", - "@istanbuljs/schema": "^0.1.2", - "istanbul-lib-coverage": "^3.0.0", - "semver": "^6.3.0" - }, "dependencies": { - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - } + "mime-db": ">= 1.43.0 < 2" + }, + "engines": { + "node": ">= 0.6" } }, - "istanbul-lib-report": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.0.tgz", - "integrity": "sha512-wcdi+uAKzfiGT2abPpKZ0hSU1rGQjUQnLvtY5MpQ7QCTahD3VODhcu4wcfY1YtkGaDD5yuydOLINXsfbus9ROw==", + "node_modules/compression": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", + "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", "dev": true, - "requires": { - "istanbul-lib-coverage": "^3.0.0", - "make-dir": "^3.0.0", - "supports-color": "^7.1.0" - }, + "license": "MIT", "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", - "dev": true, - "requires": { - "semver": "^6.0.0" - } - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "bytes": "3.1.2", + "compressible": "~2.0.18", + "debug": "2.6.9", + "negotiator": "~0.6.4", + "on-headers": "~1.1.0", + "safe-buffer": "5.2.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "istanbul-lib-source-maps": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.0.tgz", - "integrity": "sha512-c16LpFRkR8vQXyHZ5nLpY35JZtzj1PQY1iZmesUbf1FZHbIupcWfjgOXBY9YHkLEQ6puz1u4Dgj6qmU/DisrZg==", + "node_modules/compression/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "requires": { - "debug": "^4.1.1", - "istanbul-lib-coverage": "^3.0.0", - "source-map": "^0.6.1" - }, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "ms": "2.0.0" } }, - "istanbul-reports": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.0.2.tgz", - "integrity": "sha512-9tZvz7AiR3PEDNGiV9vIouQ/EAcqMXFmkcA1CDFTwOB98OZVDL0PH9glHotf5Ugp6GCOTypfzGWI/OqjWNCRUw==", + "node_modules/compression/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/compression/node_modules/negotiator": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", + "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", "dev": true, - "requires": { - "html-escaper": "^2.0.0", - "istanbul-lib-report": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "isurl": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", - "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", + "dev": true + }, + "node_modules/config-chain": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.12.tgz", + "integrity": "sha512-a1eOIcu8+7lUInge4Rpf/n4Krkf3Dd9lqhljRzII1/Zno/kRtUWnznPO3jOKBmTEktkt3fkxisUcivoj0ebzoA==", "dev": true, "optional": true, - "requires": { - "has-to-string-tag-x": "^1.2.0", - "is-object": "^1.0.1" + "dependencies": { + "ini": "^1.3.4", + "proto-list": "~1.2.1" } }, - "jest": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest/-/jest-26.6.3.tgz", - "integrity": "sha512-lGS5PXGAzR4RF7V5+XObhqz2KZIDUA1yD0DG6pBVmy10eh0ZIXQImRuzocsI/N2XZ1GrLFwTS27In2i2jlpq1Q==", + "node_modules/configstore": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/configstore/-/configstore-5.0.1.tgz", + "integrity": "sha512-aMKprgk5YhBNyH25hj8wGt2+D52Sw1DRRIzqBwLp2Ya9mFmY8KPvvtvmna8SxVR9JMZ4kzMD68N22vlaRpkeFA==", "dev": true, - "requires": { - "@jest/core": "^26.6.3", - "import-local": "^3.0.2", - "jest-cli": "^26.6.3" + "dependencies": { + "dot-prop": "^5.2.0", + "graceful-fs": "^4.1.2", + "make-dir": "^3.0.0", + "unique-string": "^2.0.0", + "write-file-atomic": "^3.0.0", + "xdg-basedir": "^4.0.0" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/configstore/node_modules/write-file-atomic": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", + "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "dev": true, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "jest-cli": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-26.6.3.tgz", - "integrity": "sha512-GF9noBSa9t08pSyl3CY4frMrqp+aQXFGFkf5hEPbh/pIUFYWMK6ZLTfbmadxJVcJrdRoChlWQsA2VkJcDFK8hg==", - "dev": true, - "requires": { - "@jest/core": "^26.6.3", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "import-local": "^3.0.2", - "is-ci": "^2.0.0", - "jest-config": "^26.6.3", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "prompts": "^2.0.1", - "yargs": "^15.4.1" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "imurmurhash": "^0.1.4", + "is-typedarray": "^1.0.0", + "signal-exit": "^3.0.2", + "typedarray-to-buffer": "^3.1.5" } }, - "jest-changed-files": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-26.6.2.tgz", - "integrity": "sha512-fDS7szLcY9sCtIip8Fjry9oGf3I2ht/QT21bAHm5Dmf0mD4X3ReNUf17y+bO6fR8WgbIZTlbyG1ak/53cbRzKQ==", + "node_modules/connect-history-api-fallback": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", + "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "execa": "^4.0.0", - "throat": "^5.0.0" - }, - "dependencies": { - "cross-spawn": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", - "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", - "dev": true, - "requires": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - } - }, - "execa": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/execa/-/execa-4.1.0.tgz", - "integrity": "sha512-j5W0//W7f8UxAn8hXVnwG8tLwdiUy4FJLcSupCg6maBYZDpyBvTApK7KyuI4bKj8KOh1r2YH+6ucuYtJv1bTZA==", - "dev": true, - "requires": { - "cross-spawn": "^7.0.0", - "get-stream": "^5.0.0", - "human-signals": "^1.1.1", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.0", - "onetime": "^5.1.0", - "signal-exit": "^3.0.2", - "strip-final-newline": "^2.0.0" - } - }, - "get-stream": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", - "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", - "dev": true, - "requires": { - "pump": "^3.0.0" - } - }, - "is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "dev": true - }, - "npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "dev": true, - "requires": { - "path-key": "^3.0.0" - } - }, - "path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "dev": true - }, - "shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "dev": true, - "requires": { - "shebang-regex": "^3.0.0" - } - }, - "shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "dev": true - } + "engines": { + "node": ">=0.8" } }, - "jest-circus": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-26.6.3.tgz", - "integrity": "sha512-ACrpWZGcQMpbv13XbzRzpytEJlilP/Su0JtNCi5r/xLpOUhnaIJr8leYYpLEMgPFURZISEHrnnpmB54Q/UziPw==", + "node_modules/console-stream": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/console-stream/-/console-stream-0.1.1.tgz", + "integrity": "sha1-oJX+B7IEZZVfL6/Si11yvM2UnUQ=", "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/babel__traverse": "^7.0.4", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "dedent": "^0.7.0", - "expect": "^26.6.2", - "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runner": "^26.6.3", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "stack-utils": "^2.0.2", - "throat": "^5.0.0" - } - }, - "jest-config": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-26.6.3.tgz", - "integrity": "sha512-t5qdIj/bCj2j7NFVHb2nFB4aUdfucDn3JRKgrZnplb8nieAirAzRSHP8uDEd+qV6ygzg9Pz4YG7UTJf94LPSyg==", - "dev": true, - "requires": { - "@babel/core": "^7.1.0", - "@jest/test-sequencer": "^26.6.3", - "@jest/types": "^26.6.2", - "babel-jest": "^26.6.3", - "chalk": "^4.0.0", - "deepmerge": "^4.2.2", - "glob": "^7.1.1", - "graceful-fs": "^4.2.4", - "jest-environment-jsdom": "^26.6.2", - "jest-environment-node": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-jasmine2": "^26.6.3", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "deepmerge": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.2.2.tgz", - "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } - } + "optional": true }, - "jest-dev-server": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-4.4.0.tgz", - "integrity": "sha512-STEHJ3iPSC8HbrQ3TME0ozGX2KT28lbT4XopPxUm2WimsX3fcB3YOptRh12YphQisMhfqNSNTZUmWyT3HEXS2A==", + "node_modules/constant-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/constant-case/-/constant-case-3.0.4.tgz", + "integrity": "sha512-I2hSBi7Vvs7BEuJDr5dDHfzb/Ruj3FyvFyh7KLilAjNQw3Be+xgqUBA2W6scVEcL0hL1dwPRtIqEPVUCKkSsyQ==", "dev": true, - "requires": { - "chalk": "^3.0.0", - "cwd": "^0.10.0", - "find-process": "^1.4.3", - "prompts": "^2.3.0", - "spawnd": "^4.4.0", - "tree-kill": "^1.2.2", - "wait-on": "^3.3.0" - }, "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "chalk": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-3.0.0.tgz", - "integrity": "sha512-4D3B6Wf41KOYRFdszmDqMCGq5VV/uMAB273JILmO+3jAlh8X4qDtdtgCR3fxtbLEMzSx22QdhnDcJvu2u1fVwg==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "core-js": { - "version": "2.6.12", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.12.tgz", - "integrity": "sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "wait-on": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-3.3.0.tgz", - "integrity": "sha512-97dEuUapx4+Y12aknWZn7D25kkjMk16PbWoYzpSdA8bYpVfS6hpl2a2pOWZ3c+Tyt3/i4/pglyZctG3J4V1hWQ==", - "dev": true, - "requires": { - "@hapi/joi": "^15.0.3", - "core-js": "^2.6.5", - "minimist": "^1.2.0", - "request": "^2.88.0", - "rx": "^4.1.0" - } - } + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case": "^2.0.2" } }, - "jest-diff": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-26.6.2.tgz", - "integrity": "sha512-6m+9Z3Gv9wN0WFVasqjCL/06+EFCMTqDEUl/b87HYK2rAPTyfz4ZIuSlPhY51PIQRWx5TaxeF1qmXKe9gfN3sA==", + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", "dev": true, - "requires": { - "chalk": "^4.0.0", - "diff-sequences": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" } }, - "jest-docblock": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-26.0.0.tgz", - "integrity": "sha512-RDZ4Iz3QbtRWycd8bUEPxQsTlYazfYn/h5R65Fc6gOfwozFhoImx+affzky/FFBuqISPTqjXomoIGJVKBWoo0w==", + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", "dev": true, - "requires": { - "detect-newline": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "jest-each": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-26.6.2.tgz", - "integrity": "sha512-Mer/f0KaATbjl8MCJ+0GEpNdqmnVmDYqCTJYTvoo7rqmRiDllmp2AYN+06F93nXcY3ur9ShIjS+CO/uD+BbH4A==", - "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2" - } + "node_modules/continuable-cache": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/continuable-cache/-/continuable-cache-0.3.1.tgz", + "integrity": "sha1-vXJ6f67XfnH/OYWskzUakSczrQ8=", + "dev": true }, - "jest-environment-jsdom": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-26.6.2.tgz", - "integrity": "sha512-jgPqCruTlt3Kwqg5/WVFyHIOJHsiAvhcp2qiR2QQstuG9yWox5+iHpU3ZrcBxW14T4fe5Z68jAfLRh7joCSP2Q==", - "dev": true, - "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2", - "jsdom": "^16.4.0" - } + "node_modules/convert-source-map": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-1.9.0.tgz", + "integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==", + "dev": true }, - "jest-environment-node": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-26.6.2.tgz", - "integrity": "sha512-zhtMio3Exty18dy8ee8eJ9kjnRyZC1N4C1Nt/VShN1apyXc8rWGtJ9lI7vqiWcyyXS4BVSEn9lxAM2D+07/Tag==", + "node_modules/cookie": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", + "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", "dev": true, - "requires": { - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "jest-mock": "^26.6.2", - "jest-util": "^26.6.2" + "license": "MIT", + "engines": { + "node": ">= 0.6" } }, - "jest-get-type": { - "version": "26.3.0", - "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-26.3.0.tgz", - "integrity": "sha512-TpfaviN1R2pQWkIihlfEanwOXK0zcxrKEE4MlU6Tn7keoXdN6/3gK/xl0yEh8DOunn5pOVGKf8hB4R9gVh04ig==", + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", "dev": true }, - "jest-haste-map": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-26.6.2.tgz", - "integrity": "sha512-easWIJXIw71B2RdR8kgqpjQrbMRWQBgiBwXYEhtGUTaX+doCjBheluShdDMeR8IMfJiTqH4+zfhtg29apJf/8w==", + "node_modules/copy-descriptor": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/copy-descriptor/-/copy-descriptor-0.1.1.tgz", + "integrity": "sha1-Z29us8OZl8LuGsOpJP1hJHSPV40=", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/graceful-fs": "^4.1.2", - "@types/node": "*", - "anymatch": "^3.0.3", - "fb-watchman": "^2.0.0", - "fsevents": "^2.1.2", - "graceful-fs": "^4.2.4", - "jest-regex-util": "^26.0.0", - "jest-serializer": "^26.6.2", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "micromatch": "^4.0.2", - "sane": "^4.0.3", - "walker": "^1.0.7" - }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "engines": { + "node": ">=0.10.0" } }, - "jest-image-snapshot": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/jest-image-snapshot/-/jest-image-snapshot-3.0.1.tgz", - "integrity": "sha512-bW8eYxgAVyO8cNLlTt15wd5YiWvRfzQyNQ4K8FKHUEPasQADEZ5NzaWmnOpSdh3/NLYoH++TMp6o/rRVLpOIkQ==", - "dev": true, - "requires": { - "chalk": "^1.1.3", - "get-stdin": "^5.0.1", - "glur": "^1.1.2", - "lodash": "^4.17.4", - "mkdirp": "^0.5.1", - "pixelmatch": "^5.1.0", - "pngjs": "^3.4.0", - "rimraf": "^2.6.2" - }, + "node_modules/copy-webpack-plugin": { + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-10.2.4.tgz", + "integrity": "sha512-xFVltahqlsRcyyJqQbDY6EYTtyQZF9rf+JPjwHObLdPFMEISqkFkr7mFoVOC6BfYS/dNThyoQKvziugm+OnwBg==", + "dev": true, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "fast-glob": "^3.2.7", + "glob-parent": "^6.0.1", + "globby": "^12.0.2", + "normalize-path": "^3.0.0", + "schema-utils": "^4.0.0", + "serialize-javascript": "^6.0.0" + }, + "engines": { + "node": ">= 12.20.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" } }, - "jest-jasmine2": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-jasmine2/-/jest-jasmine2-26.6.3.tgz", - "integrity": "sha512-kPKUrQtc8aYwBV7CqBg5pu+tmYXlvFlSFYn18ev4gPFtrRzB15N2gW/Roew3187q2w2eHuu0MU9TJz6w0/nPEg==", + "node_modules/copy-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "@babel/traverse": "^7.1.0", - "@jest/environment": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "co": "^4.6.0", - "expect": "^26.6.2", - "is-generator-fn": "^2.0.0", - "jest-each": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "pretty-format": "^26.6.2", - "throat": "^5.0.0" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "jest-leak-detector": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-26.6.2.tgz", - "integrity": "sha512-i4xlXpsVSMeKvg2cEKdfhh0H39qlJlP5Ex1yQxwF9ubahboQYMgTtz5oML35AVA3B4Eu+YsmwaiKVev9KCvLxg==", + "node_modules/copy-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "requires": { - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "jest-matcher-utils": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-26.6.2.tgz", - "integrity": "sha512-llnc8vQgYcNqDrqRDXWwMr9i7rS5XFiCwvh6DTP7Jqa2mqpcCBBlpCbn+trkG0KNhPu/h8rzyBkriOtBstvWhw==", + "node_modules/copy-webpack-plugin/node_modules/array-union": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-3.0.1.tgz", + "integrity": "sha512-1OvF9IbWwaeiM9VhzYXVQacMibxpXOMYVNIvMtKRyX9SImBXpKcFr8XvFDeEslCyuH/t6KRt7HEO94AlP8Iatw==", "dev": true, - "requires": { - "chalk": "^4.0.0", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "pretty-format": "^26.6.2" + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "jest-message-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-26.6.2.tgz", - "integrity": "sha512-rGiLePzQ3AzwUshu2+Rn+UMFk0pHN58sOG+IaJbk5Jxuqo3NYO1U2/MIR4S1sKgsoYSXSzdtSa0TgrmtUwEbmA==", + "node_modules/copy-webpack-plugin/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", "dev": true, - "requires": { - "@babel/code-frame": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/stack-utils": "^2.0.0", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "micromatch": "^4.0.2", - "pretty-format": "^26.6.2", - "slash": "^3.0.0", - "stack-utils": "^2.0.2" - }, + "license": "ISC", "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" } }, - "jest-mock": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-26.6.2.tgz", - "integrity": "sha512-YyFjePHHp1LzpzYcmgqkJ0nm0gg/lJx2aZFzFy1S6eUqNjXsOqTK10zNRff2dNfssgokjkG65OlWNcIlgd3zew==", + "node_modules/copy-webpack-plugin/node_modules/globby": { + "version": "12.2.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-12.2.0.tgz", + "integrity": "sha512-wiSuFQLZ+urS9x2gGPl1H5drc5twabmm4m2gTR27XDFyjUHJUNsS8o/2aKyIF6IoBaR630atdher0XJ5g6OMmA==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*" + "license": "MIT", + "dependencies": { + "array-union": "^3.0.1", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.7", + "ignore": "^5.1.9", + "merge2": "^1.4.1", + "slash": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "jest-pnp-resolver": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.2.tgz", - "integrity": "sha512-olV41bKSMm8BdnuMsewT4jqlZ8+3TCARAXjZGT9jcoSnrfUnRCqnMoF9XEeoWjbzObpqF9dRhHQj0Xb9QdF6/w==", - "dev": true + "node_modules/copy-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" }, - "jest-regex-util": { - "version": "26.0.0", - "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-26.0.0.tgz", - "integrity": "sha512-Gv3ZIs/nA48/Zvjrl34bf+oD76JHiGDUxNOVgUjh3j890sblXryjY4rss71fPtD/njchl6PSE2hIhvyWa1eT0A==", - "dev": true + "node_modules/copy-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, - "jest-resolve": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-26.6.2.tgz", - "integrity": "sha512-sOxsZOq25mT1wRsfHcbtkInS+Ek7Q8jCHUB0ZUTP0tc/c41QHriU/NunqMfCUWsL4H3MHpvQD4QR9kSYhS7UvQ==", + "node_modules/copy-webpack-plugin/node_modules/slash": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", + "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "jest-pnp-resolver": "^1.2.2", - "jest-util": "^26.6.2", - "read-pkg-up": "^7.0.1", - "resolve": "^1.18.1", - "slash": "^3.0.0" + "license": "MIT", + "engines": { + "node": ">=12" }, - "dependencies": { - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - } - } + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "jest-resolve-dependencies": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-26.6.3.tgz", - "integrity": "sha512-pVwUjJkxbhe4RY8QEWzN3vns2kqyuldKpxlxJlzEYfKSvY6/bMvxoFrYYzUO1Gx28yKWN37qyV7rIoIp2h8fTg==", + "node_modules/core-js": { + "version": "3.49.0", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.49.0.tgz", + "integrity": "sha512-es1U2+YTtzpwkxVLwAFdSpaIMyQaq0PBgm3YD1W3Qpsn1NAmO3KSgZfu+oGSWVu6NvLHoHCV/aYcsE5wiB7ALg==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-snapshot": "^26.6.2" + "hasInstallScript": true, + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "jest-runner": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-26.6.3.tgz", - "integrity": "sha512-atgKpRHnaA2OvByG/HpGA4g6CSPS/1LK0jK3gATJAoptC1ojltpmVlYC3TYgdmGp+GLuhzpH30Gvs36szSL2JQ==", + "node_modules/core-js-compat": { + "version": "3.40.0", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.40.0.tgz", + "integrity": "sha512-0XEDpr5y5mijvw8Lbc6E5AkjrHfp7eEoPlu36SWeAbcL8fn1G1ANe8DBlo2XoNN89oVpxWwOjYIPVzR4ZvsKCQ==", "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "chalk": "^4.0.0", - "emittery": "^0.7.1", - "exit": "^0.1.2", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-docblock": "^26.0.0", - "jest-haste-map": "^26.6.2", - "jest-leak-detector": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", - "jest-runtime": "^26.6.3", - "jest-util": "^26.6.2", - "jest-worker": "^26.6.2", - "source-map-support": "^0.5.6", - "throat": "^5.0.0" - } - }, - "jest-runtime": { - "version": "26.6.3", - "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-26.6.3.tgz", - "integrity": "sha512-lrzyR3N8sacTAMeonbqpnSka1dHNux2uk0qqDXVkMv2c/A3wYnvQ4EXuI013Y6+gSKSCxdaczvf4HF0mVXHRdw==", - "dev": true, - "requires": { - "@jest/console": "^26.6.2", - "@jest/environment": "^26.6.2", - "@jest/fake-timers": "^26.6.2", - "@jest/globals": "^26.6.2", - "@jest/source-map": "^26.6.2", - "@jest/test-result": "^26.6.2", - "@jest/transform": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/yargs": "^15.0.0", - "chalk": "^4.0.0", - "cjs-module-lexer": "^0.6.0", - "collect-v8-coverage": "^1.0.0", - "exit": "^0.1.2", - "glob": "^7.1.3", - "graceful-fs": "^4.2.4", - "jest-config": "^26.6.3", - "jest-haste-map": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-mock": "^26.6.2", - "jest-regex-util": "^26.0.0", - "jest-resolve": "^26.6.2", - "jest-snapshot": "^26.6.2", - "jest-util": "^26.6.2", - "jest-validate": "^26.6.2", - "slash": "^3.0.0", - "strip-bom": "^4.0.0", - "yargs": "^15.4.1" - }, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "cliui": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", - "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", - "dev": true, - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^6.2.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-bom": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", - "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", - "dev": true - }, - "wrap-ansi": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", - "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "yargs": { - "version": "15.4.1", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", - "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", - "dev": true, - "requires": { - "cliui": "^6.0.0", - "decamelize": "^1.2.0", - "find-up": "^4.1.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^4.2.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^18.1.2" - } - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "browserslist": "^4.24.3" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "jest-serializer": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-serializer/-/jest-serializer-26.6.2.tgz", - "integrity": "sha512-S5wqyz0DXnNJPd/xfIzZ5Xnp1HrJWBczg8mMfMpN78OJ5eDxXyf+Ygld9wX1DnUWbIbhM1YDY95NjR4CBXkb2g==", + "node_modules/core-js-pure": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.35.1.tgz", + "integrity": "sha512-zcIdi/CL3MWbBJYo5YCeVAAx+Sy9yJE9I3/u9LkFABwbeaPhTMRWraM8mYFp9jW5Z50hOy7FVzCc8dCrpZqtIQ==", "dev": true, - "requires": { - "@types/node": "*", - "graceful-fs": "^4.2.4" + "hasInstallScript": true, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" } }, - "jest-snapshot": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-26.6.2.tgz", - "integrity": "sha512-OLhxz05EzUtsAmOMzuupt1lHYXCNib0ECyuZ/PZOx9TrZcC8vL0x+DUG3TL+GLX3yHG45e6YGjIm0XwDc3q3og==", + "node_modules/core-js-url-browser": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/core-js-url-browser/-/core-js-url-browser-3.6.4.tgz", + "integrity": "sha512-VCMkPikOVp5JXftTj0E3gPZNKa0exQX837KxyPcnMAKvImWG8+RbXwEHEGMjiNz+9Vl2YgutkVYOpq7iaSOt/Q==" + }, + "node_modules/core-util-is": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", + "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=" + }, + "node_modules/cosmiconfig": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.1.0.tgz", + "integrity": "sha512-AdmX6xUzdNASswsFtmwSt7Vj8po9IuqXm0UXz7QKPuEUmPB4XyjGfaAr2PSuELMwkRMVH1EpIkX5bTZGRB3eCA==", "dev": true, - "requires": { - "@babel/types": "^7.0.0", - "@jest/types": "^26.6.2", - "@types/babel__traverse": "^7.0.4", - "@types/prettier": "^2.0.0", - "chalk": "^4.0.0", - "expect": "^26.6.2", - "graceful-fs": "^4.2.4", - "jest-diff": "^26.6.2", - "jest-get-type": "^26.3.0", - "jest-haste-map": "^26.6.2", - "jest-matcher-utils": "^26.6.2", - "jest-message-util": "^26.6.2", - "jest-resolve": "^26.6.2", - "natural-compare": "^1.4.0", - "pretty-format": "^26.6.2", - "semver": "^7.3.2" + "dependencies": { + "@types/parse-json": "^4.0.0", + "import-fresh": "^3.2.1", + "parse-json": "^5.0.0", + "path-type": "^4.0.0", + "yaml": "^1.10.0" + }, + "engines": { + "node": ">=10" } }, - "jest-util": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-26.6.2.tgz", - "integrity": "sha512-MDW0fKfsn0OI7MS7Euz6h8HNDXVQ0gaM9uW6RjfDmd1DAFcaxX9OqIakHIqhbnmF08Cf2DLDG+ulq8YQQ0Lp0Q==", + "node_modules/create-jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/create-jest/-/create-jest-29.7.0.tgz", + "integrity": "sha512-Adz2bdH0Vq3F53KEMJOoftQFutWCukm6J24wbPWRO4k1kMY7gS7ds/uoJkNuV8wDCtWWnuwGcJwpWcih+zEW1Q==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "@types/node": "*", + "dependencies": { + "@jest/types": "^29.6.3", "chalk": "^4.0.0", - "graceful-fs": "^4.2.4", - "is-ci": "^2.0.0", - "micromatch": "^4.0.2" + "exit": "^0.1.2", + "graceful-fs": "^4.2.9", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "prompts": "^2.0.1" }, - "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - } + "bin": { + "create-jest": "bin/create-jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "jest-validate": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-26.6.2.tgz", - "integrity": "sha512-NEYZ9Aeyj0i5rQqbq+tpIOom0YS1u2MVu6+euBsvpgIme+FOfRmoC4R5p0JiAUpaFvFy24xgrpMknarR/93XjQ==", + "node_modules/create-jest/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "camelcase": "^6.0.0", - "chalk": "^4.0.0", - "jest-get-type": "^26.3.0", - "leven": "^3.1.0", - "pretty-format": "^26.6.2" - }, "dependencies": { - "camelcase": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.2.0.tgz", - "integrity": "sha512-c7wVvbw3f37nuobQNtgsgG9POC9qMbNuMQmTCqZv23b6MIz0fcYpBiOlv9gEN/hdLdnZTDQhg6e9Dq5M1vKvfg==", - "dev": true - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "jest-watcher": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-26.6.2.tgz", - "integrity": "sha512-WKJob0P/Em2csiVthsI68p6aGKTIcsfjH9Gsx1f0A3Italz43e3ho0geSAVsmj09RWOELP1AZ/DXyJgOgDKxXQ==", + "node_modules/create-jest/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", "dev": true, - "requires": { - "@jest/test-result": "^26.6.2", - "@jest/types": "^26.6.2", - "@types/node": "*", - "ansi-escapes": "^4.2.1", - "chalk": "^4.0.0", - "jest-util": "^26.6.2", - "string-length": "^4.0.1" + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" } }, - "jest-worker": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-26.6.2.tgz", - "integrity": "sha512-KWYVV1c4i+jbMpaBC+U++4Va0cp8OisU185o73T1vo99hqi7w8tSJfUXYswwqqrjzwxa6KpRK54WhPvwf5w6PQ==", + "node_modules/create-jest/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^7.0.0" - }, "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/create-jest/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/create-jest/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" } }, - "joi": { - "version": "17.4.0", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.4.0.tgz", - "integrity": "sha512-F4WiW2xaV6wc1jxete70Rw4V/VuMd6IN+a5ilZsxG4uYtUXWu2kq9W5P2dz30e7Gmw8RCbY/u/uk+dMPma9tAg==", + "node_modules/create-jest/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0", - "@hapi/topo": "^5.0.0", - "@sideway/address": "^4.1.0", - "@sideway/formula": "^3.0.0", - "@sideway/pinpoint": "^2.0.0" - }, "dependencies": { - "@hapi/hoek": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.1.1.tgz", - "integrity": "sha512-CAEbWH7OIur6jEOzaai83jq3FmKmv4PmX1JYfs9IrYcGEVI/lyL1EXJGCj7eFVJ0bg5QR8LMxBlEtA+xKiLpFw==", - "dev": true - }, - "@hapi/topo": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.0.0.tgz", - "integrity": "sha512-tFJlT47db0kMqVm3H4nQYgn6Pwg10GTZHb1pwmSiv1K4ks6drQOtfEF5ZnPjkvC+y4/bUPHK+bc87QvLcL+WMw==", - "dev": true, - "requires": { - "@hapi/hoek": "^9.0.0" - } - } + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "jpegtran-bin": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", - "integrity": "sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ==", + "node_modules/cross-spawn": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-5.1.0.tgz", + "integrity": "sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "logalot": "^2.0.0" + "dependencies": { + "lru-cache": "^4.0.1", + "shebang-command": "^1.2.0", + "which": "^1.2.9" } }, - "jquery": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.6.0.tgz", - "integrity": "sha512-JVzAR/AjBvVt2BmYhxRCSYysDsPcssdmTFnzyLEts9qNwmjmu4JTAMYubEfwVOSwpQ1I1sKKFcxhZCI2buerfw==" - }, - "jquery-color": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/jquery-color/-/jquery-color-2.2.0.tgz", - "integrity": "sha512-4VoxsLMw860EQGNT/TmP3Lbr7/1OCQlBPS4ILj7bxRApJrPQfpqzdIOTY8Ll9nGY7UHtWqDuzR7cUcS1lcWjVw==" - }, - "jquery-form": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/jquery-form/-/jquery-form-4.3.0.tgz", - "integrity": "sha512-q3uaVCEWdLOYUCI6dpNdwf/7cJFOsUgdpq6r0taxtGQ5NJSkOzofyWm4jpOuJ5YxdmL1FI5QR+q+HB63HHLGnQ==", - "requires": { - "jquery": ">=1.7.2" + "node_modules/cross-spawn/node_modules/lru-cache": { + "version": "4.1.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-4.1.5.tgz", + "integrity": "sha512-sWZlbEP2OsHNkXrMl5GYk/jKk70MBng6UU4YI/qGDYbgf6YbP4EvmqISbXCoJiRKs+1bSpFHVgQxvJ17F2li5g==", + "dev": true, + "optional": true, + "dependencies": { + "pseudomap": "^1.0.2", + "yallist": "^2.1.2" } }, - "jquery-hoverintent": { - "version": "1.10.1", - "resolved": "https://registry.npmjs.org/jquery-hoverintent/-/jquery-hoverintent-1.10.1.tgz", - "integrity": "sha512-PNZAVnNcuIB5MDmZPWK7H2lQINRJ4Z8+EGLseIZd/gnd5Q9W3dBOKv0vKG7WPFxG2/Na1YX0/soeufucO6bCJQ==", - "requires": { - "jquery": ">=1.7.0" + "node_modules/cross-spawn/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "js-polyfills": { - "version": "0.1.43", - "resolved": "https://registry.npmjs.org/js-polyfills/-/js-polyfills-0.1.43.tgz", - "integrity": "sha512-wWCJcw7uMA12uk7qcqZlIQy9nj+Evh1wVUmn5MOlJ7GPC8HT5PLjB9Uiqjw9ldAbbOuNOWJ6ENb7NwU6qqf48g==" - }, - "js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + "node_modules/cross-spawn/node_modules/yallist": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-2.1.2.tgz", + "integrity": "sha512-ncTzHV7NvsQZkYe1DW7cbDLm0YpzHmZF5r/iyP3ZnQtMiJ+pjzisCiMNI+Sj+xQF5pXhSHxSB3uDbsBTzY/c2A==", + "dev": true, + "license": "ISC", + "optional": true }, - "js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "node_modules/crypto-random-string": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", + "integrity": "sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==", "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" + "engines": { + "node": ">=8" } }, - "js2xmlparser": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.1.tgz", - "integrity": "sha512-KrPTolcw6RocpYjdC7pL7v62e55q7qOMHvLX1UCLc5AAS8qeJ6nukarEJAF2KL2PZxlbGueEbINqZR2bDe/gUw==", + "node_modules/cson-parser": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/cson-parser/-/cson-parser-1.3.5.tgz", + "integrity": "sha1-fsZ14DkUVTO/KmqFYHPxWZ2cLSQ=", "dev": true, - "requires": { - "xmlcreate": "^2.0.3" + "dependencies": { + "coffee-script": "^1.10.0" } }, - "jsbn": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", - "integrity": "sha1-peZUwuWi3rXyAdls77yoDA7y9RM=", + "node_modules/csp_evaluator": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/csp_evaluator/-/csp_evaluator-1.1.1.tgz", + "integrity": "sha512-N3ASg0C4kNPUaNxt1XAvzHIVuzdtr8KLgfk1O8WDyimp1GisPAHESupArO2ieHk9QWbrJ/WkQODyh21Ps/xhxw==", "dev": true }, - "jsdoc": { - "version": "3.6.7", - "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.7.tgz", - "integrity": "sha512-sxKt7h0vzCd+3Y81Ey2qinupL6DpRSZJclS04ugHDNmRUXGzqicMJ6iwayhSA0S0DwwX30c5ozyUthr1QKF6uw==", + "node_modules/css-declaration-sorter": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.2.0.tgz", + "integrity": "sha512-h70rUM+3PNFuaBDTLe8wF/cdWu+dOZmb7pJt8Z2sedYbAcQVQV/tEchueg3GWxwqS0cxtbxmaHEdkNACqcvsow==", "dev": true, - "requires": { - "@babel/parser": "^7.9.4", - "bluebird": "^3.7.2", - "catharsis": "^0.9.0", - "escape-string-regexp": "^2.0.0", - "js2xmlparser": "^4.0.1", - "klaw": "^3.0.0", - "markdown-it": "^10.0.0", - "markdown-it-anchor": "^5.2.7", - "marked": "^2.0.3", - "mkdirp": "^1.0.4", - "requizzle": "^0.2.3", - "strip-json-comments": "^3.1.0", - "taffydb": "2.6.2", - "underscore": "~1.13.1" + "engines": { + "node": "^14 || ^16 || >=18" }, - "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } + "peerDependencies": { + "postcss": "^8.0.9" } }, - "jsdoctypeparser": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/jsdoctypeparser/-/jsdoctypeparser-9.0.0.tgz", - "integrity": "sha512-jrTA2jJIL6/DAEILBEh2/w9QxCuwmvNXIry39Ay/HVfhE3o2yVV0U44blYkqdHA/OKloJEqvJy0xU+GSdE2SIw==", - "dev": true - }, - "jsdom": { - "version": "16.7.0", - "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-16.7.0.tgz", - "integrity": "sha512-u9Smc2G1USStM+s/x1ru5Sxrl6mPYCbByG1U/hUmqaVsm4tbNyS7CicOSRyuGQYZhTu0h84qkZZQ/I+dzizSVw==", + "node_modules/css-functions-list": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/css-functions-list/-/css-functions-list-3.2.3.tgz", + "integrity": "sha512-IQOkD3hbR5KrN93MtcYuad6YPuTSUhntLHDuLEbFWE+ff2/XSZNdZG+LcbbIW5AXKg/WFIfYItIzVoHngHXZzA==", "dev": true, - "requires": { - "abab": "^2.0.5", - "acorn": "^8.2.4", - "acorn-globals": "^6.0.0", - "cssom": "^0.4.4", - "cssstyle": "^2.3.0", - "data-urls": "^2.0.0", - "decimal.js": "^10.2.1", - "domexception": "^2.0.1", - "escodegen": "^2.0.0", - "form-data": "^3.0.0", - "html-encoding-sniffer": "^2.0.1", - "http-proxy-agent": "^4.0.1", - "https-proxy-agent": "^5.0.0", - "is-potential-custom-element-name": "^1.0.1", - "nwsapi": "^2.2.0", - "parse5": "6.0.1", - "saxes": "^5.0.1", - "symbol-tree": "^3.2.4", - "tough-cookie": "^4.0.0", - "w3c-hr-time": "^1.0.2", - "w3c-xmlserializer": "^2.0.0", - "webidl-conversions": "^6.1.0", - "whatwg-encoding": "^1.0.5", - "whatwg-mimetype": "^2.3.0", - "whatwg-url": "^8.5.0", - "ws": "^7.4.6", - "xml-name-validator": "^3.0.0" - }, - "dependencies": { - "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", - "dev": true - }, - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "form-data": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", - "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", - "dev": true, - "requires": { - "asynckit": "^0.4.0", - "combined-stream": "^1.0.8", - "mime-types": "^2.1.12" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=12 || >=16" } }, - "jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", - "dev": true - }, - "jshint": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.12.0.tgz", - "integrity": "sha512-TwuuaUDmra0JMkuqvqy+WGo2xGHSNjv1BA1nTIgtH2K5z1jHuAEeAgp7laaR+hLRmajRjcrM71+vByBDanCyYA==", + "node_modules/css-loader": { + "version": "6.7.3", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.3.tgz", + "integrity": "sha512-qhOH1KlBMnZP8FzRO6YCH9UHXQhVMcEGLyNdb7Hv2cpcmJbW0YrddO+tG1ab5nT41KpHIYGsbeHqxB9xPu1pKQ==", "dev": true, - "requires": { - "cli": "~1.0.0", - "console-browserify": "1.1.x", - "exit": "0.1.x", - "htmlparser2": "3.8.x", - "lodash": "~4.17.19", - "minimatch": "~3.0.2", - "shelljs": "0.3.x", - "strip-json-comments": "1.0.x" + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.19", + "postcss-modules-extract-imports": "^3.0.0", + "postcss-modules-local-by-default": "^4.0.0", + "postcss-modules-scope": "^3.0.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.3.8" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/css-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-2.1.0.tgz", + "integrity": "sha512-Dqk7LQKpwLoH3VovzZnkzegqNSuAziQyNZUcrdDM401iY+R5NkGBXGmtO05/yaXQziALuPogeG0b7UAgjnTJTQ==", + "dev": true, + "optional": true, "dependencies": { - "console-browserify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", - "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", - "dev": true, - "requires": { - "date-now": "^0.1.4" - } - }, - "domhandler": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", - "integrity": "sha1-LeWaCCLVAn+r/28DLCsloqir5zg=", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "domutils": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", - "integrity": "sha1-3NhIiib1Y9YQeeSMn3t+Mjc2gs8=", - "dev": true, - "requires": { - "dom-serializer": "0", - "domelementtype": "1" - } - }, - "entities": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", - "integrity": "sha1-sph6o4ITR/zeZCsk/fyeT7cSvyY=", - "dev": true - }, - "htmlparser2": { - "version": "3.8.3", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", - "integrity": "sha1-mWwosZFRaovoZQGn15dX5ccMEGg=", - "dev": true, - "requires": { - "domelementtype": "1", - "domhandler": "2.3", - "domutils": "1.5", - "entities": "1.0", - "readable-stream": "1.1" - } - }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true - }, - "readable-stream": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", - "integrity": "sha1-fPTFTvZI44EwhMY23SB54WbAgdk=", - "dev": true, - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.1", - "isarray": "0.0.1", - "string_decoder": "~0.10.x" - } - }, - "string_decoder": { - "version": "0.10.31", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", - "integrity": "sha1-YuIDvEF2bGwoyfyEMB2rHFMQ+pQ=", - "dev": true - }, - "strip-json-comments": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", - "integrity": "sha1-HhX7ysl9Pumb8tc7TGVrCCu6+5E=", - "dev": true - } + "boolbase": "^1.0.0", + "css-what": "^3.2.1", + "domutils": "^1.7.0", + "nth-check": "^1.0.2" } }, - "json-buffer": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", - "integrity": "sha1-Wx85evx11ne96Lz8Dkfh+aPZqJg=", + "node_modules/css-select-base-adapter": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/css-select-base-adapter/-/css-select-base-adapter-0.1.1.tgz", + "integrity": "sha512-jQVeeRG70QI08vSTwf1jHxp74JoZsr2XSgETae8/xC8ovSnL2WF87GTLO86Sbwdt2lK4Umg4HnnwMO4YF3Ce7w==", "dev": true, "optional": true }, - "json-parse-better-errors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", - "integrity": "sha512-mrqyZKfX5EhL7hvqcV6WG1yYjnjeuYDzDhhcAAUrq8Po85NBQBJP+ZDUT75qZQ98IkUoBqdkExkukOU7Ts2wrw==", - "dev": true - }, - "json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==" - }, - "json-schema": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz", - "integrity": "sha1-tIDIkuWaLwWVTOcnvT8qTogvnhM=", - "dev": true - }, - "json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "dev": true - }, - "json-stable-stringify-without-jsonify": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", - "integrity": "sha1-nbe1lJatPzz+8wp1FC0tkwrXJlE=", - "dev": true - }, - "json-stringify-safe": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", - "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", - "dev": true - }, - "json2php": { - "version": "0.0.4", - "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.4.tgz", - "integrity": "sha1-a9haHdpqXdfpECK7JEA8wbfC7jQ=", - "dev": true - }, - "json3": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/json3/-/json3-3.3.3.tgz", - "integrity": "sha512-c7/8mbUsKigAbLkD5B010BK4D9LZm7A1pNItkEwiUZRpIN66exu/e7YQWysGun+TRKaJp8MhemM+VkfWv42aCA==" - }, - "json5": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.1.tgz", - "integrity": "sha512-aKS4WQjPenRxiQsC93MNfjx+nbF4PAdYzmd/1JIj8HYzqfbu86beTuNgXDzPknWk0n0uARlyewZo4s++ES36Ow==", + "node_modules/css-tree": { + "version": "1.0.0-alpha.37", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.0.0-alpha.37.tgz", + "integrity": "sha512-DMxWJg0rnz7UgxKT0Q1HU/L9BeJI0M6ksor0OgqOnF+aRCDWg/N2641HmVyU9KVIu0OVVWOb2IpC9A+BJRnejg==", "dev": true, - "requires": { - "minimist": "^1.2.0" + "optional": true, + "dependencies": { + "mdn-data": "2.0.4", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "jsonc-parser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.2.1.tgz", - "integrity": "sha512-o6/yDBYccGvTz1+QFevz6l6OBZ2+fMVu2JZ9CIhzsYRX4mjaK5IyX9eldUdCmga16zlgQxyrj5pt9kzuj2C02w==", - "dev": true - }, - "jsonfile": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-5.0.0.tgz", - "integrity": "sha512-NQRZ5CRo74MhMMC3/3r5g2k4fjodJ/wh8MxjFbCViWKFjxrnudWSY5vomh+23ZaXzAS7J3fBZIR2dV6WbmfM0w==", - "requires": { - "graceful-fs": "^4.1.6", - "universalify": "^0.1.2" + "node_modules/css-tree/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "jsprim": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.1.tgz", - "integrity": "sha1-MT5mvB5cwG5Di8G3SZwuXFastqI=", + "node_modules/css-what": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-3.4.2.tgz", + "integrity": "sha512-ACUm3L0/jiZTqfzRM3Hi9Q8eZqd6IK37mMWPLz9PJxkLWllYeRf+EHUSHYEtFop2Eqytaq1FizFVh7XfBnXCDQ==", "dev": true, - "requires": { - "assert-plus": "1.0.0", - "extsprintf": "1.3.0", - "json-schema": "0.2.3", - "verror": "1.10.0" + "optional": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "jsx-ast-utils": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.2.0.tgz", - "integrity": "sha512-EIsmt3O3ljsU6sot/J4E1zDRxfBNrhjyf/OKjlydwgEimQuznlM4Wv7U+ueONJMyEn1WRE0K8dhi3dVAXYT24Q==", + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", "dev": true, - "requires": { - "array-includes": "^3.1.2", - "object.assign": "^4.1.2" + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" } }, - "just-extend": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-4.2.1.tgz", - "integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==", - "dev": true + "node_modules/csslint": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/csslint/-/csslint-1.0.5.tgz", + "integrity": "sha512-GXGpPqGIuEBKesM4bt2IKFrzDKpemh9wVZRHVuculUErar554QrXHOonhgkBOP3uiZzbAETz0N2A4oWlIoxPuw==", + "license": "MIT", + "dependencies": { + "clone": "~2.1.0", + "parserlib": "~1.1.1" + }, + "bin": { + "csslint": "dist/cli.js" + }, + "engines": { + "node": ">=0.10.0" + } }, - "karma": { - "version": "6.3.2", - "resolved": "https://registry.npmjs.org/karma/-/karma-6.3.2.tgz", - "integrity": "sha512-fo4Wt0S99/8vylZMxNj4cBFyOBBnC1bewZ0QOlePij/2SZVWxqbyLeIddY13q6URa2EpLRW8ixvFRUMjkmo1bw==", - "requires": { - "body-parser": "^1.19.0", - "braces": "^3.0.2", - "chokidar": "^3.4.2", - "colors": "^1.4.0", - "connect": "^3.7.0", - "di": "^0.0.1", - "dom-serialize": "^2.2.1", - "glob": "^7.1.6", - "graceful-fs": "^4.2.4", - "http-proxy": "^1.18.1", - "isbinaryfile": "^4.0.6", - "lodash": "^4.17.19", - "log4js": "^6.2.1", - "mime": "^2.4.5", - "minimatch": "^3.0.4", - "qjobs": "^1.2.0", - "range-parser": "^1.2.1", - "rimraf": "^3.0.2", - "socket.io": "^3.1.0", - "source-map": "^0.6.1", - "tmp": "0.2.1", - "ua-parser-js": "^0.7.23", - "yargs": "^16.1.1" + "node_modules/cssnano": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-7.1.3.tgz", + "integrity": "sha512-mLFHQAzyapMVFLiJIn7Ef4C2UCEvtlTlbyILR6B5ZsUAV3D/Pa761R5uC1YPhyBkRd3eqaDm2ncaNrD7R4mTRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssnano-preset-default": "^7.0.11", + "lilconfig": "^3.1.3" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/cssnano" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-preset-default": { + "version": "7.0.11", + "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-7.0.11.tgz", + "integrity": "sha512-waWlAMuCakP7//UCY+JPrQS1z0OSLeOXk2sKWJximKWGupVxre50bzPlvpbUwZIDylhf/ptf0Pk+Yf7C+hoa3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "css-declaration-sorter": "^7.2.0", + "cssnano-utils": "^5.0.1", + "postcss-calc": "^10.1.1", + "postcss-colormin": "^7.0.6", + "postcss-convert-values": "^7.0.9", + "postcss-discard-comments": "^7.0.6", + "postcss-discard-duplicates": "^7.0.2", + "postcss-discard-empty": "^7.0.1", + "postcss-discard-overridden": "^7.0.1", + "postcss-merge-longhand": "^7.0.5", + "postcss-merge-rules": "^7.0.8", + "postcss-minify-font-values": "^7.0.1", + "postcss-minify-gradients": "^7.0.1", + "postcss-minify-params": "^7.0.6", + "postcss-minify-selectors": "^7.0.6", + "postcss-normalize-charset": "^7.0.1", + "postcss-normalize-display-values": "^7.0.1", + "postcss-normalize-positions": "^7.0.1", + "postcss-normalize-repeat-style": "^7.0.1", + "postcss-normalize-string": "^7.0.1", + "postcss-normalize-timing-functions": "^7.0.1", + "postcss-normalize-unicode": "^7.0.6", + "postcss-normalize-url": "^7.0.1", + "postcss-normalize-whitespace": "^7.0.1", + "postcss-ordered-values": "^7.0.2", + "postcss-reduce-initial": "^7.0.6", + "postcss-reduce-transforms": "^7.0.1", + "postcss-svgo": "^7.1.1", + "postcss-unique-selectors": "^7.0.5" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/cssnano-utils": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-5.0.1.tgz", + "integrity": "sha512-ZIP71eQgG9JwjVZsTPSqhc6GHgEr53uJ7tK5///VfyWj6Xp2DBmixWHqJgPno+PqATzn48pL42ww9x5SSGmhZg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/csso": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/csso/-/csso-4.2.0.tgz", + "integrity": "sha512-wvlcdIbf6pwKEk7vHj8/Bkc0B4ylXZruLvOgs9doS5eOsOpuodOV2zJChSpkp+pRpYQLQMeF04nr3Z68Sta9jA==", + "dev": true, + "optional": true, "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==" - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "requires": { - "color-convert": "^2.0.1" - } - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "requires": { - "fill-range": "^7.0.1" - } - }, - "cliui": { - "version": "7.0.4", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", - "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", - "requires": { - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0", - "wrap-ansi": "^7.0.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" - }, - "colors": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/colors/-/colors-1.4.0.tgz", - "integrity": "sha512-a+UqTh4kgZg/SlGvfbzDHpgRu7AAQOmmqRHJnxhRZICKFUT91brVhNNt58CMWU9PsBbv3PDCZUHbVxuDiH2mtA==" - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==" - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==" - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "tmp": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.1.tgz", - "integrity": "sha512-76SUhtfqR2Ijn+xllcI5P1oyannHNHByD80W1q447gU3mp9G9PSpGdWmjUOHRDPiHYacIk66W7ubDTuPF3BEtQ==", - "requires": { - "rimraf": "^3.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "requires": { - "is-number": "^7.0.0" - } - }, - "wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "requires": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - } - }, - "y18n": { - "version": "5.0.8", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", - "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==" - }, - "yargs": { - "version": "16.2.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", - "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", - "requires": { - "cliui": "^7.0.2", - "escalade": "^3.1.1", - "get-caller-file": "^2.0.5", - "require-directory": "^2.1.1", - "string-width": "^4.2.0", - "y18n": "^5.0.5", - "yargs-parser": "^20.2.2" - } - }, - "yargs-parser": { - "version": "20.2.7", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.7.tgz", - "integrity": "sha512-FiNkvbeHzB/syOjIUxFDCnhSfzAL8R5vs40MgLFBorXACCOAEaWu0gRZl14vG8MR9AOJIZbmkjhusqBYZ3HTHw==" - } + "css-tree": "^1.1.2" + }, + "engines": { + "node": ">=8.0.0" } }, - "keyv": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", - "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "node_modules/csso/node_modules/css-tree": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-1.1.3.tgz", + "integrity": "sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "json-buffer": "3.0.0" + "dependencies": { + "mdn-data": "2.0.14", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=8.0.0" } }, - "killable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/killable/-/killable-1.0.1.tgz", - "integrity": "sha512-LzqtLKlUwirEUyl/nicirVmNiPvYs7l5n8wOPP7fyJVpUPkvCnW/vuiXGpylGUlnPDnB7311rARzAt3Mhswpjg==", - "dev": true + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.14.tgz", + "integrity": "sha512-dn6wd0uw5GsdswPFfsgMp5NSB0/aDe6fK94YJV/AJDYXL6HVLWBsxeq7js7Ad+mU2K9LAlwpk6kN2D5mwCPVow==", + "dev": true, + "optional": true }, - "kind-of": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", - "integrity": "sha1-MeohpzS6ubuw8yRm2JOupR5KPGQ=", - "requires": { - "is-buffer": "^1.1.5" + "node_modules/csso/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "klaw": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", - "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "node_modules/cssstyle": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/cssstyle/-/cssstyle-4.6.0.tgz", + "integrity": "sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==", "dev": true, - "requires": { - "graceful-fs": "^4.1.9" + "license": "MIT", + "dependencies": { + "@asamuzakjp/css-color": "^3.2.0", + "rrweb-cssom": "^0.8.0" + }, + "engines": { + "node": ">=18" } }, - "kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "dev": true + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" }, - "klona": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.4.tgz", - "integrity": "sha512-ZRbnvdg/NxqzC7L9Uyqzf4psi1OM4Cuc+sJAkQPjO6XkQIJTNbfK2Rsmbw8fx1p2mkZdp2FZYo2+LwXYY/uwIA==", - "dev": true + "node_modules/currently-unhandled": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/currently-unhandled/-/currently-unhandled-0.4.1.tgz", + "integrity": "sha1-mI3zP+qxke95mmE2nddsF635V+o=", + "dev": true, + "optional": true, + "dependencies": { + "array-find-index": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "known-css-properties": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.21.0.tgz", - "integrity": "sha512-sZLUnTqimCkvkgRS+kbPlYW5o8q5w1cu+uIisKpEWkj31I8mx8kNG162DwRav8Zirkva6N5uoFsm9kzK4mUXjw==", - "dev": true + "node_modules/cwd": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/cwd/-/cwd-0.10.0.tgz", + "integrity": "sha512-YGZxdTTL9lmLkCUTpg4j0zQ7IhRB5ZmqNBbGCl3Tg6MP/d5/6sY7L5mmTjzbc6JKgVZYiqTQTNhPFsbXNGlRaA==", + "dev": true, + "dependencies": { + "find-pkg": "^0.1.2", + "fs-exists-sync": "^0.1.0" + }, + "engines": { + "node": ">=0.8" + } }, - "language-subtag-registry": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.21.tgz", - "integrity": "sha512-L0IqwlIXjilBVVYKFT37X9Ih11Um5NEl9cbJIuU/SwP/zEEAbBPOnEeeuxVMf45ydWQRDQN3Nqc96OgbH1K+Pg==", - "dev": true + "node_modules/damerau-levenshtein": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", + "integrity": "sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==", + "dev": true, + "license": "BSD-2-Clause" }, - "language-tags": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.5.tgz", - "integrity": "sha1-0yHbxNowuovzAk4ED6XBRmH5GTo=", + "node_modules/dashdash": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", + "integrity": "sha1-hTz6D3y+L+1d4gMmuN1YEDX24vA=", "dev": true, - "requires": { - "language-subtag-registry": "~0.3.2" + "dependencies": { + "assert-plus": "^1.0.0" + }, + "engines": { + "node": ">=0.10" } }, - "lazy-cache": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", - "integrity": "sha1-odePw6UEdMuAhF07O24dpJpEbo4=" - }, - "leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "dev": true + "node_modules/data-uri-to-buffer": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-5.0.1.tgz", + "integrity": "sha512-a9l6T1qqDogvvnw0nKlfZzqsyikEBZBClF39V3TFoKhDtGBqHu2HkuomJc02j5zft8zrUaXEuoicLeW54RkzPg==", + "dev": true, + "engines": { + "node": ">= 14" + } }, - "levn": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", - "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "node_modules/data-urls": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/data-urls/-/data-urls-5.0.0.tgz", + "integrity": "sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==", "dev": true, - "requires": { - "prelude-ls": "^1.2.1", - "type-check": "~0.4.0" + "license": "MIT", + "dependencies": { + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.0.0" + }, + "engines": { + "node": ">=18" } }, - "liftup": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", - "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "node_modules/data-view-buffer": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-buffer/-/data-view-buffer-1.0.2.tgz", + "integrity": "sha512-EmKO5V3OLXh1rtK2wgXRansaK1/mtVdTUEiEI0W8RkvgT05kfxaH29PliLnpLP73yYO6142Q72QNa8Wx/A5CqQ==", "dev": true, - "requires": { - "extend": "^3.0.2", - "findup-sync": "^4.0.0", - "fined": "^1.2.0", - "flagged-respawn": "^1.0.1", - "is-plain-object": "^2.0.4", - "object.map": "^1.0.1", - "rechoir": "^0.7.0", - "resolve": "^1.19.0" + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/data-view-byte-length": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/data-view-byte-length/-/data-view-byte-length-1.0.2.tgz", + "integrity": "sha512-tuhGbE6CfTM9+5ANGf+oQb72Ky/0+s3xKUpHvShfiz2RxMFgFPjsXuRLBVMtvMs15awe45SRb83D6wH4ew6wlQ==", + "dev": true, "dependencies": { - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "findup-sync": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", - "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^4.0.0", - "micromatch": "^4.0.2", - "resolve-dir": "^1.0.1" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/inspect-js" } }, - "line-height": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/line-height/-/line-height-0.3.1.tgz", - "integrity": "sha1-SxIF7d4YKHKl76PI9iCzGHqcVMk=", - "requires": { - "computed-style": "~0.1.3" + "node_modules/data-view-byte-offset": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/data-view-byte-offset/-/data-view-byte-offset-1.0.1.tgz", + "integrity": "sha512-BS8PfmtDGnrgYdOonGZQdLZslWIeCGFP9tpan0hi1Co2Zr2NKADsvGYA8XxuG/4UWgJ6Cjtv+YJnB6MM69QGlQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-data-view": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "lines-and-columns": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.1.6.tgz", - "integrity": "sha1-HADHQ7QzzQpOgHWPe2SldEDZ/wA=" + "node_modules/date-now": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/date-now/-/date-now-0.1.4.tgz", + "integrity": "sha1-6vQ5/U1ISK105cx9vvIAZyueNFs=", + "dev": true }, - "linkify-it": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-2.2.0.tgz", - "integrity": "sha512-GnAl/knGn+i1U/wjBz3akz2stz+HrHLsxMwHQGofCDfPvlf+gDKN58UtfmUquTY4/MXeE2x7k19KQmeoZi94Iw==", + "node_modules/dateformat": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/dateformat/-/dateformat-4.6.3.tgz", + "integrity": "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA==", "dev": true, - "requires": { - "uc.micro": "^1.0.1" + "engines": { + "node": "*" } }, - "livereload-js": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", - "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", + "node_modules/debounce": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", + "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", "dev": true }, - "load-json-file": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", - "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^2.2.0", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0", - "strip-bom": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "parse-json": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", - "integrity": "sha1-9ID0BDTvgHQfhGkJn43qGPVaTck=", - "dev": true, - "requires": { - "error-ex": "^1.2.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "strip-bom": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", - "integrity": "sha1-YhmoVhZSBJHzV4i9vxRHqZx+aw4=", - "dev": true, - "requires": { - "is-utf8": "^0.2.0" - } + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true } } }, - "loader-runner": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-2.4.0.tgz", - "integrity": "sha512-Jsmr89RcXGIwivFY21FcRrisYZfvLMTWx5kOLc+JTxtpBOG6xML0vzbc6SEQG2FO9/4Fc3wW4LVcB5DmGflaRw==", - "dev": true - }, - "loader-utils": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-1.4.0.tgz", - "integrity": "sha512-qH0WSMBtn/oHuwjy/NucEgbx5dbxxnxup9s4PVXJUDHZBQY+s0NWA9rJf53RBnQZxfch7euUui7hpoAPvALZdA==", + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha1-9lNNFRSCabIDUue+4m9QH5oZEpA=", "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^1.0.1" + "engines": { + "node": ">=0.10.0" } }, - "locate-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", - "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", - "requires": { - "p-locate": "^3.0.0", - "path-exists": "^3.0.0" + "node_modules/decamelize-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/decamelize-keys/-/decamelize-keys-1.1.1.tgz", + "integrity": "sha512-WiPxgEirIV0/eIOMcnFBA3/IJZAZqKnwAwWyvvdi4lsr1WCN22nhdf/3db3DoZcUjTV2SqfzIwNyp6y2xs3nmg==", + "dev": true, + "dependencies": { + "decamelize": "^1.1.0", + "map-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + "node_modules/decimal.js": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", + "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==", + "dev": true, + "license": "MIT" }, - "lodash.clonedeep": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.clonedeep/-/lodash.clonedeep-4.5.0.tgz", - "integrity": "sha1-4j8/nE+Pvd6HJSnBBxhXoIblzO8=", - "dev": true + "node_modules/decode-uri-component": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.2.tgz", + "integrity": "sha512-FqUYQ+8o158GyGTrMFJms9qh3CqTKvAqgqsTnkLI8sKu0028orqBhxNMFkFen0zGyg6epACD32pjVk58ngIErQ==", + "dev": true, + "engines": { + "node": ">=0.10" + } }, - "lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha1-gteb/zCmfEAF/9XiUVMArZyk168=", - "dev": true + "node_modules/decompress": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress/-/decompress-4.2.1.tgz", + "integrity": "sha512-e48kc2IjU+2Zw8cTb6VZcJQ3lgVbS4uuB1TfCHbiZIP/haNXm+SVyhu+87jts5/3ROpd82GSVCoNs/z8l4ZOaQ==", + "dev": true, + "optional": true, + "dependencies": { + "decompress-tar": "^4.0.0", + "decompress-tarbz2": "^4.0.0", + "decompress-targz": "^4.0.0", + "decompress-unzip": "^4.0.1", + "graceful-fs": "^4.1.10", + "make-dir": "^1.0.0", + "pify": "^2.3.0", + "strip-dirs": "^2.0.0" + }, + "engines": { + "node": ">=4" + } }, - "lodash.differencewith": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.differencewith/-/lodash.differencewith-4.5.0.tgz", - "integrity": "sha1-uvr7yRi1UVTheRdqALsK76rIVLc=", - "dev": true + "node_modules/decompress-response": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-3.3.0.tgz", + "integrity": "sha1-gKTdMjdIOEv6JICDYirt7Jgq3/M=", + "dev": true, + "optional": true, + "dependencies": { + "mimic-response": "^1.0.0" + }, + "engines": { + "node": ">=4" + } }, - "lodash.escape": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/lodash.escape/-/lodash.escape-4.0.1.tgz", - "integrity": "sha1-yQRGkMIeBClL6qUXcS/e0fqI3pg=", - "dev": true + "node_modules/decompress-tar": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tar/-/decompress-tar-4.1.1.tgz", + "integrity": "sha512-JdJMaCrGpB5fESVyxwpCx4Jdj2AagLmv3y58Qy4GE6HMVjWz1FeVQk1Ct4Kye7PftcdOo/7U7UKzYBJgqnGeUQ==", + "dev": true, + "optional": true, + "dependencies": { + "file-type": "^5.2.0", + "is-stream": "^1.1.0", + "tar-stream": "^1.5.2" + }, + "engines": { + "node": ">=4" + } }, - "lodash.flatten": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flatten/-/lodash.flatten-4.4.0.tgz", - "integrity": "sha1-8xwiIlqWMtK7+OSt2+8kCqdlph8=", - "dev": true + "node_modules/decompress-tar/node_modules/bl": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/bl/-/bl-1.2.3.tgz", + "integrity": "sha512-pvcNpa0UU69UT341rO6AYy4FVAIkUHuZXRIWbq+zHnsVcRzDDjIAhGuuYoi0d//cwIwtt4pkpKycWEfjdV+vww==", + "dev": true, + "optional": true, + "dependencies": { + "readable-stream": "^2.3.5", + "safe-buffer": "^5.1.1" + } }, - "lodash.flattendeep": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.flattendeep/-/lodash.flattendeep-4.4.0.tgz", - "integrity": "sha1-+wMJF/hqMTTlvJvsDWngAT3f7bI=", - "dev": true + "node_modules/decompress-tar/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } }, - "lodash.get": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", - "integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk=", - "dev": true + "node_modules/decompress-tar/node_modules/tar-stream": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-1.6.2.tgz", + "integrity": "sha512-rzS0heiNf8Xn7/mpdSVVSMAWAoy9bfb1WOTYC78Z0UQKeKa/CWS8FOq0lKGNa8DWKAn9gxjCvMLYc5PGXYlK2A==", + "dev": true, + "optional": true, + "dependencies": { + "bl": "^1.0.0", + "buffer-alloc": "^1.2.0", + "end-of-stream": "^1.0.0", + "fs-constants": "^1.0.0", + "readable-stream": "^2.3.0", + "to-buffer": "^1.1.1", + "xtend": "^4.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } }, - "lodash.isequal": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.isequal/-/lodash.isequal-4.5.0.tgz", - "integrity": "sha1-QVxEePK8wwEgwizhDtMib30+GOA=", - "dev": true + "node_modules/decompress-tarbz2": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-tarbz2/-/decompress-tarbz2-4.1.1.tgz", + "integrity": "sha512-s88xLzf1r81ICXLAVQVzaN6ZmX4A6U4z2nMbOwobxkLoIIfjVMBg7TeguTUXkKeXni795B6y5rnvDw7rxhAq9A==", + "dev": true, + "optional": true, + "dependencies": { + "decompress-tar": "^4.1.0", + "file-type": "^6.1.0", + "is-stream": "^1.1.0", + "seek-bzip": "^1.0.5", + "unbzip2-stream": "^1.0.9" + }, + "engines": { + "node": ">=4" + } }, - "lodash.isplainobject": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", - "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", - "dev": true + "node_modules/decompress-tarbz2/node_modules/file-type": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-6.2.0.tgz", + "integrity": "sha512-YPcTBDV+2Tm0VqjybVd32MHdlEGAtuxS3VAYsumFokDSMG+ROT5wawGlnHDoz7bfMcMDt9hxuXvXwoKUx2fkOg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } }, - "lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha1-vMbEmkKihA7Zl/Mj6tpezRguC/4=", - "dev": true + "node_modules/decompress-targz": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/decompress-targz/-/decompress-targz-4.1.1.tgz", + "integrity": "sha512-4z81Znfr6chWnRDNfFNqLwPvm4db3WuZkqV+UgXQzSngG3CEKdBkw5jrv3axjjL96glyiiKjsxJG3X6WBZwX3w==", + "dev": true, + "optional": true, + "dependencies": { + "decompress-tar": "^4.1.1", + "file-type": "^5.2.0", + "is-stream": "^1.1.0" + }, + "engines": { + "node": ">=4" + } }, - "lodash.merge": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", - "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", - "dev": true + "node_modules/decompress-targz/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } }, - "lodash.truncate": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", - "integrity": "sha1-WjUNoLERO4N+z//VgSy+WNbq4ZM=", + "node_modules/decompress-unzip": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/decompress-unzip/-/decompress-unzip-4.0.1.tgz", + "integrity": "sha1-3qrM39FK6vhVePczroIQ+bSEj2k=", + "dev": true, + "optional": true, + "dependencies": { + "file-type": "^3.8.0", + "get-stream": "^2.2.0", + "pify": "^2.3.0", + "yauzl": "^2.4.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress-unzip/node_modules/file-type": { + "version": "3.9.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-3.9.0.tgz", + "integrity": "sha512-RLoqTXE8/vPmMuTI88DAzhMYC99I8BWv7zYP4A1puo5HIjEJ5EX48ighy4ZyKMG9EDXxBgW6e++cn7d1xuFghA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/get-stream": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-2.3.1.tgz", + "integrity": "sha512-AUGhbbemXxrZJRD5cDvKtQxLuYaIbNtDTK8YqupCI393Q2KSTreEsLUN3ZxAWFGiKTzL6nKuzfcIvieflUX9qA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "object-assign": "^4.0.1", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress-unzip/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/decompress/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/decompress/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/dedent": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.1.tgz", + "integrity": "sha512-+LxW+KLWxu3HW3M2w2ympwtqPrqYRzU8fqi6Fhd18fBALe15blJPI/I4+UHveMVG6lJqB4JNd4UG0S5cnVHwIg==", + "dev": true, + "peerDependencies": { + "babel-plugin-macros": "^3.1.0" + }, + "peerDependenciesMeta": { + "babel-plugin-macros": { + "optional": true + } + } + }, + "node_modules/deep-extend": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", + "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/deep-for-each": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/deep-for-each/-/deep-for-each-3.0.0.tgz", + "integrity": "sha512-pPN+0f8jlnNP+z90qqOdxGghJU5XM6oBDhvAR+qdQzjCg5pk/7VPPvKK1GqoXEFkHza6ZS+Otzzvmr0g3VUaKw==", + "dev": true, + "dependencies": { + "lodash.isplainobject": "^4.0.6" + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/default-gateway": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", + "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", + "dev": true, + "dependencies": { + "execa": "^5.0.0" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/default-gateway/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/default-gateway/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/default-gateway/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/default-gateway/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/default-gateway/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-lazy-prop": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", + "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dev": true, + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-property": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-2.0.2.tgz", + "integrity": "sha512-jwK2UV4cnPpbcG7+VRARKTZPUWowwXA8bzH5NP6ud0oeAxyYPuGZUAC7hMugpCdz4BeSZl2Dl9k66CHJ/46ZYQ==", + "dev": true, + "dependencies": { + "is-descriptor": "^1.0.2", + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/define-property/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/delegate": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", + "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==" + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/desandro-matches-selector": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/desandro-matches-selector/-/desandro-matches-selector-2.0.2.tgz", + "integrity": "sha1-cXvu1NwT59jzdi9wem1YpndCGOE=" + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-file": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/detect-file/-/detect-file-1.0.0.tgz", + "integrity": "sha1-8NZtA2cqglyxtzvbP+YjEMjlUrc=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/detect-libc": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz", + "integrity": "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg==", + "dev": true, + "license": "Apache-2.0", + "optional": true, + "bin": { + "detect-libc": "bin/detect-libc.js" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/detect-newline": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/detect-newline/-/detect-newline-3.1.0.tgz", + "integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/detect-node": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", + "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", "dev": true }, - "lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha1-0CJTc662Uq3BvILklFM5qEJ1R3M=", + "node_modules/devtools-protocol": { + "version": "0.0.1312386", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1312386.tgz", + "integrity": "sha512-DPnhUXvmvKT2dFA/j7B+riVLUt9Q6RKJlcppojL5CoRywJJKLDYnRlw0gTFKfgDPHP5E04UoB71SxoJlVZy8FA==", "dev": true }, - "log-symbols": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", - "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "node_modules/diff": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.2.2.tgz", + "integrity": "sha512-vtcDfH3TOjP8UekytvnHH1o1P4FcUdt4eQ1Y+Abap1tk/OB2MWQvcwS2ClCd1zuIhc3JKOx6p3kod8Vfys3E+A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/diff-sequences": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.6.3.tgz", + "integrity": "sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dns-packet": { + "version": "5.6.1", + "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", + "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", + "dev": true, + "dependencies": { + "@leichtgewicht/ip-codec": "^2.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/doctrine": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-3.0.0.tgz", + "integrity": "sha512-yS+Q5i3hBf7GBkd4KG8a7eBNNWNGLTaEwwYWUijIYM7zrlYDM0BFXHjjPWlWZ1Rg7UaddZeIDmi9jF3HmqiQ2w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/dom-serializer": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-0.2.2.tgz", + "integrity": "sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==", + "dev": true, + "dependencies": { + "domelementtype": "^2.0.1", + "entities": "^2.0.0" + } + }, + "node_modules/dom-serializer/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domelementtype": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-1.3.1.tgz", + "integrity": "sha512-BSKB+TSpMpFI/HOxCNr1O8aMOTZ8hT3pM3GQ0w/mWRmkhEDSFJkkyzz4XQsBV44BChwGkrDfMyjVD0eA2aFV3w==", + "dev": true + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domhandler/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domutils": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.7.0.tgz", + "integrity": "sha512-Lgd2XcJ/NjEw+7tFvfKxOzCYKZsdct5lczQ2ZaQY8Djz7pfAD3Gbp8ySJWtreII/vDlMVmxwa6pHmdxIYgttDg==", + "dev": true, + "optional": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/dot-prop": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-5.3.0.tgz", + "integrity": "sha512-QM8q3zDe58hqUqjraQOmzZ1LIH9SWQJTlEKCH4kJ2oQvLZk7RbQXvtDM2XEq3fwkV9CCvvH4LA0AV+ogFsBM2Q==", + "dev": true, + "dependencies": { + "is-obj": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/dotenv": { + "version": "17.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.3.1.tgz", + "integrity": "sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand": { + "version": "12.0.3", + "resolved": "https://registry.npmjs.org/dotenv-expand/-/dotenv-expand-12.0.3.tgz", + "integrity": "sha512-uc47g4b+4k/M/SeaW1y4OApx+mtLWl92l5LMPP0GNXctZqELk+YGgOPIIC5elYmUH4OuoK3JLhuRUYegeySiFA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dotenv": "^16.4.5" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/dotenv-expand/node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "node_modules/download": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/download/-/download-6.2.5.tgz", + "integrity": "sha512-DpO9K1sXAST8Cpzb7kmEhogJxymyVUd5qz/vCOSyvwtp2Klj2XcDt5YUuasgxka44SxF0q5RriKIwJmQHG2AuA==", + "dev": true, + "optional": true, + "dependencies": { + "caw": "^2.0.0", + "content-disposition": "^0.5.2", + "decompress": "^4.0.0", + "ext-name": "^5.0.0", + "file-type": "5.2.0", + "filenamify": "^2.0.0", + "get-stream": "^3.0.0", + "got": "^7.0.0", + "make-dir": "^1.0.0", + "p-event": "^1.0.0", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/file-type": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-5.2.0.tgz", + "integrity": "sha512-Iq1nJ6D2+yIO4c8HHg4fyVb8mAJieo1Oloy1mLLaB2PvezNedhBVm+QU7g0qM42aiMbRXTxKKwGD17rjKNJYVQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/download/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dev": true, + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/duplexer": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", + "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", + "dev": true + }, + "node_modules/duplexer3": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer3/-/duplexer3-0.1.4.tgz", + "integrity": "sha1-7gHdHKwO08vH/b6jfcCo8c4ALOI=", + "dev": true, + "optional": true + }, + "node_modules/ecc-jsbn": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", + "integrity": "sha1-OoOpBOVDUyh4dMVkt1SThoSamMk=", + "dev": true, + "dependencies": { + "jsbn": "~0.1.0", + "safer-buffer": "^2.1.0" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true, + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.313", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.313.tgz", + "integrity": "sha512-QBMrTWEf00GXZmJyx2lbYD45jpI3TUFnNIzJ5BBc8piGUDwMPa1GV6HJWTZVvY/eiN3fSopl7NRbgGp9sZ9LTA==", + "dev": true, + "license": "ISC" + }, + "node_modules/element-closest": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/element-closest/-/element-closest-3.0.2.tgz", + "integrity": "sha512-JxKQiJKX0Zr5Q2/bCaTx8P+UbfyMET1OQd61qu5xQFeWr1km3fGaxelSJtnfT27XQ5Uoztn2yIyeamAc/VX13g==", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/emittery": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/emittery/-/emittery-0.13.1.tgz", + "integrity": "sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sindresorhus/emittery?sponsor=1" + } + }, + "node_modules/emoji-regex": { + "version": "9.2.2", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", + "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", + "dev": true, + "license": "MIT" + }, + "node_modules/emojis-list": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", + "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/encodeurl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", + "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/enhanced-resolve": { + "version": "5.20.1", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.1.tgz", + "integrity": "sha512-Qohcme7V1inbAfvjItgw0EaxVX5q2rdVEZHRBrEQdRZTssLDGsL8Lwrznl8oQ/6kuTJONLaDcGjkNP247XEhcA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/enquirer": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/enquirer/-/enquirer-2.4.1.tgz", + "integrity": "sha512-rRqJg/6gd538VHvR3PSrdRBb/1Vy2YfzHqzvbhGIQpDRKIa4FgV/54b5Q1xYSxOOwKvjXweS26E0Q+nAMwp2pQ==", + "dev": true, + "dependencies": { + "ansi-colors": "^4.1.1", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/enquirer/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-hCx1oky9PFrJ611mf0ifBLBRW8lUUVRlFolb5gWRfIELabBlbp9xZvrqZLZAs+NxFnbfQoeGd8wDkygjg7U85w==", + "dev": true, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/env-paths": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", + "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/envinfo": { + "version": "7.11.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.11.0.tgz", + "integrity": "sha512-G9/6xF1FPbIw0TtalAMaVPpiq2aDEuKLXM314jPVAO9r2fo2a4BLqMNkmRS7O/xPPZ+COAhGIz3ETvHEV3eUcg==", + "dev": true, + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/error": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/error/-/error-7.2.1.tgz", + "integrity": "sha512-fo9HBvWnx3NGUKMvMwB/CBCMMrfEJgbDTVDEkPygA3Bdd3lM1OyCd+rbQ8BwnpF6GdVeOLDNmyL4N5Bg80ZvdA==", + "dev": true, + "dependencies": { + "string-template": "~0.2.1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, + "node_modules/error-stack-parser": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/error-stack-parser/-/error-stack-parser-2.0.7.tgz", + "integrity": "sha512-chLOW0ZGRf4s8raLrDxa5sdkvPec5YdvwbFnqJme4rk0rFajP8mPtrDL1+I+CwrQDCjswDA5sREX7jYQDQs9vA==", + "dev": true, + "dependencies": { + "stackframe": "^1.1.1" + } + }, + "node_modules/es-abstract": { + "version": "1.24.1", + "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.24.1.tgz", + "integrity": "sha512-zHXBLhP+QehSSbsS9Pt23Gg964240DPd6QCf8WpkqEXxQ7fhdZzYsocOr5u7apWonsS5EjZDmTF+/slGMyasvw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-buffer-byte-length": "^1.0.2", + "arraybuffer.prototype.slice": "^1.0.4", + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "data-view-buffer": "^1.0.2", + "data-view-byte-length": "^1.0.2", + "data-view-byte-offset": "^1.0.1", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "es-set-tostringtag": "^2.1.0", + "es-to-primitive": "^1.3.0", + "function.prototype.name": "^1.1.8", + "get-intrinsic": "^1.3.0", + "get-proto": "^1.0.1", + "get-symbol-description": "^1.1.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "internal-slot": "^1.1.0", + "is-array-buffer": "^3.0.5", + "is-callable": "^1.2.7", + "is-data-view": "^1.0.2", + "is-negative-zero": "^2.0.3", + "is-regex": "^1.2.1", + "is-set": "^2.0.3", + "is-shared-array-buffer": "^1.0.4", + "is-string": "^1.1.1", + "is-typed-array": "^1.1.15", + "is-weakref": "^1.1.1", + "math-intrinsics": "^1.1.0", + "object-inspect": "^1.13.4", + "object-keys": "^1.1.1", + "object.assign": "^4.1.7", + "own-keys": "^1.0.1", + "regexp.prototype.flags": "^1.5.4", + "safe-array-concat": "^1.1.3", + "safe-push-apply": "^1.0.0", + "safe-regex-test": "^1.1.0", + "set-proto": "^1.0.0", + "stop-iteration-iterator": "^1.1.0", + "string.prototype.trim": "^1.2.10", + "string.prototype.trimend": "^1.0.9", + "string.prototype.trimstart": "^1.0.8", + "typed-array-buffer": "^1.0.3", + "typed-array-byte-length": "^1.0.3", + "typed-array-byte-offset": "^1.0.4", + "typed-array-length": "^1.0.7", + "unbox-primitive": "^1.1.0", + "which-typed-array": "^1.1.19" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-iterator-helpers": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.3.1.tgz", + "integrity": "sha512-zWwRvqWiuBPr0muUG/78cW3aHROFCNIQ3zpmYDpwdbnt2m+xlNyRWpHBpa2lJjSBit7BQ+RXA1iwbSmu5yJ/EQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.24.1", + "es-errors": "^1.3.0", + "es-set-tostringtag": "^2.1.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.3.0", + "globalthis": "^1.0.4", + "gopd": "^1.2.0", + "has-property-descriptors": "^1.0.2", + "has-proto": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "iterator.prototype": "^1.1.5", + "math-intrinsics": "^1.1.0", + "safe-array-concat": "^1.1.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-shim-unscopables": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/es-shim-unscopables/-/es-shim-unscopables-1.1.0.tgz", + "integrity": "sha512-d9T8ucsEhh8Bi1woXCf+TIKDIROLG5WCkxg8geBCbvk22kzwC5G2OnXVMO6FUsvQlgUUXQ2itephWDLqDzbeCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-to-primitive": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.3.0.tgz", + "integrity": "sha512-w+5mJ3GuFL+NjVtJlvydShqE1eN3h3PbI7/5LAsYJP/2qtuMXjfL2LpHSRqo4b4eSF5K/DH1JXKUAHSB2UW50g==", + "dev": true, + "dependencies": { + "is-callable": "^1.2.7", + "is-date-object": "^1.0.5", + "is-symbol": "^1.0.4" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/escodegen/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/escodegen/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint": { + "version": "8.57.1", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-8.57.1.tgz", + "integrity": "sha512-ypowyDxpVSYpkXr9WPv2PAZCtNip1Mv5KTW0SCurXv/9iOpcrH9PaqUElksqEB6pChqHGDRCFTyrZlGhnLNGiA==", + "deprecated": "This version is no longer supported. Please see https://eslint.org/version-support for other options.", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@eslint-community/regexpp": "^4.6.1", + "@eslint/eslintrc": "^2.1.4", + "@eslint/js": "8.57.1", + "@humanwhocodes/config-array": "^0.13.0", + "@humanwhocodes/module-importer": "^1.0.1", + "@nodelib/fs.walk": "^1.2.8", + "@ungap/structured-clone": "^1.2.0", + "ajv": "^6.12.4", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.2", + "debug": "^4.3.2", + "doctrine": "^3.0.0", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^7.2.2", + "eslint-visitor-keys": "^3.4.3", + "espree": "^9.6.1", + "esquery": "^1.4.2", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^6.0.1", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "globals": "^13.19.0", + "graphemer": "^1.4.0", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "is-path-inside": "^3.0.3", + "js-yaml": "^4.1.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "levn": "^0.4.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.2", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3", + "strip-ansi": "^6.0.1", + "text-table": "^0.2.0" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-config-prettier": { + "version": "8.10.2", + "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-8.10.2.tgz", + "integrity": "sha512-/IGJ6+Dka158JnP5n5YFMOszjDWrXggGz1LaK/guZq9vZTmniaKlHcsscvkAhn9y4U+BU3JuUdYvtAMcv30y4A==", + "dev": true, + "license": "MIT", + "bin": { + "eslint-config-prettier": "bin/cli.js" + }, + "peerDependencies": { + "eslint": ">=7.0.0" + } + }, + "node_modules/eslint-import-context": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/eslint-import-context/-/eslint-import-context-0.1.9.tgz", + "integrity": "sha512-K9Hb+yRaGAGUbwjhFNHvSmmkZs9+zbuoe3kFQ4V1wYjrepUFYM2dZAfNtjbbj3qsPfUfsA68Bx/ICWQMi+C8Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-tsconfig": "^4.10.1", + "stable-hash-x": "^0.2.0" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-context" + }, + "peerDependencies": { + "unrs-resolver": "^1.0.0" + }, + "peerDependenciesMeta": { + "unrs-resolver": { + "optional": true + } + } + }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-import-resolver-typescript": { + "version": "4.4.4", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-typescript/-/eslint-import-resolver-typescript-4.4.4.tgz", + "integrity": "sha512-1iM2zeBvrYmUNTj2vSC/90JTHDth+dfOfiNKkxApWRsTJYNrc8rOdxxIf5vazX+BiAXTeOT0UvWpGI/7qIWQOw==", + "dev": true, + "license": "ISC", + "dependencies": { + "debug": "^4.4.1", + "eslint-import-context": "^0.1.8", + "get-tsconfig": "^4.10.1", + "is-bun-module": "^2.0.0", + "stable-hash-x": "^0.2.0", + "tinyglobby": "^0.2.14", + "unrs-resolver": "^1.7.11" + }, + "engines": { + "node": "^16.17.0 || >=18.6.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-import-resolver-typescript" + }, + "peerDependencies": { + "eslint": "*", + "eslint-plugin-import": "*", + "eslint-plugin-import-x": "*" + }, + "peerDependenciesMeta": { + "eslint-plugin-import": { + "optional": true + }, + "eslint-plugin-import-x": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils": { + "version": "2.12.1", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.12.1.tgz", + "integrity": "sha512-L8jSWTze7K2mTg0vos/RuLRS5soomksDPoJLXIslC7c8Wmut3bx7CPpJijDcBZtxQ5lrbUdM+s0OlNbz0DCDNw==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import": { + "version": "2.32.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.32.0.tgz", + "integrity": "sha512-whOE1HFo/qJDyX4SnXzP4N6zOWn79WhnCUY/iDR0mPfQZO8wcYE4JClzI2oZrhBnnMUCBCHZhO6VQyoBU95mZA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@rtsao/scc": "^1.1.0", + "array-includes": "^3.1.9", + "array.prototype.findlastindex": "^1.2.6", + "array.prototype.flat": "^1.3.3", + "array.prototype.flatmap": "^1.3.3", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.12.1", + "hasown": "^2.0.2", + "is-core-module": "^2.16.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "object.groupby": "^1.0.3", + "object.values": "^1.2.1", + "semver": "^6.3.1", + "string.prototype.trimend": "^1.0.9", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-import/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-plugin-jest": { + "version": "27.9.0", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.9.0.tgz", + "integrity": "sha512-QIT7FH7fNmd9n4se7FFKHbsLKGQiw885Ds6Y/sxKgCZ6natwCsXdgPOADnYVxN2QrRweF0FZWbJ6S7Rsn7llug==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/utils": "^5.10.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@typescript-eslint/eslint-plugin": "^5.0.0 || ^6.0.0 || ^7.0.0", + "eslint": "^7.0.0 || ^8.0.0", + "jest": "*" + }, + "peerDependenciesMeta": { + "@typescript-eslint/eslint-plugin": { + "optional": true + }, + "jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/scope-manager": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-5.62.0.tgz", + "integrity": "sha512-VXuvVvZeQCQb5Zgf4HAxc04q5j+WrNAtNh9OwCsCgpKqESMTu3tF/jhZ3xG6T4NZwWl65Bg8KuS2uEvhSfLl0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/types": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-5.62.0.tgz", + "integrity": "sha512-87NVngcbVXUahrRTqIK27gD2t5Cu1yuCXxbLcFtCzZGlfyVWWh8mLHkoxzjsB6DDNnvdL+fW8MiwPEJyGJQDgQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/typescript-estree": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-5.62.0.tgz", + "integrity": "sha512-CmcQ6uY7b9y694lKdRB8FEel7JbU/40iSAPomu++SjLMntB+2Leay2LO6i8VnJk58MtE9/nQSFIH6jpyRWyYzA==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/visitor-keys": "5.62.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "semver": "^7.3.7", + "tsutils": "^3.21.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/utils": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-5.62.0.tgz", + "integrity": "sha512-n8oxjeb5aIbPFEtmQxQYOLI0i9n5ySBEY/ZEHHZqKQSFnxio1rv6dthascc9dLuwrL0RC5mPCxB7vnAVGAYWAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.2.0", + "@types/json-schema": "^7.0.9", + "@types/semver": "^7.3.12", + "@typescript-eslint/scope-manager": "5.62.0", + "@typescript-eslint/types": "5.62.0", + "@typescript-eslint/typescript-estree": "5.62.0", + "eslint-scope": "^5.1.1", + "semver": "^7.3.7" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/eslint-plugin-jest/node_modules/@typescript-eslint/visitor-keys": { + "version": "5.62.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-5.62.0.tgz", + "integrity": "sha512-07ny+LHRzQXepkGg6w0mFY41fVUNBrL2Roj/++7V1txKugfjm/Ci/qSND03r2RhlJhJYMcTn9AhhSSqQp0Ysyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "5.62.0", + "eslint-visitor-keys": "^3.3.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-plugin-jest/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-plugin-jsdoc": { + "version": "46.10.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsdoc/-/eslint-plugin-jsdoc-46.10.1.tgz", + "integrity": "sha512-x8wxIpv00Y50NyweDUpa+58ffgSAI5sqe+zcZh33xphD0AVh+1kqr1ombaTRb7Fhpove1zfUuujlX9DWWBP5ag==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@es-joy/jsdoccomment": "~0.41.0", + "are-docs-informative": "^0.0.2", + "comment-parser": "1.4.1", + "debug": "^4.3.4", + "escape-string-regexp": "^4.0.0", + "esquery": "^1.5.0", + "is-builtin-module": "^3.2.1", + "semver": "^7.5.4", + "spdx-expression-parse": "^4.0.0" + }, + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0 || ^9.0.0" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint-plugin-jsdoc/node_modules/spdx-expression-parse": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-4.0.0.tgz", + "integrity": "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" + } + }, + "node_modules/eslint-plugin-jsx-a11y": { + "version": "6.10.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.2.tgz", + "integrity": "sha512-scB3nz4WmG75pV8+3eRUQOHZlNSUhFNq37xnpgRkCCELU3XMvXAxLk1eqWWyE22Ki4Q01Fnsw9BA3cJHDPgn2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "aria-query": "^5.3.2", + "array-includes": "^3.1.8", + "array.prototype.flatmap": "^1.3.2", + "ast-types-flow": "^0.0.8", + "axe-core": "^4.10.0", + "axobject-query": "^4.1.0", + "damerau-levenshtein": "^1.0.8", + "emoji-regex": "^9.2.2", + "hasown": "^2.0.2", + "jsx-ast-utils": "^3.3.5", + "language-tags": "^1.0.9", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.8", + "safe-regex-test": "^1.0.3", + "string.prototype.includes": "^2.0.1" + }, + "engines": { + "node": ">=4.0" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" + } + }, + "node_modules/eslint-plugin-playwright": { + "version": "0.15.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-playwright/-/eslint-plugin-playwright-0.15.3.tgz", + "integrity": "sha512-LQMW5y0DLK5Fnpya7JR1oAYL2/7Y9wDiYw6VZqlKqcRGSgjbVKNqxraphk7ra1U3Bb5EK444xMgUlQPbMg2M1g==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "eslint": ">=7", + "eslint-plugin-jest": ">=25" + }, + "peerDependenciesMeta": { + "eslint-plugin-jest": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-prettier": { + "version": "5.5.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-prettier/-/eslint-plugin-prettier-5.5.5.tgz", + "integrity": "sha512-hscXkbqUZ2sPithAuLm5MXL+Wph+U7wHngPBv9OMWwlP8iaflyxpjTYZkmdgB4/vPIhemRlBEoLrH7UC1n7aUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "prettier-linter-helpers": "^1.0.1", + "synckit": "^0.11.12" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint-plugin-prettier" + }, + "peerDependencies": { + "@types/eslint": ">=8.0.0", + "eslint": ">=8.0.0", + "eslint-config-prettier": ">= 7.0.0 <10.0.0 || >=10.1.0", + "prettier": ">=3.0.0" + }, + "peerDependenciesMeta": { + "@types/eslint": { + "optional": true + }, + "eslint-config-prettier": { + "optional": true + } + } + }, + "node_modules/eslint-plugin-react": { + "version": "7.37.5", + "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-7.37.5.tgz", + "integrity": "sha512-Qteup0SqU15kdocexFNAJMvCJEfa2xUKNV4CC1xsVMrIIqEy3SQ/rqyxCWNzfrd3/ldy6HMlD2e0JDVpDg2qIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.8", + "array.prototype.findlast": "^1.2.5", + "array.prototype.flatmap": "^1.3.3", + "array.prototype.tosorted": "^1.1.4", + "doctrine": "^2.1.0", + "es-iterator-helpers": "^1.2.1", + "estraverse": "^5.3.0", + "hasown": "^2.0.2", + "jsx-ast-utils": "^2.4.1 || ^3.0.0", + "minimatch": "^3.1.2", + "object.entries": "^1.1.9", + "object.fromentries": "^2.0.8", + "object.values": "^1.2.1", + "prop-types": "^15.8.1", + "resolve": "^2.0.0-next.5", + "semver": "^6.3.1", + "string.prototype.matchall": "^4.0.12", + "string.prototype.repeat": "^1.0.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" + } + }, + "node_modules/eslint-plugin-react-hooks": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/eslint-plugin-react-hooks/-/eslint-plugin-react-hooks-4.6.2.tgz", + "integrity": "sha512-QzliNJq4GinDBcD8gPB5v0wh6g8q3SUi6EFF0x8N/BL9PoVs0atuGc47ozMRyOWAKdwaZ5OnbOEa3WR+dSGKuQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0" + } + }, + "node_modules/eslint-plugin-react/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint-plugin-react/node_modules/resolve": { + "version": "2.0.0-next.6", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-2.0.0-next.6.tgz", + "integrity": "sha512-3JmVl5hMGtJ3kMmB3zi3DL25KfkCEyy3Tw7Gmw7z5w8M9WlwoPFnIvwChzu1+cF3iaK3sp18hhPz8ANeimdJfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "node-exports-info": "^1.6.0", + "object-keys": "^1.1.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/eslint-plugin-react/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-2.1.0.tgz", + "integrity": "sha512-0rSmRBzXgDzIsD6mGdJgevzgezI534Cer5L/vyMX0kHzT/jiB43jRhd9YUlMGYLQy2zprNmoT8qasCGtY+QaKw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=10" + } + }, + "node_modules/eslint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/eslint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/eslint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/eslint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/eslint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/eslint/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/eslint/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/eslint-scope": { + "version": "7.2.2", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz", + "integrity": "sha512-dOt21O7lTMhDM+X9mB4GX+DZrZtCUJPL/wlcTqxyrx5IvO0IYtILdtrQGQp+8n5S0gwSVmOf9NQrjMOgfQZlIg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/eslint/node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/eslint/node_modules/globals": { + "version": "13.24.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", + "integrity": "sha512-AhO5QUcj8llrbG09iWhPU2B204J1xnPeL8kQmVorSsy+Sjj1sk8gIyh6cUocGmH4L0UuhAJy+hJMRA4mgA4mFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "type-fest": "^0.20.2" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/eslint/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/eslint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/espree": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/espree/-/espree-9.6.1.tgz", + "integrity": "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==", + "dependencies": { + "acorn": "^8.9.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esquery/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ev-emitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-1.1.1.tgz", + "integrity": "sha512-ipiDYhdQSCZ4hSbX4rMW+XzNKMD1prg/sTvoVmSLkuQ1MVlwjJQQA+sW8tMYR3BLUr9KjodFV4pvzunvRhd33Q==" + }, + "node_modules/eventemitter2": { + "version": "0.4.14", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-0.4.14.tgz", + "integrity": "sha512-K7J4xq5xAD5jHsGM5ReWXRTFa3JRGofHiMcVgQ8PRwgWxzjHpMWCIzsmyf60+mh8KLsqYPcjUMa0AC4hd6lPyQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/events-universal": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/events-universal/-/events-universal-1.0.1.tgz", + "integrity": "sha512-LUd5euvbMLpwOF8m6ivPCbhQeSiYVNb8Vs0fQ8QjXo0JTkEHpz8pxdQf0gStltaPpw0Cca8b39KxvK9cfKRiAw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "bare-events": "^2.7.0" + } + }, + "node_modules/exec-buffer": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/exec-buffer/-/exec-buffer-3.2.0.tgz", + "integrity": "sha512-wsiD+2Tp6BWHoVv3B+5Dcx6E7u5zky+hUwOHjuH2hKSLR3dvRmX8fk8UD8uqQixHs4Wk6eDmiegVrMPjKj7wpA==", + "dev": true, + "optional": true, + "dependencies": { + "execa": "^0.7.0", + "p-finally": "^1.0.0", + "pify": "^3.0.0", + "rimraf": "^2.5.4", + "tempfile": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exec-buffer/node_modules/execa": { + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-0.7.0.tgz", + "integrity": "sha512-RztN09XglpYI7aBBrJCPW95jEH7YF1UEPOoX9yDhUTPdp7mK+CQvnLTuD10BNXZ3byLTu2uehZ8EcKT/4CGiFw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "cross-spawn": "^5.0.1", + "get-stream": "^3.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/exec-buffer/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/exec-buffer/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/execa": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/execa/-/execa-1.0.0.tgz", + "integrity": "sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA==", + "dev": true, + "optional": true, + "dependencies": { + "cross-spawn": "^6.0.0", + "get-stream": "^4.0.0", + "is-stream": "^1.1.0", + "npm-run-path": "^2.0.0", + "p-finally": "^1.0.0", + "signal-exit": "^3.0.0", + "strip-eof": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/execa/node_modules/cross-spawn": { + "version": "6.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.6.tgz", + "integrity": "sha512-VqCUuhcd1iB+dsv8gxPttb5iZh/D0iubSP21g36KXdEuf6I5JiioesUVjpCdHV9MZRUfVFlvwtIUyPfxo5trtw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "nice-try": "^1.0.4", + "path-key": "^2.0.1", + "semver": "^5.5.0", + "shebang-command": "^1.2.0", + "which": "^1.2.9" + }, + "engines": { + "node": ">=4.8" + } + }, + "node_modules/execa/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "optional": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/execa/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "optional": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/executable": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/executable/-/executable-4.1.1.tgz", + "integrity": "sha512-8iA79xD3uAch729dUG8xaaBBFGaEa0wdD2VkYLFHwlqosEj/jT66AzcreRDSgV7ehnNLBW2WR5jIXwGKjVdTLg==", + "dev": true, + "optional": true, + "dependencies": { + "pify": "^2.2.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/executable/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/exit": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/exit/-/exit-0.1.2.tgz", + "integrity": "sha1-BjJjj42HfMghB9MKD/8aF8uhzQw=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/expand-brackets": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/expand-brackets/-/expand-brackets-2.1.4.tgz", + "integrity": "sha1-t3c14xXOMPa27/D4OwQVGiJEliI=", + "dev": true, + "dependencies": { + "debug": "^2.3.3", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "posix-character-classes": "^0.1.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/expand-brackets/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expand-brackets/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/expand-tilde": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-1.2.2.tgz", + "integrity": "sha512-rtmc+cjLZqnu9dSYosX9EWmSJhTwpACgJQTfj4hgg2JjOD/6SIQalZrt4a3aQeh++oNxkazcaxrhPUj6+g5G/Q==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/expect/-/expect-29.7.0.tgz", + "integrity": "sha512-2Zks0hf1VLFYI1kbh0I5jP3KHHyCHpkfyHBzsSXRFgl/Bg9mWYfMW8oD+PdMPlEwy5HNsR9JutYy6pMeOh61nw==", + "dev": true, + "dependencies": { + "@jest/expect-utils": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/expect-puppeteer": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/expect-puppeteer/-/expect-puppeteer-4.4.0.tgz", + "integrity": "sha512-6Ey4Xy2xvmuQu7z7YQtMsaMV0EHJRpVxIDOd5GRrm04/I3nkTKIutELfECsLp6le+b3SSa3cXhPiw6PgqzxYWA==", + "dev": true + }, + "node_modules/express": { + "version": "4.22.1", + "resolved": "https://registry.npmjs.org/express/-/express-4.22.1.tgz", + "integrity": "sha512-F2X8g9P1X7uCPZMA3MVf9wcTqlyNp7IhH5qPCI0izhaOIYXaW9L535tGA3qmjRzpH+bZczqq7hVKxTR4NWnu+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "~1.20.3", + "content-disposition": "~0.5.4", + "content-type": "~1.0.4", + "cookie": "~0.7.1", + "cookie-signature": "~1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "~1.3.1", + "fresh": "~0.5.2", + "http-errors": "~2.0.0", + "merge-descriptors": "1.0.3", + "methods": "~1.1.2", + "on-finished": "~2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "~0.1.12", + "proxy-addr": "~2.0.7", + "qs": "~6.14.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "~0.19.0", + "serve-static": "~1.16.2", + "setprototypeof": "1.2.0", + "statuses": "~2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/express/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/express/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/express/node_modules/path-to-regexp": { + "version": "0.1.12", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", + "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ext-list": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ext-list/-/ext-list-2.2.2.tgz", + "integrity": "sha512-u+SQgsubraE6zItfVA0tBuCBhfU9ogSRnsvygI7wht9TS510oLkBRXBsqopeUG/GBOIQyKZO9wjTqIu/sf5zFA==", + "dev": true, + "optional": true, + "dependencies": { + "mime-db": "^1.28.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ext-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ext-name/-/ext-name-5.0.0.tgz", + "integrity": "sha512-yblEwXAbGv1VQDmow7s38W77hzAgJAO50ztBLMcUyUBfxv1HC+LGwtiEN+Co6LtlqT/5uwVOxsD4TNIilWhwdQ==", + "dev": true, + "optional": true, + "dependencies": { + "ext-list": "^2.0.0", + "sort-keys-length": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extend-shallow": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-3.0.2.tgz", + "integrity": "sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "assign-symbols": "^1.0.0", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extend-shallow/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/external-editor": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/external-editor/-/external-editor-2.2.0.tgz", + "integrity": "sha512-bSn6gvGxKt+b7+6TKEv1ZycHleA7aHhRHyAqJyp5pbUFuYYNIzpZnQDk7AsYckyWdEnTeAnay0aCy2aV6iTk9A==", + "dev": true, + "dependencies": { + "chardet": "^0.4.0", + "iconv-lite": "^0.4.17", + "tmp": "^0.0.33" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/external-editor/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", + "integrity": "sha512-Nmb6QXkELsuBr24CJSkilo6UHHgbekK5UiZgfE6UHD3Eb27YC6oD+bhcT+tJ6cl8dmsgdQxnWlcry8ksBIBLpw==", + "dev": true, + "dependencies": { + "array-unique": "^0.3.2", + "define-property": "^1.0.0", + "expand-brackets": "^2.1.4", + "extend-shallow": "^2.0.1", + "fragment-cache": "^0.2.1", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/define-property": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/extglob/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/extsprintf": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", + "integrity": "sha1-lpGEQOMEGnpBT4xS48V06zw+HgU=", + "dev": true, + "engines": [ + "node >=0.6.0" + ] + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true + }, + "node_modules/fast-diff": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-diff/-/fast-diff-1.3.0.tgz", + "integrity": "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/fast-fifo": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/fast-fifo/-/fast-fifo-1.3.2.tgz", + "integrity": "sha512-/d9sfos4yxzpwkDkuN7k2SqFKtYNmCTzgfEpz82x34IM9/zc8KGxQoXg1liNC/izpRM/MBdt44Nmx41ZWqk+FQ==", + "dev": true + }, + "node_modules/fast-glob": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", + "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", + "dev": true, + "dependencies": { + "@nodelib/fs.stat": "^2.0.2", + "@nodelib/fs.walk": "^1.2.3", + "glob-parent": "^5.1.2", + "merge2": "^1.3.0", + "micromatch": "^4.0.8" + }, + "engines": { + "node": ">=8.6.0" + } + }, + "node_modules/fast-glob/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/fast-glob/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/fast-glob/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/fast-glob/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.0.1.tgz", + "integrity": "sha512-MWipKbbYiYI0UC7cl8m/i/IWTqfC8YXsqjzybjddLsFjStroQzsHXkc73JutMvBiXmOvapk+axIl79ig5t55Bw==", + "dev": true + }, + "node_modules/fast-xml-parser": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-4.5.4.tgz", + "integrity": "sha512-jE8ugADnYOBsu1uaoayVl1tVKAMNOXyjwvv2U6udEA2ORBhDooJDWoGxTkhd4Qn4yh59JVVt/pKXtjPwx9OguQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "optional": true, + "dependencies": { + "strnum": "^1.0.5" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fastq": { + "version": "1.13.0", + "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.13.0.tgz", + "integrity": "sha512-YpkpUnK8od0o1hmeSc7UUs/eB/vIPWJYjKck2QKIzAf71Vm1AAQ3EbuZB3g2JIy+pg+ERD0vqI79KyZiB2e2Nw==", + "dev": true, + "dependencies": { + "reusify": "^1.0.4" + } + }, + "node_modules/faye-websocket": { + "version": "0.10.0", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.10.0.tgz", + "integrity": "sha512-Xhj93RXbMSq8urNCUq4p9l0P6hnySJ/7YNRhYNug0bLOuii7pKO7xQFb5mx9xZXWCar88pLPb805PvUkwrLZpQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fb-watchman": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/fb-watchman/-/fb-watchman-2.0.2.tgz", + "integrity": "sha512-p5161BqbuCaSnB8jIbzQHOlpgsPmK5rJVDfDKO91Axs5NC1uu3HRQm6wt9cd9/+GtQQIO53JdGXXoyDpTAsgYA==", + "dev": true, + "dependencies": { + "bser": "2.1.1" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha1-JcfInLH5B3+IkbvmHY85Dq4lbx4=", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fetch-blob": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.1.5.tgz", + "integrity": "sha512-N64ZpKqoLejlrwkIAnb9iLSA3Vx/kjgzpcDhygcqJ2KKjky8nCgUQ+dzXtbrLaWZGZNmNfQTsiQ0weZ1svglHg==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/figures": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", + "integrity": "sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "escape-string-regexp": "^1.0.5", + "object-assign": "^4.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/file-entry-cache": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", + "integrity": "sha512-7Gps/XWymbLk2QLYK4NzpMOrYjMhdIxXuIvy2QBsLE6ljuodKvdkWs/cpyJJ3CVIVpH0Oi1Hvg1ovbMzLdFBBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^3.0.4" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/file-sync-cmp": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/file-sync-cmp/-/file-sync-cmp-0.1.1.tgz", + "integrity": "sha1-peeo/7+kk7Q7kju9TKiaU7Y7YSs=", + "dev": true + }, + "node_modules/file-type": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-10.11.0.tgz", + "integrity": "sha512-uzk64HRpUZyTGZtVuvrjP0FYxzQrBf4rojot6J65YMEbwBLB0CWm0CLojVpwpmFmxcE/lkvYICgfcGozbBq6rw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/filename-reserved-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/filename-reserved-regex/-/filename-reserved-regex-2.0.0.tgz", + "integrity": "sha1-q/c9+rc10EVECr/qLZHzieu/oik=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/filenamify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/filenamify/-/filenamify-2.1.0.tgz", + "integrity": "sha512-ICw7NTT6RsDp2rnYKVd8Fu4cr6ITzGy3+u4vUujPkabyaz+03F24NWEX7fs5fp+kBonlaqPH8fAO2NM+SXt/JA==", + "dev": true, + "optional": true, + "dependencies": { + "filename-reserved-regex": "^2.0.0", + "strip-outer": "^1.0.0", + "trim-repeated": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/fill-range": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-4.0.0.tgz", + "integrity": "sha512-VcpLTWqWDiTerugjj8e3+esbg+skS3M9e54UuR3iCeIDMXCLTsAH8hTSzDQU/X6/6t3eYkOKoZSef2PlU6U1XQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "is-number": "^3.0.0", + "repeat-string": "^1.6.1", + "to-regex-range": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fill-range/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/finalhandler": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", + "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/finalhandler/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" + }, + "node_modules/find-cache-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", + "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", + "dev": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^7.0.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/find-up": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", + "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", + "dev": true, + "dependencies": { + "locate-path": "^7.1.0", + "path-exists": "^5.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/locate-path": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", + "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "dev": true, + "dependencies": { + "p-locate": "^6.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-limit": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", + "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^1.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/p-locate": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", + "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "dev": true, + "dependencies": { + "p-limit": "^4.0.0" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/path-exists": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", + "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", + "dev": true, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "node_modules/find-cache-dir/node_modules/pkg-dir": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", + "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "dev": true, + "dependencies": { + "find-up": "^6.3.0" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-dir/node_modules/yocto-queue": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.2.tgz", + "integrity": "sha512-4LCcse/U2MHZ63HAJVE+v71o7yOdIe4cZ70Wpf8D/IyjDKYQLV5GD46B+hSTjJsvV5PztjvHoU580EftxjDZFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-file-up": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/find-file-up/-/find-file-up-0.1.3.tgz", + "integrity": "sha512-mBxmNbVyjg1LQIIpgO8hN+ybWBgDQK8qjht+EbrTCGmmPV/sc7RF1i9stPTD6bpvXZywBdrwRYxhSdJv867L6A==", + "dev": true, + "dependencies": { + "fs-exists-sync": "^0.1.0", + "resolve-dir": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-parent-dir": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/find-parent-dir/-/find-parent-dir-0.3.1.tgz", + "integrity": "sha512-o4UcykWV/XN9wm+jMEtWLPlV8RXCZnMhQI6F6OdHeSez7iiJWePw8ijOlskJZMsaQoGR/b7dH6lO02HhaTN7+A==", + "dev": true + }, + "node_modules/find-pkg": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/find-pkg/-/find-pkg-0.1.2.tgz", + "integrity": "sha512-0rnQWcFwZr7eO0513HahrWafsc3CTFioEB7DRiEYCUM/70QXSY8f3mCST17HXLcPvEhzH/Ty/Bxd72ZZsr/yvw==", + "dev": true, + "dependencies": { + "find-file-up": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/find-process": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", + "integrity": "sha512-ncYFnWEIwL7PzmrK1yZtaccN8GhethD37RzBHG6iOZoFYB4vSmLLXfeWJjeN5nMvCJMjOtBvBBF8OgxEcikiZg==", + "dev": true, + "dependencies": { + "chalk": "~4.1.2", + "commander": "^12.1.0", + "loglevel": "^1.9.2" + }, + "bin": { + "find-process": "bin/find-process.js" + } + }, + "node_modules/find-process/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/find-process/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/find-process/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/find-process/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/find-process/node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/find-process/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-process/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-versions": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/find-versions/-/find-versions-3.2.0.tgz", + "integrity": "sha512-P8WRou2S+oe222TOCHitLy8zj+SIsVJh52VP4lvXkaFVnOFFdoWv1H1Jjvel1aI6NCFOAaeAVm8qrI0odiLcww==", + "dev": true, + "optional": true, + "dependencies": { + "semver-regex": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/findup-sync": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-5.0.0.tgz", + "integrity": "sha512-MzwXju70AuyflbgeOhzvQWAvvQdo1XL0A9bVvlXsYcFEBM87WR4OakL4OfZq+QRmr+duJubio+UtNQCPsVESzQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.3", + "micromatch": "^4.0.4", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/findup-sync/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/findup-sync/node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/findup-sync/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/findup-sync/node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/findup-sync/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/findup-sync/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/fined": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fined/-/fined-1.2.0.tgz", + "integrity": "sha512-ZYDqPLGxDkDhDZBjZBb+oD1+j0rA4E0pXY50eplAAOPg2N/gUBSSk5IM1/QhPfyVo19lJ+CvXpqfvk+b2p/8Ng==", + "dev": true, + "dependencies": { + "expand-tilde": "^2.0.2", + "is-plain-object": "^2.0.3", + "object.defaults": "^1.1.0", + "object.pick": "^1.2.0", + "parse-filepath": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/fined/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fined/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fizzy-ui-utils": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/fizzy-ui-utils/-/fizzy-ui-utils-2.0.7.tgz", + "integrity": "sha512-CZXDVXQ1If3/r8s0T+v+qVeMshhfcuq0rqIFgJnrtd+Bu8GmDmqMjntjUePypVtjHXKJ6V4sw9zeyox34n9aCg==", + "dependencies": { + "desandro-matches-selector": "^2.0.0" + } + }, + "node_modules/flagged-respawn": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/flagged-respawn/-/flagged-respawn-1.0.1.tgz", + "integrity": "sha512-lNaHNVymajmk0OJMBn8fVUAU1BtDeKIqKoVhk4xAALB57aALg6b4W0MfJ/cUE0g9YBXy5XhSlPIpYIJ7HaY/3Q==", + "dev": true, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/flat-cache": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-3.2.0.tgz", + "integrity": "sha512-CYcENa+FtcUKLmhhqyctpclsq7QF38pKjZHsGNiSQF5r4FtoKDWabFDl3hzaEQMvT1LHEysw5twgLvpYYb4vbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.3", + "rimraf": "^3.0.2" + }, + "engines": { + "node": "^10.12.0 || >=12.0.0" + } + }, + "node_modules/flat-cache/node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/flat-cache/node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/flat-cache/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/follow-redirects": { + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", + "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "license": "MIT", + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/for-each": { + "version": "0.3.5", + "resolved": "https://registry.npmjs.org/for-each/-/for-each-0.3.5.tgz", + "integrity": "sha512-dKx12eRCVIzqCxFGplyFKJMPvLEWgmNtUrpTiJIR5u97zEhRG8ySrtboPHZXx7daLxQVrl643cTzbab2tkQjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/for-in": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-1.0.2.tgz", + "integrity": "sha512-7EwmXrOjyL+ChxMhmG5lnW9MPt1aIeZEwKhQzoBUdTV0N3zuwWDZYVJatDvZ2OyzPUvdIAZDsCetk3coyMfcnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/for-own": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-0.1.5.tgz", + "integrity": "sha512-SKmowqGTJoPzLO1T0BBJpkfp3EMacCMOuH40hOUbrbzElVktk4DioXVM99QkLCyKoiuOmyjgcWMpVz2xjE7LZw==", + "dev": true, + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/forever-agent": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", + "integrity": "sha1-+8cfDEGt6zf5bFd60e1C2P2sypE=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/form-data": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", + "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.6", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fraction.js": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz", + "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "node_modules/fragment-cache": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz", + "integrity": "sha1-QpD60n8T6Jvn8zeZxrxaCr//DRk=", + "dev": true, + "dependencies": { + "map-cache": "^0.2.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/from2": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/from2/-/from2-2.3.0.tgz", + "integrity": "sha1-i/tVAr3kpNNs/e6gB/zKIdfjgq8=", + "dependencies": { + "inherits": "^2.0.1", + "readable-stream": "^2.0.0" + } + }, + "node_modules/from2-string": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/from2-string/-/from2-string-1.1.0.tgz", + "integrity": "sha512-m8vCh+KnXXXBtfF2VUbiYlQ+nczLcntB0BrtNgpmLkHylhObe9WF1b2LZjBBzrZzA6P4mkEla6ZYQoOUTG8cYA==", + "dependencies": { + "from2": "^2.0.3" + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true, + "optional": true + }, + "node_modules/fs-exists-sync": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/fs-exists-sync/-/fs-exists-sync-0.1.0.tgz", + "integrity": "sha512-cR/vflFyPZtrN6b38ZyWxpWdhlXrzZEBawlpBQMq7033xVY7/kg0GDMBK5jg8lDYQckdJ5x/YC88lM3C7VMsLg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs-monkey": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.0.5.tgz", + "integrity": "sha512-8uMbBjrhzW76TYgEV27Y5E//W2f/lTFmx78P2w19FZSxarhI/798APGQyuGCwmkNxgwGRhrLfvWyLBvNtuOmew==", + "dev": true + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=" + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/function.prototype.name": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/function.prototype.name/-/function.prototype.name-1.1.8.tgz", + "integrity": "sha512-e5iwyodOHhbMr/yNrc7fDYG4qlbIvI5gajyzPnb5TCwyhjApznQh1BMFou9b30SevY43gCJKXycoCBjMbsuW0Q==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "functions-have-names": "^1.2.3", + "hasown": "^2.0.2", + "is-callable": "^1.2.7" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/functions-have-names": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/functions-have-names/-/functions-have-names-1.2.3.tgz", + "integrity": "sha512-xckBUXyTIqT97tq2x2AMb+g163b5JFysYk0x4qxNFwbfQkmNZoiRHb6sPzI9/QV33WeuvVYBUIiD4NzNIyqaRQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaze": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/gaze/-/gaze-1.1.3.tgz", + "integrity": "sha512-BRdNm8hbWzFzWHERTrejLqwHDfS4GibPoq5wjTPIoJHoBtKGPg3xAFfxmM+9ztbXelxcf2hwQcaz1PtmFeue8g==", + "dev": true, + "dependencies": { + "globule": "^1.0.0" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-package-type": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/get-package-type/-/get-package-type-0.1.0.tgz", + "integrity": "sha512-pjzuKtY64GYfWizNAJ0fr9VqttZkNiK2iS430LtIHzjBEr6bX8Am2zm4sW4Ro5wjWW5cAlRL1qAMTcXbjNAO2Q==", + "dev": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/get-port": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/get-port/-/get-port-5.1.1.tgz", + "integrity": "sha512-g/Q1aTSDOxFpchXC4i8ZWvxA1lnPqx/JHqcpIw0/LX9T8x/GBbi6YnlN5nhaKIFkT8oFsscUKgDJYxfwfS6QsQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-proxy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/get-proxy/-/get-proxy-2.1.0.tgz", + "integrity": "sha512-zmZIaQTWnNQb4R4fJUEp/FC51eZsc6EkErspy3xtIYStaq8EB/hDIWipxsal+E8rz0qD7f2sL/NA9Xee4RInJw==", + "dev": true, + "optional": true, + "dependencies": { + "npm-conf": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/get-size": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/get-size/-/get-size-2.0.3.tgz", + "integrity": "sha512-lXNzT/h/dTjTxRbm9BXb+SGxxzkm97h/PCIKtlN/CBCxxmkkIVV21udumMS93MuVTDX583gqc94v3RjuHmI+2Q==" + }, + "node_modules/get-stdin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", + "integrity": "sha512-F5aQMywwJ2n85s4hJPTT9RPxGmubonuB10MNYo17/xph174n2MIR33HRguhzVag10O/npM7SPk73LMZNP+FaWw==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/get-stream": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-4.1.0.tgz", + "integrity": "sha512-GMat4EJ5161kIy2HevLlr4luNjBgvmj413KaQA7jt4V8B4RDsfpHk7WQ9GVqfYyyx8OS/L66Kox+rJRNklLK7w==", + "dev": true, + "optional": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/get-symbol-description": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/get-symbol-description/-/get-symbol-description-1.1.0.tgz", + "integrity": "sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.6.tgz", + "integrity": "sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.1.tgz", + "integrity": "sha512-7ZqONUVqaabogsYNWlYj0t3YZaL6dhuEueZXGF+/YVmf6dHmaFg8/6psJKqhx9QykIDKzpGcy2cn4oV4YC7V/Q==", + "dev": true, + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^5.0.1", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-value": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/get-value/-/get-value-2.0.6.tgz", + "integrity": "sha1-3BXKHGcjh8p2vTesCjlbogQqLCg=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/getobject": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/getobject/-/getobject-1.0.0.tgz", + "integrity": "sha512-tbUz6AKKKr2YiMB+fLWIgq5ZeBOobop9YMMAU9dC54/ot2ksMXt3DOFyBuhZw6ptcVszEykgByK20j7W9jHFag==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/getpass": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", + "integrity": "sha1-Xv+OPmhNVprkyysSgmBOi6YhSfo=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0" + } + }, + "node_modules/gifsicle": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/gifsicle/-/gifsicle-4.0.1.tgz", + "integrity": "sha512-A/kiCLfDdV+ERV/UB+2O41mifd+RxH8jlRG8DMxZO84Bma/Fw0htqZ+hY2iaalLRNyUu7tYZQslqUBJxBggxbg==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "execa": "^1.0.0", + "logalot": "^2.0.0" + }, + "bin": { + "gifsicle": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/glob": { + "version": "7.1.7", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.7.tgz", + "integrity": "sha512-OvD9ENzPLbegENnYP5UUfJIirTg4+XwMWGaQfQTY0JenxNvvIKP3U3/tAQSPIu/lHxXYSZmpXlUHeqAIdKzBLQ==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.3.0.tgz", + "integrity": "sha512-Iozmtbqv0noj0uDDqoL0zNq0VBEfK2YFoMAZoxJe4cwphvLR+JskfF30QhXHOR4m3KrE6NLRYw+U9MRXvifyig==", + "dev": true, + "license": "BSD" + }, + "node_modules/global-modules": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-0.2.3.tgz", + "integrity": "sha512-JeXuCbvYzYXcwE6acL9V2bAOeSIGl4dD+iwLY9iUx2VBJJ80R18HCn+JCwHM9Oegdfya3lEkGCdaRkSyc10hDA==", + "dev": true, + "dependencies": { + "global-prefix": "^0.1.4", + "is-windows": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-modules/node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-0.1.5.tgz", + "integrity": "sha512-gOPiyxcD9dJGCEArAhF4Hd0BAqvAe/JzERP7tYumE4yIkmIedPUVXcJFWbV3/p/ovIIvKjkrTk+f1UVkq7vvbw==", + "dev": true, + "dependencies": { + "homedir-polyfill": "^1.0.0", + "ini": "^1.3.4", + "is-windows": "^0.2.0", + "which": "^1.2.12" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/is-windows": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-0.2.0.tgz", + "integrity": "sha512-n67eJYmXbniZB7RF4I/FTjK1s6RPOCTxhYrVYLRaCt3lF0mpWZPKr3T2LSZAqyjQsxR2qMmGYXXzK0YWwcPM1Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/global-prefix/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/globalthis": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/globalthis/-/globalthis-1.0.4.tgz", + "integrity": "sha512-DpLKbNU4WylpxJykQujfCcwYWiV/Jhm50Goo0wrVILAv5jOr9d+H+UR3PhSCD2rCCEIg0uc+G+muBTwD54JhDQ==", + "dev": true, + "dependencies": { + "define-properties": "^1.2.1", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/globalyzer": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/globalyzer/-/globalyzer-0.1.0.tgz", + "integrity": "sha512-40oNTM9UfG6aBmuKxk/giHn5nQ8RVz/SS4Ir6zgzOv9/qC3kKZ9v4etGTcJbEl/NyVQH7FGU7d+X1egr57Md2Q==", + "dev": true + }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/globjoin": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/globjoin/-/globjoin-0.1.4.tgz", + "integrity": "sha512-xYfnw62CKG8nLkZBfWbhWwDw02CHty86jfPcc2cr3ZfeuK9ysoVPPEUxf21bAD/rWAgk52SuBrLJlefNy8mvFg==", + "dev": true, + "license": "MIT" + }, + "node_modules/globrex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/globrex/-/globrex-0.1.2.tgz", + "integrity": "sha512-uHJgbwAMwNFf5mLst7IWLNg14x1CkeqglJb/K3doi4dw6q2IvAAmM/Y81kevy83wP+Sst+nutFTYOGg3d1lsxg==", + "dev": true + }, + "node_modules/globule": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/globule/-/globule-1.3.4.tgz", + "integrity": "sha512-OPTIfhMBh7JbBYDpa5b+Q5ptmMWKwcNcFSR/0c6t8V4f3ZAVBEsKNY37QdVqmLRYSMhOUGYrY0QhSoEpzGr/Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob": "~7.1.1", + "lodash": "^4.17.21", + "minimatch": "~3.0.2" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/globule/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/good-listener": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", + "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", + "dependencies": { + "delegate": "^3.1.2" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/got": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/got/-/got-7.1.0.tgz", + "integrity": "sha512-Y5WMo7xKKq1muPsxD+KmrR8DH5auG7fBdDVueZwETwV6VytKyU9OX/ddpq2/1hp1vIPvVb4T81dKQz3BivkNLw==", + "dev": true, + "optional": true, + "dependencies": { + "decompress-response": "^3.2.0", + "duplexer3": "^0.1.4", + "get-stream": "^3.0.0", + "is-plain-obj": "^1.1.0", + "is-retry-allowed": "^1.0.0", + "is-stream": "^1.0.0", + "isurl": "^1.0.0-alpha5", + "lowercase-keys": "^1.0.0", + "p-cancelable": "^0.3.0", + "p-timeout": "^1.1.1", + "safe-buffer": "^5.0.1", + "timed-out": "^4.0.0", + "url-parse-lax": "^1.0.0", + "url-to-options": "^1.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/got/node_modules/get-stream": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-3.0.0.tgz", + "integrity": "sha512-GlhdIUuVakc8SJ6kK0zAFbiGzRFzNnY4jUuEbV9UROo4Y+0Ny4fjvcZFVTeDA4odpFyOQzaw6hXukJSq/f28sQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" + }, + "node_modules/graphemer": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/graphemer/-/graphemer-1.4.0.tgz", + "integrity": "sha512-EtKwoO6kxCL9WO5xipiHTZlSzBm7WLT627TqC/uVRd0HKmq8NXyebnNYxDoBi7wt8eTWrUrKXCOVaFq9x1kgag==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/grunt/-/grunt-1.6.1.tgz", + "integrity": "sha512-/ABUy3gYWu5iBmrUSRBP97JLpQUm0GgVveDCp6t3yRNIoltIYw7rEj3g5y1o2PGPR2vfTRGa7WC/LZHLTXnEzA==", + "dev": true, + "dependencies": { + "dateformat": "~4.6.2", + "eventemitter2": "~0.4.13", + "exit": "~0.1.2", + "findup-sync": "~5.0.0", + "glob": "~7.1.6", + "grunt-cli": "~1.4.3", + "grunt-known-options": "~2.0.0", + "grunt-legacy-log": "~3.0.0", + "grunt-legacy-util": "~2.0.1", + "iconv-lite": "~0.6.3", + "js-yaml": "~3.14.0", + "minimatch": "~3.0.4", + "nopt": "~3.0.6" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/grunt-banner": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/grunt-banner/-/grunt-banner-0.6.0.tgz", + "integrity": "sha1-P4eQIdEj+linuloLb7a+QStYhaw=", + "dev": true, + "dependencies": { + "chalk": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + }, + "peerDependencies": { + "grunt": ">=0.4.0" + } + }, + "node_modules/grunt-banner/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-banner/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-banner/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-banner/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-banner/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-contrib-clean": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-clean/-/grunt-contrib-clean-2.0.1.tgz", + "integrity": "sha512-uRvnXfhiZt8akb/ZRDHJpQQtkkVkqc/opWO4Po/9ehC2hPxgptB9S6JHDC/Nxswo4CJSM0iFPT/Iym3cEMWzKA==", + "dev": true, + "dependencies": { + "async": "^3.2.3", + "rimraf": "^2.6.2" + }, + "engines": { + "node": ">=12" + }, + "peerDependencies": { + "grunt": ">=0.4.5" + } + }, + "node_modules/grunt-contrib-clean/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-contrib-concat": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-concat/-/grunt-contrib-concat-2.1.0.tgz", + "integrity": "sha512-Vnl95JIOxfhEN7bnYIlCgQz41kkbi7tsZ/9a4usZmxNxi1S2YAIOy8ysFmO8u4MN26Apal1O106BwARdaNxXQw==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "source-map": "^0.5.3" + }, + "engines": { + "node": ">=0.12.0" + }, + "peerDependencies": { + "grunt": ">=1.4.1" + } + }, + "node_modules/grunt-contrib-concat/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/grunt-contrib-concat/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/grunt-contrib-concat/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/grunt-contrib-concat/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/grunt-contrib-concat/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-concat/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-copy": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-copy/-/grunt-contrib-copy-1.0.0.tgz", + "integrity": "sha1-cGDGWB6QS4qw0A8HbgqPbj58NXM=", + "dev": true, + "dependencies": { + "chalk": "^1.1.1", + "file-sync-cmp": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-copy/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-copy/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-copy/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-copy/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-contrib-copy/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-contrib-cssmin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-cssmin/-/grunt-contrib-cssmin-5.0.0.tgz", + "integrity": "sha512-SNp4H4+85mm2xaHYi83FBHuOXylpi5vcwgtNoYCZBbkgeXQXoeTAKa59VODRb0woTDBvxouP91Ff5PzCkikg6g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "clean-css": "^5.3.2", + "maxmin": "^3.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/grunt-contrib-cssmin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/grunt-contrib-cssmin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/grunt-contrib-cssmin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/grunt-contrib-cssmin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/grunt-contrib-cssmin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-cssmin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-imagemin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-imagemin/-/grunt-contrib-imagemin-4.0.0.tgz", + "integrity": "sha512-2GYQBQFfJLjeTThJ8E7+vLgvgfOh78u0bgieIK85c2Rv9V6ssd2AvBvuF7T26mK261EN/SlNefpW5+zGWzfrVw==", + "dev": true, + "dependencies": { + "chalk": "^2.4.1", + "imagemin": "^6.0.0", + "p-map": "^1.2.0", + "plur": "^3.0.1", + "pretty-bytes": "^5.1.0" + }, + "engines": { + "node": ">=8" + }, + "optionalDependencies": { + "imagemin-gifsicle": "^6.0.1", + "imagemin-jpegtran": "^6.0.0", + "imagemin-optipng": "^6.0.0", + "imagemin-svgo": "^7.0.0" + } + }, + "node_modules/grunt-contrib-imagemin/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-imagemin/node_modules/irregular-plurals": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-2.0.0.tgz", + "integrity": "sha512-Y75zBYLkh0lJ9qxeHlMjQ7bSbyiSqNW/UOPWDmzC7cXskL1hekSITh1Oc6JV0XCWWZ9DE8VYSB71xocLk3gmGw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/grunt-contrib-imagemin/node_modules/p-map": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-1.2.0.tgz", + "integrity": "sha512-r6zKACMNhjPJMTl8KcFH4li//gkrXWfbD6feV8l6doRHlzljFWGJ2AP6iKaCJXyZmAUMOPtvbW7EXkbWO/pLEA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/grunt-contrib-imagemin/node_modules/plur": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/plur/-/plur-3.1.1.tgz", + "integrity": "sha512-t1Ax8KUvV3FFII8ltczPn2tJdjqbd1sIzu6t4JL7nQ3EyeL/lTrj5PWKb06ic5/6XYDr65rQ4uzQEGN70/6X5w==", + "dev": true, + "dependencies": { + "irregular-plurals": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/grunt-contrib-jshint": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-jshint/-/grunt-contrib-jshint-3.2.0.tgz", + "integrity": "sha512-pcXWCSZWfoMSvcV4BwH21TUtLtcX0Ms8IGuOPIcLeXK3fud9KclY7iqMKY94jFx8TxZzh028YYtpR+io8DiEaQ==", + "dev": true, + "dependencies": { + "chalk": "~4.1.2", + "hooker": "^0.2.3", + "jshint": "~2.13.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-contrib-jshint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/grunt-contrib-jshint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/grunt-contrib-jshint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/grunt-contrib-jshint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/grunt-contrib-jshint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-jshint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-qunit": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/grunt-contrib-qunit/-/grunt-contrib-qunit-10.1.1.tgz", + "integrity": "sha512-qSzY/aWl4xn8dQc2eAwKrXNB0171WHgb4aA3ZdKkN88csxS3tCD3Eh8ljfsscFAKIKZkhjierRgQypep/aV4NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter2": "^6.4.9", + "puppeteer": "^22.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/grunt-contrib-qunit/node_modules/eventemitter2": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/eventemitter2/-/eventemitter2-6.4.9.tgz", + "integrity": "sha512-JEPTiaOt9f04oa6NOkc4aH+nVp5I3wEjpHbIPqfgCdD5v5bUzy7xQqwcVO2aDQgOWhI28da57HksMrzK9HlRxg==", + "dev": true + }, + "node_modules/grunt-contrib-uglify": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/grunt-contrib-uglify/-/grunt-contrib-uglify-5.2.2.tgz", + "integrity": "sha512-ITxiWxrjjP+RZu/aJ5GLvdele+sxlznh+6fK9Qckio5ma8f7Iv8woZjRkGfafvpuygxNefOJNc+hfjjBayRn2Q==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "maxmin": "^3.0.0", + "uglify-js": "^3.16.1", + "uri-path": "^1.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/grunt-contrib-uglify/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/grunt-contrib-uglify/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/grunt-contrib-uglify/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/grunt-contrib-uglify/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/grunt-contrib-uglify/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-uglify/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-contrib-watch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-contrib-watch/-/grunt-contrib-watch-1.1.0.tgz", + "integrity": "sha512-yGweN+0DW5yM+oo58fRu/XIRrPcn3r4tQx+nL7eMRwjpvk+rQY6R8o94BPK0i2UhTg9FN21hS+m8vR8v9vXfeg==", + "dev": true, + "dependencies": { + "async": "^2.6.0", + "gaze": "^1.1.0", + "lodash": "^4.17.10", + "tiny-lr": "^1.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-file-append": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/grunt-file-append/-/grunt-file-append-0.0.7.tgz", + "integrity": "sha1-P376M2lvoFdwsoCU9EUIyvxdLto=", + "dev": true, + "engines": { + "node": ">= 0.8.0" + }, + "peerDependencies": { + "grunt": ">0.4.1" + } + }, + "node_modules/grunt-jsdoc": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/grunt-jsdoc/-/grunt-jsdoc-2.4.1.tgz", + "integrity": "sha512-S0zxU0wDewRu7z+vijEItOWe/UttxWVmvz0qz2ZVcAYR2GpXjsiski2CAVN0b18t2qeVLdmxZkJaEWCOsKzcAw==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.1", + "jsdoc": "^3.6.3" + }, + "bin": { + "grunt-jsdoc": "bin/grunt-jsdoc" + }, + "engines": { + "node": ">= 8.12.0" + } + }, + "node_modules/grunt-jsdoc/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/grunt-jsdoc/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-jsdoc/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-jsdoc/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-known-options": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/grunt-known-options/-/grunt-known-options-2.0.0.tgz", + "integrity": "sha512-GD7cTz0I4SAede1/+pAbmJRG44zFLPipVtdL9o3vqx9IEyb7b4/Y3s7r6ofI3CchR5GvYJ+8buCSioDv5dQLiA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-legacy-log": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log/-/grunt-legacy-log-3.0.0.tgz", + "integrity": "sha512-GHZQzZmhyq0u3hr7aHW4qUH0xDzwp2YXldLPZTCjlOeGscAOWWPftZG3XioW8MasGp+OBRIu39LFx14SLjXRcA==", + "dev": true, + "dependencies": { + "colors": "~1.1.2", + "grunt-legacy-log-utils": "~2.1.0", + "hooker": "~0.2.3", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/grunt-legacy-log-utils": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/grunt-legacy-log-utils/-/grunt-legacy-log-utils-2.1.0.tgz", + "integrity": "sha512-lwquaPXJtKQk0rUM1IQAop5noEpwFqOXasVoedLeNzaibf/OPWjKYvvdqnEHNmU+0T0CaReAXIbGo747ZD+Aaw==", + "dev": true, + "dependencies": { + "chalk": "~4.1.0", + "lodash": "~4.17.19" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/grunt-legacy-log-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-legacy-log-utils/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-legacy-log-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-legacy-log/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-legacy-util": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/grunt-legacy-util/-/grunt-legacy-util-2.0.1.tgz", + "integrity": "sha512-2bQiD4fzXqX8rhNdXkAywCadeqiPiay0oQny77wA2F3WF4grPJXCvAcyoWUJV+po/b15glGkxuSiQCK299UC2w==", + "dev": true, + "dependencies": { + "async": "~3.2.0", + "exit": "~0.1.2", + "getobject": "~1.0.0", + "hooker": "~0.2.3", + "lodash": "~4.17.21", + "underscore.string": "~3.3.5", + "which": "~2.0.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt-legacy-util/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-legacy-util/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/grunt-patch-wordpress": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/grunt-patch-wordpress/-/grunt-patch-wordpress-4.0.0.tgz", + "integrity": "sha512-SA2FMfnNU6vAaSKpUcuF8O9d1LTpvoGHMU0d1OTEOwAfchWn8z6GwKgQeAC00ONvhjWXsZ3/g4gVTG1QbdqJqQ==", + "dev": true, + "dependencies": { + "grunt": "^1.0.3", + "inquirer": "^5.1.0", + "request": "^2.83.0", + "xmlrpc": "^1.3.2" + }, + "engines": { + "node": ">=20.10.0", + "npm": ">=10.2.3" + } + }, + "node_modules/grunt-replace-lts": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/grunt-replace-lts/-/grunt-replace-lts-1.1.0.tgz", + "integrity": "sha512-YCLFHDM7/mEb+7tzdstb756ZYEUTSiyiEj5XhfLIxmVrDKShXQ8STD9f0s7HZXwwHwxFgPr4zELSP7J3kYra7w==", + "dev": true, + "dependencies": { + "chalk": "^1.1.0", + "file-sync-cmp": "^0.1.0", + "lodash": "^4.17.15", + "next-applause": "^2.2.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-replace-lts/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-replace-lts/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-replace-lts/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-replace-lts/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-replace-lts/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-rtlcss": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/grunt-rtlcss/-/grunt-rtlcss-2.0.2.tgz", + "integrity": "sha512-WbI2thnwlF04N+xvJu+NxqEaCyPuLyar196SYhEQFZ2EJHkOS8YYE+Zkh+X9cWhwAtKp7ZEpR/IKXcyQggOIsQ==", + "dev": true, + "dependencies": { + "chalk": "^1.0.0", + "rtlcss": "^2.0.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/grunt-rtlcss/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-rtlcss/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-rtlcss/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-rtlcss/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-rtlcss/node_modules/supports-color": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/grunt-sass": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/grunt-sass/-/grunt-sass-4.1.0.tgz", + "integrity": "sha512-4RsrEDGn4C/UpiTFYxO7so2WUUXQjokZWsZoKihFKGFQYR+zj4fop7Pz8c2aX1HPk0u2DwtsQDjucW61vbSZew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + }, + "peerDependencies": { + "grunt": ">=1" + } + }, + "node_modules/grunt-webpack": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/grunt-webpack/-/grunt-webpack-7.0.1.tgz", + "integrity": "sha512-3Mo0UfZE7v5CnSCIkN6tGMlKDOCr0okvk8SQrL9X/6JhrE2sQlGwFEI5Q6j89rZ9DTl9jEjkIev/hfR0x85wKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-for-each": "^3.0.0", + "webpack-merge": "^6.0.1" + }, + "engines": { + "node": ">=18.19.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/grunt-webpack/node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/grunt-webpack/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-webpack/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/grunt-webpack/node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/grunt-webpack/node_modules/webpack-merge": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", + "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/grunt/node_modules/grunt-cli": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/grunt-cli/-/grunt-cli-1.4.3.tgz", + "integrity": "sha512-9Dtx/AhVeB4LYzsViCjUQkd0Kw0McN2gYpdmGYKtE2a5Yt7v1Q+HYZVWhqXc/kGnxlMtqKDxSwotiGeFmkrCoQ==", + "dev": true, + "dependencies": { + "grunt-known-options": "~2.0.0", + "interpret": "~1.1.0", + "liftup": "~3.0.1", + "nopt": "~4.0.1", + "v8flags": "~3.2.0" + }, + "bin": { + "grunt": "bin/grunt" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/grunt/node_modules/grunt-cli/node_modules/nopt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-4.0.3.tgz", + "integrity": "sha512-CvaGwVMztSMJLOeXPrez7fyfObdZqNUK1cPAEzLHrTybIua9pMdmmPR5YwtfNftIOMv3DPUhFaxsZMNTQO20Kg==", + "dev": true, + "dependencies": { + "abbrev": "1", + "osenv": "^0.1.4" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/grunt/node_modules/interpret": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.1.0.tgz", + "integrity": "sha512-CLM8SNMDu7C5psFCn6Wg/tgpj/bKAg7hc2gWqcuR9OD5Ft9PhBpIu8PLicPeis+xDd6YX2ncI8MCA64I9tftIA==", + "dev": true + }, + "node_modules/grunt/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/gzip-size": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", + "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/handle-thing": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", + "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", + "dev": true + }, + "node_modules/har-schema": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", + "integrity": "sha1-qUwiJOvKwEeCoNkDVSHyRzW37JI=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/har-validator": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", + "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", + "deprecated": "this library is no longer supported", + "dev": true, + "dependencies": { + "ajv": "^6.12.3", + "har-schema": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/hard-rejection": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hard-rejection/-/hard-rejection-2.1.0.tgz", + "integrity": "sha512-VIZB+ibDhx7ObhAe7OVtoEbuP4h/MuOTHJ+J8h/eBXotJYl0fBgR72xDFCKgIh22OJZIOVNxBMWuhAr10r8HdA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/has-ansi": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", + "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", + "dev": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-ansi/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-bigints": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", + "integrity": "sha512-R3pbpkcIqv2Pm3dUwgjclDRVmWpTJW2DcMzcIhEXEx1oh/CEMObMm3KLmRJOdvhM7o4uQBnwr8pzRK2sJWIqfg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-color": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/has-color/-/has-color-0.1.7.tgz", + "integrity": "sha512-kaNz5OTAYYmt646Hkqw50/qyxP2vFnTVu5AQ1Zmk22Kk5+4Qx6BpO8+u7IKsML5fOsFk0ZT0AcCJNYwcvaLBvw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dev": true, + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.2.0.tgz", + "integrity": "sha512-KIL7eQPfHQRC8+XluaIw7BHUwwqL19bQn4hzNgdr+1wXoU0KKj6rufu47lhY7KbJR2C6T6+PfyN0Ea7wkSS+qQ==", + "dev": true, + "dependencies": { + "dunder-proto": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbol-support-x": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/has-symbol-support-x/-/has-symbol-support-x-1.4.2.tgz", + "integrity": "sha512-3ToOva++HaW+eCpgqZrCfN51IPB+7bJNVT6CUATzueB5Heb8o6Nam0V3HG5dlDvZU1Gn5QLcbahiKw/XVk5JJw==", + "dev": true, + "optional": true, + "engines": { + "node": "*" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-to-string-tag-x": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/has-to-string-tag-x/-/has-to-string-tag-x-1.4.1.tgz", + "integrity": "sha512-vdbKfmw+3LoOYVr+mtxHaX5a96+0f3DljYd8JOqvOLsf5mw2Otda2qCDT9qRqLAhrjyQ0h7ual5nOiASpsGNFw==", + "dev": true, + "optional": true, + "dependencies": { + "has-symbol-support-x": "^1.4.1" + }, + "engines": { + "node": "*" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-value": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-1.0.0.tgz", + "integrity": "sha512-IBXk4GTsLYdQ7Rvt+GRBrFSVEkmuOUy4re0Xjd9kJSUQpnTrWR4/y9RpfexN9vkAPMFuQoeWKwqzPozRTlasGw==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.6", + "has-values": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-1.0.0.tgz", + "integrity": "sha512-ODYZC64uqzmtfGMEAX/FvZiRyWLpAC3vYnNunURUnkGVTS+mI0smVsWaPydRBsE3g+ok7h960jChO8mFcWlHaQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^3.0.0", + "kind-of": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/has-values/node_modules/kind-of": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-4.0.0.tgz", + "integrity": "sha512-24XsCxmEbRwEDbz/qz3stgin8TTzZ1ESR56OMCN0ujYg+vRutNSiOj9bHH9u85DKgXguraugV5sFuvbD4FW/hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hashery": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.0.tgz", + "integrity": "sha512-nhQ6ExaOIqti2FDWoEMWARUqIKyjr2VcZzXShrI+A3zpeiuPWzx6iPftt44LhP74E5sW36B75N6VHbvRtpvO6Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.14.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/header-case": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/header-case/-/header-case-2.0.4.tgz", + "integrity": "sha512-H/vuk5TEEVZwrR0lp2zed9OCo1uAILMlx0JEMgC26rzyJJ3N1v6XkwHHXJQdR2doSjcGPM6OKPYoJgf0plJ11Q==", + "dev": true, + "dependencies": { + "capital-case": "^1.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/homedir-polyfill": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/homedir-polyfill/-/homedir-polyfill-1.0.3.tgz", + "integrity": "sha512-eSmmWE5bZTK2Nou4g0AI3zZ9rswp7GRKoKXS1BLUkvPviOqs4YTN1djQIqrXy9k5gEtdLPy86JjRwsNM9tnDcA==", + "dev": true, + "dependencies": { + "parse-passwd": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/hooker": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/hooker/-/hooker-0.2.3.tgz", + "integrity": "sha1-uDT3I8xKJCqmWWNFnfbZhMXT2Vk=", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, + "node_modules/hosted-git-info": { + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", + "dev": true + }, + "node_modules/hoverintent": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/hoverintent/-/hoverintent-2.2.1.tgz", + "integrity": "sha512-VyU54L1xW5rSqpsv/LJ6ecymGXsXXeGs9iVEKot4kKBCq5UodSAuy3DqX686LZxEpaMEfeCHPu4LndsMX5Q9eQ==" + }, + "node_modules/hpack.js": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", + "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", + "dev": true, + "dependencies": { + "inherits": "^2.0.1", + "obuf": "^1.0.0", + "readable-stream": "^2.0.1", + "wbuf": "^1.1.0" + } + }, + "node_modules/html-encoding-sniffer": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/html-encoding-sniffer/-/html-encoding-sniffer-4.0.0.tgz", + "integrity": "sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "whatwg-encoding": "^3.1.1" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/html-escaper": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", + "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", + "dev": true + }, + "node_modules/html-tags": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", + "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/htmlhint": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/htmlhint/-/htmlhint-1.8.0.tgz", + "integrity": "sha512-RT1UsSM3ldlVQ7DDqWnbbRY1Rf6wwudmdYwiJzIyZVapA0jcka5r2lE2RkMLzTDN5c8Vc06yis57TaTpZ6o3Dg==", + "license": "MIT", + "dependencies": { + "async": "3.2.6", + "chalk": "4.1.2", + "commander": "11.1.0", + "glob": "^9.0.0", + "is-glob": "^4.0.3", + "node-sarif-builder": "^3.3.1", + "strip-json-comments": "3.1.1", + "xml": "1.0.1" + }, + "bin": { + "htmlhint": "bin/htmlhint" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "Open Collective", + "url": "https://opencollective.com/htmlhint" + } + }, + "node_modules/htmlhint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/htmlhint/node_modules/async": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/async/-/async-3.2.6.tgz", + "integrity": "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==", + "license": "MIT" + }, + "node_modules/htmlhint/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/htmlhint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/htmlhint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/htmlhint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "license": "MIT" + }, + "node_modules/htmlhint/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/htmlhint/node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "license": "ISC", + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/htmlhint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlhint/node_modules/minimatch": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/htmlhint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/htmlparser2": { + "version": "3.8.3", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.8.3.tgz", + "integrity": "sha512-hBxEg3CYXe+rPIua8ETe7tmG3XDn9B0edOE/e9wH2nLczxzgdu0m0aNHY+5wFZiviLWLdANPJTssa92dMcXQ5Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "domelementtype": "1", + "domhandler": "2.3", + "domutils": "1.5", + "entities": "1.0", + "readable-stream": "1.1" + } + }, + "node_modules/htmlparser2/node_modules/domhandler": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.3.0.tgz", + "integrity": "sha512-q9bUwjfp7Eif8jWxxxPSykdRZAb6GkguBGSgvvCrhI9wB71W2K/Kvv4E61CF/mcCfnVJDeDWx/Vb/uAqbDj6UQ==", + "dev": true, + "dependencies": { + "domelementtype": "1" + } + }, + "node_modules/htmlparser2/node_modules/domutils": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-1.5.1.tgz", + "integrity": "sha512-gSu5Oi/I+3wDENBsOWBiRK1eoGxcywYSqg3rR960/+EfY0CF4EX1VPkgHOZ3WiS/Jg2DtliF6BhWcHlfpYUcGw==", + "dev": true, + "dependencies": { + "dom-serializer": "0", + "domelementtype": "1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-1.0.0.tgz", + "integrity": "sha512-LbLqfXgJMmy81t+7c14mnulFHJ170cM6E+0vMXR9k/ZiZwgX8i5pNgjTCX3SO4VeUsFLV+8InixoretwU+MjBQ==", + "dev": true, + "license": "BSD-like" + }, + "node_modules/htmlparser2/node_modules/isarray": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", + "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/htmlparser2/node_modules/readable-stream": { + "version": "1.1.14", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-1.1.14.tgz", + "integrity": "sha512-+MeVjFf4L44XUkhM1eYbD8fyEsxcV81pqMSR5gblfcLCHfZvbrqy4/qYHE+/R5HoBUT11WV5O08Cr1n3YXkWVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.1", + "isarray": "0.0.1", + "string_decoder": "~0.10.x" + } + }, + "node_modules/htmlparser2/node_modules/string_decoder": { + "version": "0.10.31", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz", + "integrity": "sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/http-cache-semantics": { + "version": "3.8.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-3.8.1.tgz", + "integrity": "sha512-5ai2iksyV8ZXmnZhHH4rWPoxxistEexSi5936zIQ1bnNTW5VnA85B6P/VpXiRM017IgRvb2kKo1a//y+0wSp3w==", + "dev": true, + "optional": true + }, + "node_modules/http-deceiver": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", + "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", + "dev": true + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-link-header": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/http-link-header/-/http-link-header-1.1.3.tgz", + "integrity": "sha512-3cZ0SRL8fb9MUlU3mKM61FcQvPfXx2dBrZW3Vbg5CXa8jFlK8OaEpePenLe1oEXQduhz8b0QjsqfS59QP4AJDQ==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/http-parser-js": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.3.tgz", + "integrity": "sha512-t7hjvef/5HEK7RWTdUzVUhl8zkEu+LlaE0IYzdMuvbSDipxBRpOn4Uhw8ZyECEa808iVT8XCjzo6xmYt4CiLZg==", + "dev": true + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/http-proxy-middleware": { + "version": "2.0.9", + "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", + "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/http-proxy": "^1.17.8", + "http-proxy": "^1.18.1", + "is-glob": "^4.0.1", + "is-plain-obj": "^3.0.0", + "micromatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "@types/express": "^4.17.13" + }, + "peerDependenciesMeta": { + "@types/express": { + "optional": true + } + } + }, + "node_modules/http-proxy-middleware/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/http-proxy-middleware/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/http-proxy-middleware/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/http-signature": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", + "integrity": "sha1-muzZJRFHcvPZW2WmCruPfBj7rOE=", + "dev": true, + "dependencies": { + "assert-plus": "^1.0.0", + "jsprim": "^1.2.2", + "sshpk": "^1.7.0" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.3.7" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/human-signals": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", + "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", + "dev": true, + "engines": { + "node": ">=10.17.0" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/ignore": { + "version": "5.2.4", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.2.4.tgz", + "integrity": "sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/ignore-walk": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-4.0.1.tgz", + "integrity": "sha512-rzDQLaW4jQbh2YrOFlJdCtX8qgJTehFRYiUB2r1osqTeDzV/3+Jh8fz1oAPzUThf3iku8Ds4IDqawI5d8mUiQw==", + "dev": true, + "dependencies": { + "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/image-ssim": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/image-ssim/-/image-ssim-0.2.0.tgz", + "integrity": "sha512-W7+sO6/yhxy83L0G7xR8YAc5Z5QFtYEXXRV6EaE8tuYBZJnA3gVgp3q7X7muhLZVodeb9UfvjSbwt9VJwjIYAg==", + "dev": true + }, + "node_modules/imagemin": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/imagemin/-/imagemin-6.1.0.tgz", + "integrity": "sha512-8ryJBL1CN5uSHpiBMX0rJw79C9F9aJqMnjGnrd/1CafegpNuA81RBAAru/jQQEOWlOJJlpRnlcVFF6wq+Ist0A==", + "dev": true, + "dependencies": { + "file-type": "^10.7.0", + "globby": "^8.0.1", + "make-dir": "^1.0.0", + "p-pipe": "^1.1.0", + "pify": "^4.0.1", + "replace-ext": "^1.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-gifsicle": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/imagemin-gifsicle/-/imagemin-gifsicle-6.0.1.tgz", + "integrity": "sha512-kuu47c6iKDQ6R9J10xCwL0lgs0+sMz3LRHqRcJ2CRBWdcNmo3T5hUaM8hSZfksptZXJLGKk8heSAvwtSdB1Fng==", + "dev": true, + "optional": true, + "dependencies": { + "exec-buffer": "^3.0.0", + "gifsicle": "^4.0.0", + "is-gif": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-jpegtran": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/imagemin-jpegtran/-/imagemin-jpegtran-6.0.0.tgz", + "integrity": "sha512-Ih+NgThzqYfEWv9t58EItncaaXIHR0u9RuhKa8CtVBlMBvY0dCIxgQJQCfwImA4AV1PMfmUKlkyIHJjb7V4z1g==", + "dev": true, + "optional": true, + "dependencies": { + "exec-buffer": "^3.0.0", + "is-jpg": "^2.0.0", + "jpegtran-bin": "^4.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-optipng": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/imagemin-optipng/-/imagemin-optipng-6.0.0.tgz", + "integrity": "sha512-FoD2sMXvmoNm/zKPOWdhKpWdFdF9qiJmKC17MxZJPH42VMAp17/QENI/lIuP7LCUnLVAloO3AUoTSNzfhpyd8A==", + "dev": true, + "optional": true, + "dependencies": { + "exec-buffer": "^3.0.0", + "is-png": "^1.0.0", + "optipng-bin": "^5.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/imagemin-svgo": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/imagemin-svgo/-/imagemin-svgo-7.1.0.tgz", + "integrity": "sha512-0JlIZNWP0Luasn1HT82uB9nU9aa+vUj6kpT+MjPW11LbprXC+iC4HDwn1r4Q2/91qj4iy9tRZNsFySMlEpLdpg==", + "dev": true, + "optional": true, + "dependencies": { + "is-svg": "^4.2.1", + "svgo": "^1.3.2" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sindresorhus/imagemin-svgo?sponsor=1" + } + }, + "node_modules/imagemin/node_modules/@nodelib/fs.stat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-1.1.3.tgz", + "integrity": "sha512-shAmDyaQC4H92APFoIaVDHCx5bStIocgvbwQyxPRrbUY20V1EYTbSDchWbuwlMG3V17cprZhA6+78JfB+3DTPw==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/imagemin/node_modules/dir-glob": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-2.0.0.tgz", + "integrity": "sha512-37qirFDz8cA5fimp9feo43fSuRo2gHwaIn6dXL8Ber1dGwUosDrGZeCCXq57WnIqE4aQ+u3eQZzsk1yOzhdwag==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "path-type": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/fast-glob": { + "version": "2.2.7", + "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-2.2.7.tgz", + "integrity": "sha512-g1KuQwHOZAmOZMuBtHdxDtju+T2RT8jgCC9aANsbpdiDDTSnjgfuVsIBNKbUeJI3oKMRExcfNDtJl4OhbffMsw==", + "dev": true, + "dependencies": { + "@mrmlnc/readdir-enhanced": "^2.2.1", + "@nodelib/fs.stat": "^1.1.2", + "glob-parent": "^3.1.0", + "is-glob": "^4.0.0", + "merge2": "^1.2.3", + "micromatch": "^3.1.10" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/imagemin/node_modules/glob-parent": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", + "integrity": "sha512-E8Ak/2+dZY6fnzlR7+ueWvhsH1SjHr4jjss4YS/h4py44jY9MhK/VFdaZJAWDz6BbL21KeteKxFSFpq8OS5gVA==", + "dev": true, + "dependencies": { + "is-glob": "^3.1.0", + "path-dirname": "^1.0.0" + } + }, + "node_modules/imagemin/node_modules/glob-parent/node_modules/is-glob": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", + "integrity": "sha512-UFpDDrPgM6qpnFNI+rh/p3bUaq9hKLZN8bMUWzxmcnZVS3omf4IPK+BrewlnWjO1WmUsMYuSjKh4UJuV4+Lqmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imagemin/node_modules/globby": { + "version": "8.0.2", + "resolved": "https://registry.npmjs.org/globby/-/globby-8.0.2.tgz", + "integrity": "sha512-yTzMmKygLp8RUpG1Ymu2VXPSJQZjNAZPD4ywgYEaG7e4tBJeUQBO8OpXrf1RCNcEs5alsoJYPAMiIHP0cmeC7w==", + "dev": true, + "dependencies": { + "array-union": "^1.0.1", + "dir-glob": "2.0.0", + "fast-glob": "^2.0.2", + "glob": "^7.1.2", + "ignore": "^3.3.5", + "pify": "^3.0.0", + "slash": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/globby/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/ignore": { + "version": "3.3.10", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.10.tgz", + "integrity": "sha512-Pgs951kaMm5GXP7MOvxERINe3gsaVjUWFm+UZPSq9xYriQAksyhg0csnS0KXSNRD5NmNdapXEpjxG49+AKh/ug==", + "dev": true + }, + "node_modules/imagemin/node_modules/make-dir": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-1.3.0.tgz", + "integrity": "sha512-2w31R7SJtieJJnQtGc7RVL2StM2vGYVfqUOvUDxH6bC6aJTxPxTF0GnIgCyu7tjockiUWAYQRbxa7vKn34s5sQ==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/make-dir/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/path-type": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", + "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", + "dev": true, + "dependencies": { + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/path-type/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/imagemin/node_modules/slash": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-1.0.0.tgz", + "integrity": "sha512-3TYDR7xWt4dIqV2JauJr+EJeW356RXijHeUlO+8djJ+uBXPn8/2dpzBc8yQhh583sVvc9CvFAeQVgijsH+PNNg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/imagesloaded": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/imagesloaded/-/imagesloaded-5.0.0.tgz", + "integrity": "sha512-/0JGSubc1MTCoDKVmonLHgbifBWHdyLkun+R/151E1c5n79hiSxcd7cB7mPXFgojYu8xnRZv7GYxzKoxW8BetQ==", + "dependencies": { + "ev-emitter": "^2.1.2" + } + }, + "node_modules/imagesloaded/node_modules/ev-emitter": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ev-emitter/-/ev-emitter-2.1.2.tgz", + "integrity": "sha512-jQ5Ql18hdCQ4qS+RCrbLfz1n+Pags27q5TwMKvZyhp5hh2UULUYZUy1keqj6k6SYsdqIYjnmz7xyyEY0V67B8Q==" + }, + "node_modules/immediate": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/immediate/-/immediate-3.0.6.tgz", + "integrity": "sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==", + "dev": true + }, + "node_modules/immutable": { + "version": "5.1.5", + "resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.5.tgz", + "integrity": "sha512-t7xcm2siw+hlUM68I+UEOK+z84RzmN59as9DZ7P1l0994DKUWV7UXBMQZVxaoMSRQ+PBZbHCOoBt7a2wxOMt+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/import-fresh": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.0.tgz", + "integrity": "sha512-veYYhQa+D1QBKznvhUHxb8faxlrwUnxseDAbAp457E0wLNio2bOSKnjYDhMj+YiAq61xrMGhQk9iXVk5FzgQMw==", + "dev": true, + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-local": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.1.0.tgz", + "integrity": "sha512-ASB07uLtnDs1o6EHjKpX34BKYDSqnFerfTOJL2HvMqF70LnxpjkzDB8J44oT9pu4AMPkQwf8jl6szgvNd2tRIg==", + "dev": true, + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/indent-string": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-2.1.0.tgz", + "integrity": "sha512-aqwDFWSgSgfRaEwao5lg5KEcVd/2a+D1rvoG7NdilmYz0NwRk6StWpWdz/Hpk34MKPpx7s8XxUqimfcQK6gGlg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "repeating": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ini": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", + "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true + }, + "node_modules/inquirer": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-5.2.0.tgz", + "integrity": "sha512-E9BmnJbAKLPGonz0HeWHtbKf+EeSP93paWO3ZYoUpq/aowXvYGjjCSuashhXPpzbArIjBbji39THkxTz9ZeEUQ==", + "dev": true, + "dependencies": { + "ansi-escapes": "^3.0.0", + "chalk": "^2.0.0", + "cli-cursor": "^2.1.0", + "cli-width": "^2.0.0", + "external-editor": "^2.1.0", + "figures": "^2.0.0", + "lodash": "^4.3.0", + "mute-stream": "0.0.7", + "run-async": "^2.2.0", + "rxjs": "^5.5.2", + "string-width": "^2.1.0", + "strip-ansi": "^4.0.0", + "through": "^2.3.6" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/inquirer/node_modules/ansi-escapes": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-3.2.0.tgz", + "integrity": "sha512-cBhpre4ma+U0T1oM5fXg7Dy1Jw7zzwv7lt/GoCpr+hDQJoYnKVPLL4dCvSEFMmQurOQvSrwT7SL/DAlhBI97RQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/figures": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-2.0.0.tgz", + "integrity": "sha512-Oa2M9atig69ZkfwiApY8F2Yy+tzMbazyvqv21R0NsSC8floSOC09BbT1ITWAdoMGQvJ/aZnR1KMwdx9tvHnTNA==", + "dev": true, + "license": "MIT", + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/inquirer/node_modules/strip-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/install-changed": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/install-changed/-/install-changed-1.1.0.tgz", + "integrity": "sha512-APUWMdQnwGcyv9bmuvgxCcrR6qtD996+hofEEAPGSjsvGIZuBpBF0yrYAKOl9tmhm2AzxJC4EXUbxZ/SId4NIA==", + "dev": true, + "bin": { + "install-changed": "bin/install-changed.js" + } + }, + "node_modules/internal-slot": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/internal-slot/-/internal-slot-1.1.0.tgz", + "integrity": "sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "hasown": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/intl-messageformat": { + "version": "10.7.14", + "resolved": "https://registry.npmjs.org/intl-messageformat/-/intl-messageformat-10.7.14.tgz", + "integrity": "sha512-mMGnE4E1otdEutV5vLUdCxRJygHB5ozUBxsPB5qhitewssrS/qGruq9bmvIRkkGsNeK5ZWLfYRld18UHGTIifQ==", + "dev": true, + "dependencies": { + "@formatjs/ecma402-abstract": "2.3.2", + "@formatjs/fast-memoize": "2.2.6", + "@formatjs/icu-messageformat-parser": "2.11.0", + "tslib": "2" + } + }, + "node_modules/into-stream": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-3.1.0.tgz", + "integrity": "sha1-lvsKk2wSur1v8XUqF9BWFqvQlMY=", + "dev": true, + "optional": true, + "dependencies": { + "from2": "^2.1.1", + "p-is-promise": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, + "node_modules/ipaddr.js": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.3.0.tgz", + "integrity": "sha512-Zv/pA+ciVFbCSBBjGfaKUya/CcGmUHzTydLMaTwrUUEM2DIEO3iZvueGxmacvmN50fGpGVKeTXpb2LcYQxeVdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/irregular-plurals": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/irregular-plurals/-/irregular-plurals-3.5.0.tgz", + "integrity": "sha512-1ANGLZ+Nkv1ptFb2pa8oG8Lem4krflKuX/gINiHJHjJUKaJHk/SXk5x6K3J+39/p0h1RQ2saROclJJ+QLvETCQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-absolute": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-absolute/-/is-absolute-1.0.0.tgz", + "integrity": "sha512-dOWoqflvcydARa360Gvv18DZ/gRuHKi2NU/wU5X1ZFzdYfH29nkiNZsF3mp4OJ3H4yo9Mx8A/uAGNzpzPN3yBA==", + "dev": true, + "dependencies": { + "is-relative": "^1.0.0", + "is-windows": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-accessor-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.1.tgz", + "integrity": "sha512-YBUanLI8Yoihw923YeFUS5fs0fF2f5TSFTNiYAAzhhDscDa3lEqYuz1pDOEP5KvX94I9ey3vsqjJcLVFVU+3QA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-array-buffer": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", + "integrity": "sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha1-d8mYQFJ6qOyxqLppe4BkWnqSap0=", + "dev": true + }, + "node_modules/is-async-function": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.1.1.tgz", + "integrity": "sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==", + "dev": true, + "dependencies": { + "async-function": "^1.0.0", + "call-bound": "^1.0.3", + "get-proto": "^1.0.1", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-bigint": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-bigint/-/is-bigint-1.1.0.tgz", + "integrity": "sha512-n4ZT37wG78iz03xPRKJrHTdZbe3IicyucEtdRsV5yglwc3GyUfbAfpSeD0FJ41NbUNSt5wbhqfp1fS+BgnvDFQ==", + "dev": true, + "dependencies": { + "has-bigints": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-boolean-object": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.2.1.tgz", + "integrity": "sha512-l9qO6eFlUETHtuihLcYOaLKByJ1f+N4kthcU9YjHy3N+B3hWv0y/2Nd0mu/7lTFnRQHTrSdXF50HQ3bl5fEnng==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-buffer": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-1.1.6.tgz", + "integrity": "sha512-NcdALwpXkTm5Zvvbk7owOUSvVvBKDgKP5/ewfXEznmQFfs4ZRmanOeKBTjRVjka3QFoN6XJ+9F3USqfHqTaU5w==", + "dev": true + }, + "node_modules/is-builtin-module": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz", + "integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==", + "dev": true, + "license": "MIT", + "dependencies": { + "builtin-modules": "^3.3.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-bun-module": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-bun-module/-/is-bun-module-2.0.0.tgz", + "integrity": "sha512-gNCGbnnnnFAUGKeZ9PdbyeGYJqewpmc2aKHUEMO5nQPWU9lOmv7jcmQIv+qHD8fXW6W7qfuCwX4rY9LNRjXrkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "semver": "^7.7.1" + } + }, + "node_modules/is-callable": { + "version": "1.2.7", + "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.7.tgz", + "integrity": "sha512-1BC0BVFhS/p0qtw6enp8e+8OD0UrK0oFLztSjNzhcKA3WDuJxxAPXzPuPtKkjEY9UUoEWlX/8fgKeu2S8i9JTA==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-data-descriptor": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.1.tgz", + "integrity": "sha512-bc4NlCDiCr28U4aEsQ3Qs2491gVq4V8G7MQyws968ImqjKuYtTJXrl7Vq7jsN7Ly/C3xj5KWFrY7sHNeDkAzXw==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-data-view": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-data-view/-/is-data-view-1.0.2.tgz", + "integrity": "sha512-RKtWF8pGmS87i2D6gqQu/l7EYRlVdfzemCJN/P3UOs//x1QE7mfhvzHIApBTRf7axvT6DMGwSwBXYCT0nfB9xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "is-typed-array": "^1.1.13" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-date-object": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.1.0.tgz", + "integrity": "sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-descriptor": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-0.1.7.tgz", + "integrity": "sha512-C3grZTvObeN1xud4cRWl366OMXZTj0+HGyk4hvfpx4ZHt1Pb60ANSXqCK7pdOTeUQpRzECBSTphqvD7U+l22Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/is-docker": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", + "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", + "dev": true, + "bin": { + "is-docker": "cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha1-qIwCU1eR8C7TfHahueqXc8gz+MI=", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-finalizationregistry": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-finalizationregistry/-/is-finalizationregistry-1.1.1.tgz", + "integrity": "sha512-1pC6N8qWJbWoPtEjgcL2xyhQOP491EQjeUo3qTKcmV8YSDDJrOepfG8pcC7h/QgnQHYSv0mJ3Z/ZWxmatVrysg==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-finite": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-finite/-/is-finite-1.1.0.tgz", + "integrity": "sha512-cdyMtqX/BOqqNBBiKlIVkytNHm49MtMlYyn1zxzvJKWmFMlGzm+ry5BBfYyeY9YmNKbRSo/o7OX9w9ale0wg3w==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", + "integrity": "sha512-VHskAKYM8RfSFXwee5t5cbN5PZeq1Wrh6qd5bkyiXIf6UQcN6w/A0eXM9r6t8d+GYOh+o6ZhiEnb88LN/Y8m2w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/is-generator-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-generator-fn/-/is-generator-fn-2.1.0.tgz", + "integrity": "sha512-cTIB4yPYL/Grw0EaSzASzg6bBy9gqCofvWN8okThAYIxKJZC+udlRAmGbM0XLeniEJSs8uEgHPGuHSe1XsOLSQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-generator-function": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-generator-function/-/is-generator-function-1.1.0.tgz", + "integrity": "sha512-nPUB5km40q9e8UfN/Zc24eLlzdSf9OfKByBw9CIdw4H1giPMeA0OIJvbchsCu4npfI2QcMVBsGEBHKZ7wLTWmQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-proto": "^1.0.0", + "has-tostringtag": "^1.0.2", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-gif": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-gif/-/is-gif-3.0.0.tgz", + "integrity": "sha512-IqJ/jlbw5WJSNfwQ/lHEDXF8rxhRgF6ythk2oiEvhpG29F704eX9NO6TvPfMiq9DrbwgcEDnETYNcZDPewQoVw==", + "dev": true, + "optional": true, + "dependencies": { + "file-type": "^10.4.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-jpg": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-jpg/-/is-jpg-2.0.0.tgz", + "integrity": "sha1-LhmX+m6RZuqsAkLarkQ0A+TvHZc=", + "dev": true, + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/is-map": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-map/-/is-map-2.0.3.tgz", + "integrity": "sha512-1Qed0/Hr2m+YqxnM09CjA2d/i6YZNfF6R2oRAOj36eUdS6qIV/huPJNSEpKbupewFs+ZsJlxsjjPbc0/afW6Lw==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-natural-number": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/is-natural-number/-/is-natural-number-4.0.1.tgz", + "integrity": "sha1-q5124dtM7VHjXeDHLr7PCfc0zeg=", + "dev": true, + "optional": true + }, + "node_modules/is-negative-zero": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-negative-zero/-/is-negative-zero-2.0.3.tgz", + "integrity": "sha512-5KoIu2Ngpyek75jXodFvnafB6DJgr3u8uuK0LEZJjrU19DrMD3EVERaR8sjz8CCGgpZvxPl9SuE1GMVPFHx1mw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-3.0.0.tgz", + "integrity": "sha512-4cboCqIpliH+mAvFNegjZQ4kgKc3ZUhQVr3HvWbSh5q3WH2v82ct+T2Y1hdU5Gdtorx/cLifQjqCbL7bpznLTg==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number-object": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-number-object/-/is-number-object-1.1.1.tgz", + "integrity": "sha512-lZhclumE1G6VYD8VHe35wFaIif+CTy5SJIi5+3y4psDgWu4wPDoBhF8NxUOinEc7pHgiTsT6MaBb92rKhhD+Xw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", + "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-object": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-object/-/is-object-1.0.2.tgz", + "integrity": "sha512-2rRIahhZr2UWb45fIOuvZGpFtz0TyOZLf32KxBbSoUCeZR495zCKlWUKKUByk3geS2eAs7ZAABt0Y/Rx0GiQGA==", + "dev": true, + "optional": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-path-inside": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", + "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/is-plain-obj": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-1.1.0.tgz", + "integrity": "sha512-yvkRyxmFKEOQ4pNXCmJG5AEQNlXJS5LaONXo5/cLdTZdWvsZ1ioJEonLGAosKlMWE8lwUy/bJzMjcw8az73+Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-plain-object": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", + "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-png": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-png/-/is-png-1.1.0.tgz", + "integrity": "sha1-1XSxK/J1wDUEVVcLDltXqwYgd84=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-potential-custom-element-name": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-potential-custom-element-name/-/is-potential-custom-element-name-1.0.1.tgz", + "integrity": "sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-relative": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-relative/-/is-relative-1.0.0.tgz", + "integrity": "sha512-Kw/ReK0iqwKeu0MITLFuj0jbPAmEiOsIwyIXvvbfa6QfmN9pkD1M+8pdk7Rl/dTKbH34/XBFMbgD4iMJhLQbGA==", + "dev": true, + "dependencies": { + "is-unc-path": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-retry-allowed": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/is-retry-allowed/-/is-retry-allowed-1.2.0.tgz", + "integrity": "sha512-RUbUeKwvm3XG2VYamhJL1xFktgjvPzL0Hq8C+6yrWIswDy3BIXGqCxhxkc30N9jqK311gVU137K8Ei55/zVJRg==", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-set": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/is-set/-/is-set-2.0.3.tgz", + "integrity": "sha512-iPAjerrse27/ygGLxw+EBR9agv9Y6uLeYVJMu+QNCoouJ1/1ri0mGrcWpfCqFZuzzx3WjtwxG098X+n4OuRkPg==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-shared-array-buffer": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/is-shared-array-buffer/-/is-shared-array-buffer-1.0.4.tgz", + "integrity": "sha512-ISWac8drv4ZGfwKl5slpHG9OwPNty4jOWPRIhBpxOoD+hqITiwuipOQ2bNthAzwA3B4fIjO4Nln74N0S9byq8A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-stream": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", + "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-string": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-string/-/is-string-1.1.1.tgz", + "integrity": "sha512-BtEeSsoaQjlSPBemMQIrY1MY0uM6vnS1g5fmufYOtnxLGUZM2178PKbhsk7Ffv58IX+ZtcvoGwccYsh0PglkAA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-svg": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/is-svg/-/is-svg-4.4.0.tgz", + "integrity": "sha512-v+AgVwiK5DsGtT9ng+m4mClp6zDAmwrW8nZi6Gg15qzvBnRWWdfWA1TGaXyCDnWq5g5asofIgMVl3PjKxvk1ug==", + "dev": true, + "optional": true, + "dependencies": { + "fast-xml-parser": "^4.1.3" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-symbol": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.1.1.tgz", + "integrity": "sha512-9gGx6GTtCQM73BgmHQXfDmLtfjjTUDSyoxTCbp5WtoixAhfgsDirWIcVQ/IHpvI5Vgd5i/J5F7B9cN/WlVbC/w==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "has-symbols": "^1.1.0", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typed-array": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/is-typed-array/-/is-typed-array-1.1.15.tgz", + "integrity": "sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==", + "dev": true, + "dependencies": { + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-typedarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", + "integrity": "sha1-5HnICFjfDBsR3dppQPlgEfzaSpo=", + "dev": true + }, + "node_modules/is-unc-path": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/is-unc-path/-/is-unc-path-1.0.0.tgz", + "integrity": "sha512-mrGpVd0fs7WWLfVsStvgF6iEJnbjDFZh9/emhRDcGWTduTfNHd9CHeUwH3gYIjdbwo4On6hunkztwOaAw0yllQ==", + "dev": true, + "dependencies": { + "unc-path-regex": "^0.1.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/is-utf8": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", + "integrity": "sha1-Sw2hRCEE0bM2NA6AeX6GXPOffXI=", + "dev": true, + "optional": true + }, + "node_modules/is-weakmap": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/is-weakmap/-/is-weakmap-2.0.2.tgz", + "integrity": "sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakref": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/is-weakref/-/is-weakref-1.1.1.tgz", + "integrity": "sha512-6i9mGWSlqzNMEqpCp93KwRS1uUOodk2OJ6b+sq7ZPDSy2WuI5NFIxp/254TytR8ftefexkWn5xNiHUNpPOfSew==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bound": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-weakset": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-weakset/-/is-weakset-2.0.4.tgz", + "integrity": "sha512-mfcwb6IzQyOKTs84CQMrOwW4gQcaTOAWJ0zzJCl2WSPDrWk/OzDaImWFH3djXhb24g4eudZfLRozAvPGw4d9hQ==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-windows": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-windows/-/is-windows-1.0.2.tgz", + "integrity": "sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-wsl": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", + "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", + "dev": true, + "dependencies": { + "is-docker": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=", + "dev": true + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isstream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", + "integrity": "sha1-R+Y/evVa+m+S4VAOaQ64uFKcCZo=", + "dev": true + }, + "node_modules/istanbul-lib-coverage": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz", + "integrity": "sha512-eOeJ5BHCmHYvQK7xt9GkdHuzuCGS1Y6g9Gvnx3Ym33fz/HpLRYxiS0wHNr+m/MBC8B647Xt608vCDEvhl9c6Mw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-5.2.1.tgz", + "integrity": "sha512-pzqtp31nLv/XFOzXGuvhCb8qhjmTVo5vjVk19XE4CRlSWz0KoeJ3bw9XsA7nOp9YBf4qHjwBxkDzKcME/J29Yg==", + "dev": true, + "dependencies": { + "@babel/core": "^7.12.3", + "@babel/parser": "^7.14.7", + "@istanbuljs/schema": "^0.1.2", + "istanbul-lib-coverage": "^3.2.0", + "semver": "^6.3.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-instrument/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/istanbul-lib-report": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-report/-/istanbul-lib-report-3.0.1.tgz", + "integrity": "sha512-GCfE1mtsHGOELCU8e/Z7YWzpmybrx/+dSTfLrvY8qRmaY6zXTKWn6WQIjaAFw069icm6GVMNkgu0NzI4iPZUNw==", + "dev": true, + "dependencies": { + "istanbul-lib-coverage": "^3.0.0", + "make-dir": "^4.0.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-report/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-report/node_modules/make-dir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-4.0.0.tgz", + "integrity": "sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==", + "dev": true, + "dependencies": { + "semver": "^7.5.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/istanbul-lib-report/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/istanbul-lib-source-maps": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/istanbul-lib-source-maps/-/istanbul-lib-source-maps-4.0.1.tgz", + "integrity": "sha512-n3s8EwkdFIJCG3BPKBYvskgXGoy88ARzvegkitk60NxRdwltLOTaH7CUiMRXvwYorl0Q712iEjcWB+fK/MrWVw==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "istanbul-lib-coverage": "^3.0.0", + "source-map": "^0.6.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/istanbul-lib-source-maps/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/istanbul-reports": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/istanbul-reports/-/istanbul-reports-3.1.6.tgz", + "integrity": "sha512-TLgnMkKg3iTDsQ9PbPTdpfAK2DzjF9mqUG7RMgcQl8oFjad8ob4laGxv5XV5U9MAfx8D6tSJiUyuAwzLicaxlg==", + "dev": true, + "dependencies": { + "html-escaper": "^2.0.0", + "istanbul-lib-report": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/isurl": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isurl/-/isurl-1.0.0.tgz", + "integrity": "sha512-1P/yWsxPlDtn7QeRD+ULKQPaIaN6yF368GZ2vDfv0AL0NwpStafjWCDDdn0k8wgFMWpVAqG7oJhxHnlud42i9w==", + "dev": true, + "optional": true, + "dependencies": { + "has-to-string-tag-x": "^1.2.0", + "is-object": "^1.0.1" + }, + "engines": { + "node": ">= 4" + } + }, + "node_modules/iterator.prototype": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/iterator.prototype/-/iterator.prototype-1.1.5.tgz", + "integrity": "sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "define-data-property": "^1.1.4", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "get-proto": "^1.0.0", + "has-symbols": "^1.1.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/jest": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest/-/jest-29.7.0.tgz", + "integrity": "sha512-NIy3oAFp9shda19hy4HK0HRTWKtPJmGdnvywu01nOqNC2vZg+Z+fvJDxpMQA88eb2I9EcafcdjYgsDthnYTvGw==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/types": "^29.6.3", + "import-local": "^3.0.2", + "jest-cli": "^29.7.0" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-changed-files": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-changed-files/-/jest-changed-files-29.7.0.tgz", + "integrity": "sha512-fEArFiwf1BpQ+4bXSprcDc3/x4HSzL4al2tozwVpDFpsxALjLYdyiIK4e5Vz66GQJIbXJ82+35PtysofptNX2w==", + "dev": true, + "dependencies": { + "execa": "^5.0.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-changed-files/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/jest-changed-files/node_modules/execa": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", + "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", + "dev": true, + "dependencies": { + "cross-spawn": "^7.0.3", + "get-stream": "^6.0.0", + "human-signals": "^2.1.0", + "is-stream": "^2.0.0", + "merge-stream": "^2.0.0", + "npm-run-path": "^4.0.1", + "onetime": "^5.1.2", + "signal-exit": "^3.0.3", + "strip-final-newline": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sindresorhus/execa?sponsor=1" + } + }, + "node_modules/jest-changed-files/node_modules/get-stream": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", + "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/is-stream": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", + "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/npm-run-path": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", + "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", + "dev": true, + "dependencies": { + "path-key": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-changed-files/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-changed-files/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-circus/-/jest-circus-29.7.0.tgz", + "integrity": "sha512-3E1nCMgipcTkCocFwM90XXQab9bS+GMsjdpmPrlelaxwD93Ad8iVEjX/vvHPdLPnFf+L40u+5+iutRdA1N9myw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/expect": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "co": "^4.6.0", + "dedent": "^1.0.0", + "is-generator-fn": "^2.0.0", + "jest-each": "^29.7.0", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "p-limit": "^3.1.0", + "pretty-format": "^29.7.0", + "pure-rand": "^6.0.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-circus/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-circus/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-circus/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-circus/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-circus/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-circus/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-cli/-/jest-cli-29.7.0.tgz", + "integrity": "sha512-OVVobw2IubN/GSYsxETi+gOe7Ka59EFMR/twOU3Jb2GnKKeMGJB5SGUUrEz3SFVmJASUdZUzy83sLNNQ2gZslg==", + "dev": true, + "dependencies": { + "@jest/core": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "create-jest": "^29.7.0", + "exit": "^0.1.2", + "import-local": "^3.0.2", + "jest-config": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "yargs": "^17.3.1" + }, + "bin": { + "jest": "bin/jest.js" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "node-notifier": "^8.0.1 || ^9.0.0 || ^10.0.0" + }, + "peerDependenciesMeta": { + "node-notifier": { + "optional": true + } + } + }, + "node_modules/jest-cli/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-cli/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-cli/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-cli/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-cli/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-config/-/jest-config-29.7.0.tgz", + "integrity": "sha512-uXbpfeQ7R6TZBqI3/TxCU4q4ttk3u0PJeC+E0zbfSoSjq6bJ7buBPxzQPL0ifrkY4DNu4JUdk0ImlBUYi840eQ==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@jest/test-sequencer": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-jest": "^29.7.0", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "deepmerge": "^4.2.2", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-circus": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-runner": "^29.7.0", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "micromatch": "^4.0.4", + "parse-json": "^5.2.0", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "peerDependencies": { + "@types/node": "*", + "ts-node": ">=9.0.0" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "ts-node": { + "optional": true + } + } + }, + "node_modules/jest-config/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-config/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-config/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-config/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-config/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-config/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-config/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-dev-server": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/jest-dev-server/-/jest-dev-server-10.1.4.tgz", + "integrity": "sha512-bGQ6sedNGtT6AFHhCVqGTXMPz7UyJi/ZrhNBgyqsP0XU9N8acCEIfqZEA22rOaZ+NdEVsaltk6tL7UT6aXfI7w==", + "dev": true, + "dependencies": { + "chalk": "^4.1.2", + "cwd": "^0.10.0", + "find-process": "^1.4.7", + "prompts": "^2.4.2", + "spawnd": "^10.1.4", + "tree-kill": "^1.2.2", + "wait-on": "^8.0.1" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/jest-dev-server/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-dev-server/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-dev-server/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-dev-server/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-dev-server/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-dev-server/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" + } + }, + "node_modules/jest-dev-server/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-dev-server/node_modules/wait-on": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-8.0.5.tgz", + "integrity": "sha512-J3WlS0txVHkhLRb2FsmRg3dkMTCV1+M6Xra3Ho7HzZDHpE7DCOnoSoCJsZotrmW3uRMhvIJGSKUKrh/MeF4iag==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.12.1", + "joi": "^18.0.1", + "lodash": "^4.17.21", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jest-diff": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.7.0.tgz", + "integrity": "sha512-LMIgiIrhigmPrs03JHpxUh2yISK3vLFPkAodPeo0+BuF7wA2FoQbkEg1u8gBYBThncu7e1oEDUfIXVuTqLRUjw==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "diff-sequences": "^29.6.3", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-diff/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-diff/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-diff/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-diff/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-diff/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-docblock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-docblock/-/jest-docblock-29.7.0.tgz", + "integrity": "sha512-q617Auw3A612guyaFgsbFeYpNP5t2aoUNLwBUbc/0kD1R4t9ixDbyFTHd1nok4epoVFpr7PmeWHrhvuV3XaJ4g==", + "dev": true, + "dependencies": { + "detect-newline": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-each/-/jest-each-29.7.0.tgz", + "integrity": "sha512-gns+Er14+ZrEoC5fhOfYCY1LOHHr0TI+rQUHZS8Ttw2l7gl+80eHc/gFf2Ktkw0+SIACDTeWvpFcv3B04VembQ==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "jest-util": "^29.7.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-each/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-each/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-each/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-each/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-each/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-environment-jsdom/-/jest-environment-jsdom-30.3.0.tgz", + "integrity": "sha512-RLEOJy6ip1lpw0yqJ8tB3i88FC7VBz7i00Zvl2qF71IdxjS98gC9/0SPWYIBVXHm5hgCYK0PAlSlnHGGy9RoMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/environment": "30.3.0", + "@jest/environment-jsdom-abstract": "30.3.0", + "jsdom": "^26.1.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/environment": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/environment/-/environment-30.3.0.tgz", + "integrity": "sha512-SlLSF4Be735yQXyh2+mctBOzNDx5s5uLv88/j8Qn1wH679PDcwy67+YdADn8NJnGjzlXtN62asGH/T4vWOkfaw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/fake-timers": "30.3.0", + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-mock": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/fake-timers": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-30.3.0.tgz", + "integrity": "sha512-WUQDs8SOP9URStX1DzhD425CqbN/HxUYCTwVrT8sTVBfMvFqYt/s61EK5T05qnHu0po6RitXIvP9otZxYDzTGQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@sinonjs/fake-timers": "^15.0.0", + "@types/node": "*", + "jest-message-util": "30.3.0", + "jest-mock": "30.3.0", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/schemas": { + "version": "30.0.5", + "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-30.0.5.tgz", + "integrity": "sha512-DmdYgtezMkh3cpU8/1uyXakv3tJRcmcXxBOcO0tbaozPwpmh4YMsnWrQm9ZmZMfa5ocbxzbFk6O4bDPEc/iAnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@sinclair/typebox": "^0.34.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@jest/types": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/@jest/types/-/types-30.3.0.tgz", + "integrity": "sha512-JHm87k7bA33hpBngtU8h6UBub/fqqA9uXfw+21j5Hmk7ooPHlboRNxHq0JcMtC+n8VJGP1mcfnD3Mk+XKe1oSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/pattern": "30.0.1", + "@jest/schemas": "30.0.5", + "@types/istanbul-lib-coverage": "^2.0.6", + "@types/istanbul-reports": "^3.0.4", + "@types/node": "*", + "@types/yargs": "^17.0.33", + "chalk": "^4.1.2" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/@sinclair/typebox": { + "version": "0.34.48", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.48.tgz", + "integrity": "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/@sinonjs/fake-timers": { + "version": "15.1.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-15.1.1.tgz", + "integrity": "sha512-cO5W33JgAPbOh07tvZjUOJ7oWhtaqGHiZw+11DPbyqh2kHTBc3eF/CjJDeQ4205RLQsX6rxCuYOroFQwl7JDRw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/ci-info": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.4.0.tgz", + "integrity": "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/sibiraj-s" + } + ], + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/jest-environment-jsdom/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-message-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-30.3.0.tgz", + "integrity": "sha512-Z/j4Bo+4ySJ+JPJN3b2Qbl9hDq3VrXmnjjGEWD/x0BCXeOXPTV1iZYYzl2X8c1MaCOL+ewMyNBcm88sboE6YWw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@jest/types": "30.3.0", + "@types/stack-utils": "^2.0.3", + "chalk": "^4.1.2", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3", + "pretty-format": "30.3.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.6" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-mock": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-30.3.0.tgz", + "integrity": "sha512-OTzICK8CpE+t4ndhKrwlIdbM6Pn8j00lvmSmq5ejiO+KxukbLjgOflKWMn3KE34EZdQm5RqTuKj+5RIEniYhog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "jest-util": "30.3.0" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/jest-util": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-30.3.0.tgz", + "integrity": "sha512-/jZDa00a3Sz7rdyu55NLrQCIrbyIkbBxareejQI315f/i8HjYN+ZWsDLLpoQSiUIEIyZF/R8fDg3BmB8AtHttg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/types": "30.3.0", + "@types/node": "*", + "chalk": "^4.1.2", + "ci-info": "^4.2.0", + "graceful-fs": "^4.2.11", + "picomatch": "^4.0.3" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format": { + "version": "30.3.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-30.3.0.tgz", + "integrity": "sha512-oG4T3wCbfeuvljnyAzhBvpN45E8iOTXCU/TD3zXW80HA3dQ4ahdqMkWGiPWZvjpQwlbyHrPTWUAqUzGzv4l1JQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jest/schemas": "30.0.5", + "ansi-styles": "^5.2.0", + "react-is": "^18.3.1" + }, + "engines": { + "node": "^18.14.0 || ^20.0.0 || ^22.0.0 || >=24.0.0" + } + }, + "node_modules/jest-environment-jsdom/node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-environment-jsdom/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-environment-node": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-environment-node/-/jest-environment-node-29.7.0.tgz", + "integrity": "sha512-DOSwCRqXirTOyheM+4d5YZOrWcdu0LNZ87ewUoywbcb2XR4wKgqiG8vNeYwhjFMbEkfju7wx2GYH0P2gevGvFw==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-mock": "^29.7.0", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-get-type": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.6.3.tgz", + "integrity": "sha512-zrteXnqYxfQh7l5FHyL38jL39di8H8rHoecLH3JNxH3BwOrBsNeabdap5e0I23lD4HHI8W5VFBZqG4Eaq5LNcw==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.7.0.tgz", + "integrity": "sha512-fP8u2pyfqx0K1rGn1R9pyE0/KTn+G7PxktWidOBTqFPLYX0b9ksaMFkhK5vrS3DVun09pckLdlx90QthlW7AmA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/graceful-fs": "^4.1.3", + "@types/node": "*", + "anymatch": "^3.0.3", + "fb-watchman": "^2.0.0", + "graceful-fs": "^4.2.9", + "jest-regex-util": "^29.6.3", + "jest-util": "^29.7.0", + "jest-worker": "^29.7.0", + "micromatch": "^4.0.4", + "walker": "^1.0.8" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + }, + "optionalDependencies": { + "fsevents": "^2.3.2" + } + }, + "node_modules/jest-haste-map/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-haste-map/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-haste-map/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-haste-map/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-haste-map/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-haste-map/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-leak-detector": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-leak-detector/-/jest-leak-detector-29.7.0.tgz", + "integrity": "sha512-kYA8IJcSYtST2BY9I+SMC32nDpBT3J2NvWJx8+JCuCdl/CR1I4EKUJROiP8XtCcxqgTTBGJNdbB1A8XRKbTetw==", + "dev": true, + "dependencies": { + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.7.0.tgz", + "integrity": "sha512-sBkD+Xi9DtcChsI3L3u0+N0opgPYnCRPtGcQYrgXmR+hmt/fYfWAL0xRXYU8eWOdfuLgBe0YCW3AFtnRLagq/g==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-matcher-utils/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-matcher-utils/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-matcher-utils/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.7.0.tgz", + "integrity": "sha512-GBEV4GRADeP+qtB2+6u61stea8mGcOT4mCtrYISZwfu9/ISHFJ/5zOMXYbpBE9RsS5+Gb63DW4FgmnKJ79Kf6w==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.12.13", + "@jest/types": "^29.6.3", + "@types/stack-utils": "^2.0.0", + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "micromatch": "^4.0.4", + "pretty-format": "^29.7.0", + "slash": "^3.0.0", + "stack-utils": "^2.0.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-message-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-message-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-message-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-message-util/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/jest-message-util/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/jest-message-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-message-util/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/jest-mock": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-mock/-/jest-mock-29.7.0.tgz", + "integrity": "sha512-ITOMZn+UkYS4ZFh83xYAOzWStloNzJFO2s8DWrE4lhtGD+AorgnbkiKERe4wQVBydIGPx059g6riW5Btp6Llnw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "jest-util": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-pnp-resolver": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/jest-pnp-resolver/-/jest-pnp-resolver-1.2.3.tgz", + "integrity": "sha512-+3NpwQEnRoIBtx4fyhblQDPgJI0H1IEIkX7ShLUjPGA7TtUTvI1oiKi3SR4oBR0hQhQR80l4WAe5RrXBwWMA8w==", + "dev": true, + "engines": { + "node": ">=6" + }, + "peerDependencies": { + "jest-resolve": "*" + }, + "peerDependenciesMeta": { + "jest-resolve": { + "optional": true + } + } + }, + "node_modules/jest-regex-util": { + "version": "29.6.3", + "resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.6.3.tgz", + "integrity": "sha512-KJJBsRCyyLNWCNBOvZyRDnAIfUiRJ8v+hOBQYGn8gDyF3UegwiP4gwRR3/SDa42g1YbVycTidUF3rKjyLFDWbg==", + "dev": true, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve/-/jest-resolve-29.7.0.tgz", + "integrity": "sha512-IOVhZSrg+UvVAshDSDtHyFCCBUl/Q3AAJv8iZ6ZjnZ74xzvwuzLXid9IIIPgTnY62SJjfuupMKZsZQRsCvxEgA==", + "dev": true, + "dependencies": { + "chalk": "^4.0.0", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-pnp-resolver": "^1.2.2", + "jest-util": "^29.7.0", + "jest-validate": "^29.7.0", + "resolve": "^1.20.0", + "resolve.exports": "^2.0.0", + "slash": "^3.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve-dependencies": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-resolve-dependencies/-/jest-resolve-dependencies-29.7.0.tgz", + "integrity": "sha512-un0zD/6qxJ+S0et7WxeI3H5XSe9lTBBR7bOHCHXkKR6luG5mwDDlIzVQ0V5cZCuoTgEdcdwzTghYkTWfubi+nA==", + "dev": true, + "dependencies": { + "jest-regex-util": "^29.6.3", + "jest-snapshot": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-resolve/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-resolve/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-resolve/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-resolve/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-resolve/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runner/-/jest-runner-29.7.0.tgz", + "integrity": "sha512-fsc4N6cPCAahybGBfTRcq5wFR6fpLznMg47sY5aDpsoejOcVYFb07AHuSnR0liMcPTgBsA3ZJL6kFOjPdoNipQ==", + "dev": true, + "dependencies": { + "@jest/console": "^29.7.0", + "@jest/environment": "^29.7.0", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "graceful-fs": "^4.2.9", + "jest-docblock": "^29.7.0", + "jest-environment-node": "^29.7.0", + "jest-haste-map": "^29.7.0", + "jest-leak-detector": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-resolve": "^29.7.0", + "jest-runtime": "^29.7.0", + "jest-util": "^29.7.0", + "jest-watcher": "^29.7.0", + "jest-worker": "^29.7.0", + "p-limit": "^3.1.0", + "source-map-support": "0.5.13" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runner/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runner/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runner/node_modules/jest-worker": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", + "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", + "dev": true, + "dependencies": { + "@types/node": "*", + "jest-util": "^29.7.0", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runner/node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/jest-runner/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-runner/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-runtime/-/jest-runtime-29.7.0.tgz", + "integrity": "sha512-gUnLjgwdGqW7B4LvOIkbKs9WGbn+QLqRQQ9juC6HndeDiezIwhDP+mhMwHWCEcfQ5RUXa6OPnFF8BJh5xegwwQ==", + "dev": true, + "dependencies": { + "@jest/environment": "^29.7.0", + "@jest/fake-timers": "^29.7.0", + "@jest/globals": "^29.7.0", + "@jest/source-map": "^29.6.3", + "@jest/test-result": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "cjs-module-lexer": "^1.0.0", + "collect-v8-coverage": "^1.0.0", + "glob": "^7.1.3", + "graceful-fs": "^4.2.9", + "jest-haste-map": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-mock": "^29.7.0", + "jest-regex-util": "^29.6.3", + "jest-resolve": "^29.7.0", + "jest-snapshot": "^29.7.0", + "jest-util": "^29.7.0", + "slash": "^3.0.0", + "strip-bom": "^4.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-runtime/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-runtime/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-runtime/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-runtime/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-runtime/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-snapshot/-/jest-snapshot-29.7.0.tgz", + "integrity": "sha512-Rm0BMWtxBcioHr1/OX5YCP8Uov4riHvKPknOGs804Zg9JGZgmIBkbtlxJC/7Z4msKYVbIJtfU+tKb8xlYNfdkw==", + "dev": true, + "dependencies": { + "@babel/core": "^7.11.6", + "@babel/generator": "^7.7.2", + "@babel/plugin-syntax-jsx": "^7.7.2", + "@babel/plugin-syntax-typescript": "^7.7.2", + "@babel/types": "^7.3.3", + "@jest/expect-utils": "^29.7.0", + "@jest/transform": "^29.7.0", + "@jest/types": "^29.6.3", + "babel-preset-current-node-syntax": "^1.0.0", + "chalk": "^4.0.0", + "expect": "^29.7.0", + "graceful-fs": "^4.2.9", + "jest-diff": "^29.7.0", + "jest-get-type": "^29.6.3", + "jest-matcher-utils": "^29.7.0", + "jest-message-util": "^29.7.0", + "jest-util": "^29.7.0", + "natural-compare": "^1.4.0", + "pretty-format": "^29.7.0", + "semver": "^7.5.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-snapshot/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-snapshot/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-snapshot/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-snapshot/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", + "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "@types/node": "*", + "chalk": "^4.0.0", + "ci-info": "^3.2.0", + "graceful-fs": "^4.2.9", + "picomatch": "^2.2.3" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-util/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-util/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-util/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-util/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-util/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-validate/-/jest-validate-29.7.0.tgz", + "integrity": "sha512-ZB7wHqaRGVw/9hST/OuFUReG7M8vKeq0/J2egIGLdvjHCmYqGARhzXmtgi+gVeZ5uXFF219aOc3Ls2yLg27tkw==", + "dev": true, + "dependencies": { + "@jest/types": "^29.6.3", + "camelcase": "^6.2.0", + "chalk": "^4.0.0", + "jest-get-type": "^29.6.3", + "leven": "^3.1.0", + "pretty-format": "^29.7.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-validate/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/jest-validate/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-validate/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-validate/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-validate/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-validate/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/jest-watcher/-/jest-watcher-29.7.0.tgz", + "integrity": "sha512-49Fg7WXkU3Vl2h6LbLtMQ/HyB6rXSIX7SqvBLQmssRBGN9I0PNvPmAmCWSOY6SOvrjhI/F7/bGAv9RtnsPA03g==", + "dev": true, + "dependencies": { + "@jest/test-result": "^29.7.0", + "@jest/types": "^29.6.3", + "@types/node": "*", + "ansi-escapes": "^4.2.1", + "chalk": "^4.0.0", + "emittery": "^0.13.1", + "jest-util": "^29.7.0", + "string-length": "^4.0.1" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" + } + }, + "node_modules/jest-watcher/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/jest-watcher/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/jest-watcher/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/jest-watcher/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-watcher/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/joi": { + "version": "18.0.2", + "resolved": "https://registry.npmjs.org/joi/-/joi-18.0.2.tgz", + "integrity": "sha512-RuCOQMIt78LWnktPoeBL0GErkNaJPTBGcYuyaBvUOQSpcpcLfWrHPPihYdOGbV5pam9VTWbeoF7TsGiHugcjGA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@hapi/address": "^5.1.1", + "@hapi/formula": "^3.0.2", + "@hapi/hoek": "^11.0.7", + "@hapi/pinpoint": "^2.0.1", + "@hapi/tlds": "^1.1.1", + "@hapi/topo": "^6.0.2", + "@standard-schema/spec": "^1.0.0" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/jpeg-js": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/jpeg-js/-/jpeg-js-0.4.4.tgz", + "integrity": "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg==", + "dev": true + }, + "node_modules/jpegtran-bin": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jpegtran-bin/-/jpegtran-bin-4.0.0.tgz", + "integrity": "sha512-2cRl1ism+wJUoYAYFt6O/rLBfpXNWG2dUWbgcEkTt5WGMnqI46eEro8T4C5zGROxKRqyKpCBSdHPvt5UYCtxaQ==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "logalot": "^2.0.0" + }, + "bin": { + "jpegtran": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jquery": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/jquery/-/jquery-3.7.1.tgz", + "integrity": "sha512-m4avr8yL8kmFN8psrbFFFmB/If14iN5o9nw/NgnnM+kybDJpRsAynV2BsfpTYrTRysYUdADVD7CkUUizgkpLfg==" + }, + "node_modules/jquery-color": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jquery-color/-/jquery-color-3.0.0.tgz", + "integrity": "sha512-TbyqpkxEfVq9K/eUn4jW14YnJn19dORJrplJ3vo9jgLrqnuOyH6gyv8yDZdYcERlrVVDub4HV5r2t5KGjcvbKg==", + "peerDependencies": { + "jquery": ">=1.12.0" + } + }, + "node_modules/jquery-form": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/jquery-form/-/jquery-form-4.3.0.tgz", + "integrity": "sha512-q3uaVCEWdLOYUCI6dpNdwf/7cJFOsUgdpq6r0taxtGQ5NJSkOzofyWm4jpOuJ5YxdmL1FI5QR+q+HB63HHLGnQ==", + "dependencies": { + "jquery": ">=1.7.2" + } + }, + "node_modules/jquery-hoverintent": { + "version": "1.10.2", + "resolved": "https://registry.npmjs.org/jquery-hoverintent/-/jquery-hoverintent-1.10.2.tgz", + "integrity": "sha512-YU4xvTywSu+/aZvbtSV8Svgcv7F3iMsXXO8Fm1Scvt9wvKDP7C1F6w1j3Pjn0lQqICxc5s7MnsL3Nbh8DEeOcg==", + "dependencies": { + "jquery": ">=1.7.0" + }, + "peerDependencies": { + "jquery": ">=1.7.0" + } + }, + "node_modules/js-library-detector": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/js-library-detector/-/js-library-detector-6.7.0.tgz", + "integrity": "sha512-c80Qupofp43y4cJ7+8TTDN/AsDwLi5oOm/plBrWI+iQt485vKXCco+yVmOwEgdo9VOdsYTuV0UlTeetVPTriXA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/js-yaml": { + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsbn": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", + "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsdoc": { + "version": "3.6.11", + "resolved": "https://registry.npmjs.org/jsdoc/-/jsdoc-3.6.11.tgz", + "integrity": "sha512-8UCU0TYeIYD9KeLzEcAu2q8N/mx9O3phAGl32nmHlE0LpaJL71mMkP4d+QE5zWfNt50qheHtOZ0qoxVrsX5TUg==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.9.4", + "@types/markdown-it": "^12.2.3", + "bluebird": "^3.7.2", + "catharsis": "^0.9.0", + "escape-string-regexp": "^2.0.0", + "js2xmlparser": "^4.0.2", + "klaw": "^3.0.0", + "markdown-it": "^12.3.2", + "markdown-it-anchor": "^8.4.1", + "marked": "^4.0.10", + "mkdirp": "^1.0.4", + "requizzle": "^0.2.3", + "strip-json-comments": "^3.1.0", + "taffydb": "2.6.2", + "underscore": "~1.13.2" + }, + "bin": { + "jsdoc": "jsdoc.js" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc-type-pratt-parser": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsdoc-type-pratt-parser/-/jsdoc-type-pratt-parser-4.0.0.tgz", + "integrity": "sha512-YtOli5Cmzy3q4dP26GraSOeAhqecewG04hoO8DY56CH4KJ9Fvv5qKWUCCo3HZob7esJQHCv6/+bnTy72xZZaVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/jsdoc/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/jsdoc/node_modules/js2xmlparser": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/js2xmlparser/-/js2xmlparser-4.0.2.tgz", + "integrity": "sha512-6n4D8gLlLf1n5mNLQPRfViYzu9RATblzPEtm1SthMX1Pjao0r9YI9nw7ZIfRxQMERS87mcswrg+r/OYrPRX6jA==", + "dev": true, + "dependencies": { + "xmlcreate": "^2.0.4" + } + }, + "node_modules/jsdoc/node_modules/markdown-it-anchor": { + "version": "8.6.2", + "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-8.6.2.tgz", + "integrity": "sha512-JNaekTlIwwyYGBN3zifZDxgz4bSL8sbEj58fdTZGmPSMMGXBZapFjcZk2I33Jy79c1fvCKHpF7MA/67FOTjvzA==", + "dev": true, + "peerDependencies": { + "@types/markdown-it": "*", + "markdown-it": "*" + } + }, + "node_modules/jsdoc/node_modules/marked": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/marked/-/marked-4.0.14.tgz", + "integrity": "sha512-HL5sSPE/LP6U9qKgngIIPTthuxC0jrfxpYMZ3LdGDD3vTnLs59m2Z7r6+LNDR3ToqEQdkKd6YaaEfJhodJmijQ==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/jsdoc/node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", + "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/jsdoc/node_modules/xmlcreate": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.4.tgz", + "integrity": "sha512-nquOebG4sngPmGPICTS5EnxqhKbCmz5Ox5hsszI2T6U5qdrJizBc+0ilYSEjTSzU0yZcmvppztXe/5Al5fUwdg==", + "dev": true + }, + "node_modules/jsdom": { + "version": "26.1.0", + "resolved": "https://registry.npmjs.org/jsdom/-/jsdom-26.1.0.tgz", + "integrity": "sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssstyle": "^4.2.1", + "data-urls": "^5.0.0", + "decimal.js": "^10.5.0", + "html-encoding-sniffer": "^4.0.0", + "http-proxy-agent": "^7.0.2", + "https-proxy-agent": "^7.0.6", + "is-potential-custom-element-name": "^1.0.1", + "nwsapi": "^2.2.16", + "parse5": "^7.2.1", + "rrweb-cssom": "^0.8.0", + "saxes": "^6.0.0", + "symbol-tree": "^3.2.4", + "tough-cookie": "^5.1.1", + "w3c-xmlserializer": "^5.0.0", + "webidl-conversions": "^7.0.0", + "whatwg-encoding": "^3.1.1", + "whatwg-mimetype": "^4.0.0", + "whatwg-url": "^14.1.1", + "ws": "^8.18.0", + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "canvas": "^3.0.0" + }, + "peerDependenciesMeta": { + "canvas": { + "optional": true + } + } + }, + "node_modules/jsdom/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/jsesc": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", + "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jshint": { + "version": "2.13.6", + "resolved": "https://registry.npmjs.org/jshint/-/jshint-2.13.6.tgz", + "integrity": "sha512-IVdB4G0NTTeQZrBoM8C5JFVLjV2KtZ9APgybDA1MK73xb09qFs0jCXyQLnCOp1cSZZZbvhq/6mfXHUTaDkffuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "cli": "~1.0.0", + "console-browserify": "1.1.x", + "exit": "0.1.x", + "htmlparser2": "3.8.x", + "lodash": "~4.17.21", + "minimatch": "~3.0.2", + "strip-json-comments": "1.0.x" + }, + "bin": { + "jshint": "bin/jshint" + } + }, + "node_modules/jshint/node_modules/console-browserify": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.1.0.tgz", + "integrity": "sha1-8CQcRXMKn8YyOyBtvzjtx0HQuxA=", + "dev": true, + "dependencies": { + "date-now": "^0.1.4" + } + }, + "node_modules/jshint/node_modules/lodash": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.23.tgz", + "integrity": "sha512-LgVTMpQtIopCi79SJeDiP0TfWi5CNEc/L/aRdTh3yIvmZXTnheWpKjSZhnvMl8iXbC1tFg9gdHHDMLoV7CnG+w==", + "dev": true, + "license": "MIT" + }, + "node_modules/jshint/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/jshint/node_modules/strip-json-comments": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-1.0.4.tgz", + "integrity": "sha512-AOPG8EBc5wAikaG1/7uFCNFJwnKOuQwFTpYBdTW6OvWHeZBQBrAA/amefHGrEiOnCPcLFZK6FUPtWVKpQVIRgg==", + "dev": true, + "license": "MIT", + "bin": { + "strip-json-comments": "cli.js" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/json-buffer": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.0.tgz", + "integrity": "sha512-CuUqjv0FUZIdXkHPI8MezCnFCdaTAacej1TZYulLoAg1h/PhwkdXFN4V/gzY4g+fMBCOV2xF+rp7t2XD2ns/NQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stringify-safe": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", + "integrity": "sha1-Epai1Y/UXxmg9s4B1lcB4sc1tus=", + "dev": true + }, + "node_modules/json2php": { + "version": "0.0.12", + "resolved": "https://registry.npmjs.org/json2php/-/json2php-0.0.12.tgz", + "integrity": "sha512-fM/jNXBHZBaizxgLCoFjkX21CyK+zO4aDQvrJnvtwOHeN1qJwRgZEE3K0gqdKBYP5DhueNVHdC2gi4Yalim98g==", + "dev": true, + "license": "BSD" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/jsonc-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.0.0.tgz", + "integrity": "sha512-fQzRfAbIBnR0IQvftw9FJveWiHp72Fg20giDrHz6TdfB12UH/uue0D3hm57UB5KgAVuniLMCaS8P1IMj9NR7cA==", + "dev": true + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/jsonlint": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/jsonlint/-/jsonlint-1.6.3.tgz", + "integrity": "sha512-jMVTMzP+7gU/IyC6hvKyWpUU8tmTkK5b3BPNuMI9U8Sit+YAWLlZwB6Y6YrdCxfg2kNz05p3XY3Bmm4m26Nv3A==", + "dependencies": { + "JSV": "^4.0.x", + "nomnom": "^1.5.x" + }, + "bin": { + "jsonlint": "lib/cli.js" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/jsprim": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", + "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", + "dev": true, + "dependencies": { + "assert-plus": "1.0.0", + "extsprintf": "1.3.0", + "json-schema": "0.4.0", + "verror": "1.10.0" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/jsprim/node_modules/json-schema": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", + "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==", + "dev": true + }, + "node_modules/JSV": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/JSV/-/JSV-4.0.2.tgz", + "integrity": "sha512-ZJ6wx9xaKJ3yFUhq5/sk82PJMuUyLk277I8mQeyDgCTjGdjWJIvPfaU5LIXaMuaN2UO1X3kZH4+lgphublZUHw==", + "engines": { + "node": "*" + } + }, + "node_modules/jsx-ast-utils": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz", + "integrity": "sha512-ZZow9HBI5O6EPgSJLUb8n2NKgmVWTwCvHGwFuJlMjvLFqlGG6pjirPhtdsseaLZjSibD8eegzmYpUZwoIlj2cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-includes": "^3.1.6", + "array.prototype.flat": "^1.3.1", + "object.assign": "^4.1.4", + "object.values": "^1.1.6" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/just-extend": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/just-extend/-/just-extend-6.2.0.tgz", + "integrity": "sha512-cYofQu2Xpom82S6qD778jBDpwvvy39s1l/hrYij2u9AMdQcGRpaBu6kY4mVhuno5kJVi1DAz4aiphA2WI1/OAw==", + "dev": true + }, + "node_modules/keyv": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-3.0.0.tgz", + "integrity": "sha512-eguHnq22OE3uVoSYG0LVWNP+4ppamWr9+zWBe1bsNcovIMy6huUJFPgy4mGwCd/rnl3vOLGW1MTlu4c57CT1xA==", + "dev": true, + "optional": true, + "dependencies": { + "json-buffer": "3.0.0" + } + }, + "node_modules/kind-of": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-3.2.2.tgz", + "integrity": "sha512-NOW9QQXMoZGg/oqnVNoNTTIFEIid1627WCffUBJEdMxYApq7mNE7CpzucIPc+ZQg25Phej7IJSmX3hO+oblOtQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-buffer": "^1.1.5" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/klaw": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/klaw/-/klaw-3.0.0.tgz", + "integrity": "sha512-0Fo5oir+O9jnXu5EefYbVK+mHMBeEVEy2cmctR1O1NECcCkPRreJKrS6Qt/j3KC2C148Dfo9i3pCmCMsdqGr0g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.1.9" + } + }, + "node_modules/kleur": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", + "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/klona": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", + "integrity": "sha512-dhG34DXATL5hSxJbIexCft8FChFXtmskoZYnoPWjXQuebWYCNkVeV3KkGegCK9CP1oswI/vQibS2GY7Em/sJJA==", + "dev": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/known-css-properties": { + "version": "0.37.0", + "resolved": "https://registry.npmjs.org/known-css-properties/-/known-css-properties-0.37.0.tgz", + "integrity": "sha512-JCDrsP4Z1Sb9JwG0aJ8Eo2r7k4Ou5MwmThS/6lcIe1ICyb7UBJKGRIUUdqc2ASdE/42lgz6zFUnzAIhtXnBVrQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/language-subtag-registry": { + "version": "0.3.23", + "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", + "integrity": "sha512-0K65Lea881pHotoGEa5gDlMxt3pctLi2RplBb7Ezh4rRdLEOtgi7n4EwK9lamnUCkKBqaeKRVebTq6BAxSkpXQ==", + "dev": true, + "license": "CC0-1.0" + }, + "node_modules/language-tags": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/language-tags/-/language-tags-1.0.9.tgz", + "integrity": "sha512-MbjN408fEndfiQXbFQ1vnd+1NoLDsnQW41410oQBXiyXDMYH5z505juWa4KUE1LqxRC7DgOgZDbKLxHIwm27hA==", + "dev": true, + "license": "MIT", + "dependencies": { + "language-subtag-registry": "^0.3.20" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/launch-editor": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.6.1.tgz", + "integrity": "sha512-eB/uXmFVpY4zezmGp5XtU21kwo7GBbKB+EQ+UZeWtGb9yAM5xt/Evk+lYH3eRNAtId+ej4u7TYPFZ07w4s7rRw==", + "dev": true, + "dependencies": { + "picocolors": "^1.0.0", + "shell-quote": "^1.8.1" + } + }, + "node_modules/lazy-cache": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-1.0.4.tgz", + "integrity": "sha512-RE2g0b5VGZsOCFOCgP7omTRYFqydmZkBwl5oNnQ1lDYC57uyO9KqNnNVxT7COSHTxrRCWVcAVOcbjk+tvh/rgQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/leven": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", + "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lie": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/lie/-/lie-3.1.1.tgz", + "integrity": "sha512-RiNhHysUjhrDQntfYSfY4MU24coXXdEOgw9WGcKHNeEwffDYbF//u87M1EWaMGzuFoSbqW0C9C6lEEhDOAswfw==", + "dev": true, + "dependencies": { + "immediate": "~3.0.5" + } + }, + "node_modules/liftup": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/liftup/-/liftup-3.0.1.tgz", + "integrity": "sha512-yRHaiQDizWSzoXk3APcA71eOI/UuhEkNN9DiW2Tt44mhYzX4joFoCZlxsSOF7RyeLlfqzFLQI1ngFq3ggMPhOw==", + "dev": true, + "dependencies": { + "extend": "^3.0.2", + "findup-sync": "^4.0.0", + "fined": "^1.2.0", + "flagged-respawn": "^1.0.1", + "is-plain-object": "^2.0.4", + "object.map": "^1.0.1", + "rechoir": "^0.7.0", + "resolve": "^1.19.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/liftup/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/liftup/node_modules/expand-tilde": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", + "integrity": "sha512-A5EmesHW6rfnZ9ysHQjPdJRni0SRar0tjtG5MNtm9n5TUvsYU8oozprtRD4AqHxcZWWlVuAmQo2nWKfN9oyjTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/liftup/node_modules/findup-sync": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-4.0.0.tgz", + "integrity": "sha512-6jvvn/12IC4quLBL1KNokxC7wWTvYncaVUYSoxWw7YykPLuRrnv4qdHcSOywOI5RpkOVGeQRtWM8/q+G6W6qfQ==", + "dev": true, + "dependencies": { + "detect-file": "^1.0.0", + "is-glob": "^4.0.0", + "micromatch": "^4.0.2", + "resolve-dir": "^1.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/liftup/node_modules/global-modules": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", + "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", + "dev": true, + "dependencies": { + "global-prefix": "^1.0.1", + "is-windows": "^1.0.1", + "resolve-dir": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup/node_modules/global-prefix": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", + "integrity": "sha512-5lsx1NUDHtSjfg0eHlmYvZKv8/nVqX4ckFbM+FrGcQ+04KWcWFo9P5MxPZYSzUvyzmdTbI7Eix8Q4IbELDqzKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.2", + "homedir-polyfill": "^1.0.1", + "ini": "^1.3.4", + "is-windows": "^1.0.1", + "which": "^1.2.14" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/liftup/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/liftup/node_modules/resolve-dir": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", + "integrity": "sha512-R7uiTjECzvOsWSfdM0QKFNBVFcK27aHOUwdvK53BcW8zqnGdYp0Fbj82cy54+2A4P2tFM22J5kRfe1R+lM/1yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "expand-tilde": "^2.0.0", + "global-modules": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/liftup/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/liftup/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/lighthouse": { + "version": "12.3.0", + "resolved": "https://registry.npmjs.org/lighthouse/-/lighthouse-12.3.0.tgz", + "integrity": "sha512-OaLE8DasnwQkn2CBo2lKtD+IQv42mNP3T+Vaw29I++rAh0Zpgc6SM15usdIYyzhRMR5EWFxze5Fyb+HENJSh2A==", + "dev": true, + "dependencies": { + "@paulirish/trace_engine": "0.0.39", + "@sentry/node": "^7.0.0", + "axe-core": "^4.10.2", + "chrome-launcher": "^1.1.2", + "configstore": "^5.0.1", + "csp_evaluator": "1.1.1", + "devtools-protocol": "0.0.1312386", + "enquirer": "^2.3.6", + "http-link-header": "^1.1.1", + "intl-messageformat": "^10.5.3", + "jpeg-js": "^0.4.4", + "js-library-detector": "^6.7.0", + "lighthouse-logger": "^2.0.1", + "lighthouse-stack-packs": "1.12.2", + "lodash-es": "^4.17.21", + "lookup-closest-locale": "6.2.0", + "metaviewport-parser": "0.3.0", + "open": "^8.4.0", + "parse-cache-control": "1.0.1", + "puppeteer-core": "^23.10.4", + "robots-parser": "^3.0.1", + "semver": "^5.3.0", + "speedline-core": "^1.4.3", + "third-party-web": "^0.26.1", + "tldts-icann": "^6.1.16", + "ws": "^7.0.0", + "yargs": "^17.3.1", + "yargs-parser": "^21.0.0" + }, + "bin": { + "chrome-debug": "core/scripts/manual-chrome-launcher.js", + "lighthouse": "cli/index.js", + "smokehouse": "cli/test/smokehouse/frontends/smokehouse-bin.js" + }, + "engines": { + "node": ">=18.16" + } + }, + "node_modules/lighthouse-logger": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/lighthouse-logger/-/lighthouse-logger-2.0.1.tgz", + "integrity": "sha512-ioBrW3s2i97noEmnXxmUq7cjIcVRjT5HBpAYy8zE11CxU9HqlWHHeRxfeN1tn8F7OEMVPIC9x1f8t3Z7US9ehQ==", + "dev": true, + "dependencies": { + "debug": "^2.6.9", + "marky": "^1.2.2" + } + }, + "node_modules/lighthouse-logger/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/lighthouse-logger/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/lighthouse-stack-packs": { + "version": "1.12.2", + "resolved": "https://registry.npmjs.org/lighthouse-stack-packs/-/lighthouse-stack-packs-1.12.2.tgz", + "integrity": "sha512-Ug8feS/A+92TMTCK6yHYLwaFMuelK/hAKRMdldYkMNwv+d9PtWxjXEg6rwKtsUXTADajhdrhXyuNCJ5/sfmPFw==", + "dev": true + }, + "node_modules/lighthouse/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true + }, + "node_modules/linkify-it": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/linkify-it/-/linkify-it-3.0.3.tgz", + "integrity": "sha512-ynTsyrFSdE5oZ/O9GEf00kPngmOfVwazR5GKDq6EYfhlpFug3J2zybX56a2PRRpc9P+FuSoGNAwjlbDs9jJBPQ==", + "dev": true, + "dependencies": { + "uc.micro": "^1.0.1" + } + }, + "node_modules/livereload-js": { + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/livereload-js/-/livereload-js-2.4.0.tgz", + "integrity": "sha512-XPQH8Z2GDP/Hwz2PCDrh2mth4yFejwA1OZ/81Ti3LgKyhDcEjsSsqFWZojHG0va/duGd+WyosY7eXLDoOyqcPw==", + "dev": true + }, + "node_modules/load-json-file": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", + "integrity": "sha1-lWkFcI1YtLq0wiYbBPWfMcmTdMA=", + "dev": true, + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "parse-json": "^2.2.0", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0", + "strip-bom": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/parse-json": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", + "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "error-ex": "^1.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/load-json-file/node_modules/strip-bom": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", + "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "is-utf8": "^0.2.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/loader-utils": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", + "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", + "dev": true, + "dependencies": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + }, + "engines": { + "node": ">=8.9.0" + } + }, + "node_modules/localforage": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/localforage/-/localforage-1.10.0.tgz", + "integrity": "sha512-14/H1aX7hzBBmmh7sGPd+AOMkkIrHM3Z1PAyGgZigA1H1p5O5ANnMyWzvpAETtG68/dC4pC0ncy3+PPGzXZHPg==", + "dev": true, + "dependencies": { + "lie": "3.1.1" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.18.1", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz", + "integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==", + "license": "MIT" + }, + "node_modules/lodash-es": { + "version": "4.17.23", + "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.23.tgz", + "integrity": "sha512-kVI48u3PZr38HdYz98UmfPnXl2DXrpdctLrFLCd3kOx1xUkOmpFPx7gCWWM5MPkL/fD8zb+Ph0QzjGFs4+hHWg==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, + "node_modules/lodash.get": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.get/-/lodash.get-4.4.2.tgz", + "integrity": "sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==", + "dev": true + }, + "node_modules/lodash.isplainobject": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz", + "integrity": "sha1-fFJqUtibRcRcxpC4gWO+BJf1UMs=", + "dev": true + }, + "node_modules/lodash.memoize": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", + "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", + "dev": true + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.truncate": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/lodash.truncate/-/lodash.truncate-4.4.2.tgz", + "integrity": "sha512-jttmRe7bRse52OsWIMDLaXxWqRAmtIUccAQ3garviCqJjafXOfNMO0yMfNpdD6zbGaTU0P5Nz7e7gAT6cKmJRw==", + "dev": true, + "license": "MIT" + }, + "node_modules/lodash.uniq": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", + "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", + "dev": true + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/log-symbols/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/log-symbols/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/log-symbols/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/log-symbols/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/log-symbols/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/logalot": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz", + "integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=", + "dev": true, + "optional": true, + "dependencies": { + "figures": "^1.3.5", + "squeak": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loglevel": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.9.2.tgz", + "integrity": "sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==", + "dev": true, + "engines": { + "node": ">= 0.6.0" + }, + "funding": { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/loglevel" + } + }, + "node_modules/longest": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", + "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lookup-closest-locale": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/lookup-closest-locale/-/lookup-closest-locale-6.2.0.tgz", + "integrity": "sha512-/c2kL+Vnp1jnV6K6RpDTHK3dgg0Tu2VVp+elEiJpjfS1UyY7AjOYHohRug6wT0OpoX2qFgNORndE9RqesfVxWQ==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/loud-rejection": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", + "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "dev": true, + "optional": true, + "dependencies": { + "currently-unhandled": "^0.4.1", + "signal-exit": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/lowercase-keys": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.0.tgz", + "integrity": "sha512-RPlX0+PHuvxVDZ7xX+EBVAp4RsVxP/TdDSN2mJYdiq1Lc4Hz7EUSjUI7RZrKKlmrIzVhf6Jo2stj7++gVarS0A==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lpad-align": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz", + "integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=", + "dev": true, + "optional": true, + "dependencies": { + "get-stdin": "^4.0.1", + "indent-string": "^2.1.0", + "longest": "^1.0.0", + "meow": "^3.3.0" + }, + "bin": { + "lpad-align": "cli.js" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/lru-cache": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", + "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "dev": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/make-dir/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/make-iterator": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", + "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "dev": true, + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/make-iterator/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/makeerror": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.12.tgz", + "integrity": "sha512-JmqCvUhmt43madlpFzG4BQzG2Z3m6tvQDNKdClZnO3VbIudJYmxsT0FNJMeiB2+JTSlTQTSbU8QdesVmwJcmLg==", + "dev": true, + "dependencies": { + "tmpl": "1.0.5" + } + }, + "node_modules/map-cache": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", + "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-obj": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", + "integrity": "sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/map-values": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", + "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", + "dev": true + }, + "node_modules/map-visit": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", + "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "dev": true, + "dependencies": { + "object-visit": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/markdown-it": { + "version": "12.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-12.3.2.tgz", + "integrity": "sha512-TchMembfxfNVpHkbtriWltGWc+m3xszaRD0CZup7GFFhzIgQqxIfn3eGj1yZpfuflzPvfkt611B2Q/Bsk1YnGg==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1", + "entities": "~2.1.0", + "linkify-it": "^3.0.1", + "mdurl": "^1.0.1", + "uc.micro": "^1.0.5" + }, + "bin": { + "markdown-it": "bin/markdown-it.js" + } + }, + "node_modules/markdown-it/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdownlint": { + "version": "0.25.1", + "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.25.1.tgz", + "integrity": "sha512-AG7UkLzNa1fxiOv5B+owPsPhtM4D6DoODhsJgiaNg1xowXovrYgOnLqAgOOFQpWOlHFVQUzjMY5ypNNTeov92g==", + "dev": true, + "dependencies": { + "markdown-it": "12.3.2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli": { + "version": "0.31.1", + "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.31.1.tgz", + "integrity": "sha512-keIOMwQn+Ch7MoBwA+TdkyVMuxAeZFEGmIIlvwgV0Z1TGS5MxPnRr29XCLhkNzCHU+uNKGjU+VEjLX+Z9kli6g==", + "dev": true, + "dependencies": { + "commander": "~9.0.0", + "get-stdin": "~9.0.0", + "glob": "~7.2.0", + "ignore": "~5.2.0", + "js-yaml": "^4.1.0", + "jsonc-parser": "~3.0.0", + "markdownlint": "~0.25.1", + "markdownlint-rule-helpers": "~0.16.0", + "minimatch": "~3.0.5", + "run-con": "~1.2.10" + }, + "bin": { + "markdownlint": "markdownlint.js" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/markdownlint-cli/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/markdownlint-cli/node_modules/commander": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-9.0.0.tgz", + "integrity": "sha512-JJfP2saEKbQqvW+FI93OYUB4ByV5cizMpFMiiJI8xDbBvQvSkIk0VvQdn1CZ8mqAO8Loq2h0gYTYtDFUZUeERw==", + "dev": true, + "engines": { + "node": "^12.20.0 || >=14" + } + }, + "node_modules/markdownlint-cli/node_modules/get-stdin": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-9.0.0.tgz", + "integrity": "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/markdownlint-cli/node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/markdownlint-cli/node_modules/glob/node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/markdownlint-cli/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/markdownlint-cli/node_modules/minimatch": { + "version": "3.0.8", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.8.tgz", + "integrity": "sha512-6FsRAQsxQ61mw+qP1ZzbL9Bc78x2p5OqNgNpnoAFLTrX8n5Kxph0CsnhmKKNXTWjXqU5L0pGPR7hYk+XWZr60Q==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/markdownlint-rule-helpers": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.16.0.tgz", + "integrity": "sha512-oEacRUVeTJ5D5hW1UYd2qExYI0oELdYK72k1TKGvIeYJIbqQWAz476NAc7LNixSySUhcNl++d02DvX0ccDk9/w==", + "dev": true + }, + "node_modules/marky": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/marky/-/marky-1.2.5.tgz", + "integrity": "sha512-q9JtQJKjpsVxCRVgQ+WapguSbKC3SQ5HEzFGPAJMStgh3QjCawp00UKv3MTTAArTmGmmPUvllHZoNbZ3gs0I+Q==", + "dev": true + }, + "node_modules/masonry-layout": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/masonry-layout/-/masonry-layout-4.2.2.tgz", + "integrity": "sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==", + "dependencies": { + "get-size": "^2.0.2", + "outlayer": "^2.1.0" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mathml-tag-names": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", + "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/maxmin": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-3.0.0.tgz", + "integrity": "sha512-wcahMInmGtg/7c6a75fr21Ch/Ks1Tb+Jtoan5Ft4bAI0ZvJqyOw8kkM7e7p8hDSzY805vmxwHT50KcjGwKyJ0g==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "figures": "^3.2.0", + "gzip-size": "^5.1.1", + "pretty-bytes": "^5.3.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/maxmin/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/maxmin/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/maxmin/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/maxmin/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/maxmin/node_modules/figures": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", + "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", + "dev": true, + "dependencies": { + "escape-string-regexp": "^1.0.5" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/maxmin/node_modules/gzip-size": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-5.1.1.tgz", + "integrity": "sha512-FNHi6mmoHvs1mxZAds4PpdCS6QG8B4C1krxJsMutgxl5t3+GlRTzzI3NEkifXx2pVsOvJdOGSmIgDhQ55FwdPA==", + "dev": true, + "dependencies": { + "duplexer": "^0.1.1", + "pify": "^4.0.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/maxmin/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/maxmin/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/mdn-data": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", + "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", + "dev": true, + "optional": true + }, + "node_modules/mdurl": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", + "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", + "dev": true + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memfs": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", + "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", + "dev": true, + "dependencies": { + "fs-monkey": "^1.0.4" + }, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/memize": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/memize/-/memize-2.1.1.tgz", + "integrity": "sha512-8Nl+i9S5D6KXnruM03Jgjb+LwSupvR13WBr4hJegaaEyobvowCVupi79y2WSiWvO1mzBWxPwEYE5feCe8vyA5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/meow": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", + "integrity": "sha512-TNdwZs0skRlpPpCUK25StC4VH+tP5GgeY1HQOOGP+lQ2xtdkN2VtT/5tiX9k3IWpkBPV9b3LsAWXn4GGi/PrSA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "camelcase-keys": "^2.0.0", + "decamelize": "^1.1.2", + "loud-rejection": "^1.0.0", + "map-obj": "^1.0.1", + "minimist": "^1.1.3", + "normalize-package-data": "^2.3.4", + "object-assign": "^4.0.1", + "read-pkg-up": "^1.0.1", + "redent": "^1.0.0", + "trim-newlines": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-deep": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", + "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "dev": true, + "dependencies": { + "arr-union": "^3.1.0", + "clone-deep": "^0.2.4", + "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/merge-descriptors": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", + "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true + }, + "node_modules/merge2": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", + "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/metaviewport-parser": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/metaviewport-parser/-/metaviewport-parser-0.3.0.tgz", + "integrity": "sha512-EoYJ8xfjQ6kpe9VbVHvZTZHiOl4HL1Z18CrZ+qahvLXT7ZO4YTC2JMyt5FaUp9JJp6J4Ybb/z7IsCXZt86/QkQ==", + "dev": true + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/micromatch": { + "version": "3.1.10", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", + "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "braces": "^2.3.1", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "extglob": "^2.0.4", + "fragment-cache": "^0.2.1", + "kind-of": "^6.0.2", + "nanomatch": "^1.2.9", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/micromatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "dev": true, + "license": "MIT", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mimic-fn": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", + "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/mimic-response": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", + "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/min-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", + "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/mini-css-extract-plugin": { + "version": "2.9.2", + "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", + "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", + "dev": true, + "dependencies": { + "schema-utils": "^4.0.0", + "tapable": "^2.2.1" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/mini-css-extract-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/mini-css-extract-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", + "dev": true + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/minimist-options": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", + "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "dev": true, + "dependencies": { + "arrify": "^1.0.1", + "is-plain-obj": "^1.1.0", + "kind-of": "^6.0.3" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/minimist-options/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "license": "ISC", + "engines": { + "node": ">=8" + } + }, + "node_modules/mitt": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mitt/-/mitt-3.0.1.tgz", + "integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==", + "dev": true + }, + "node_modules/mixin-deep": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", + "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "dev": true, + "dependencies": { + "for-in": "^1.0.2", + "is-extendable": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-extendable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", + "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-deep/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", + "integrity": "sha512-ALGF1Jt9ouehcaXaHhn6t1yGWRqGaHkPFndtFVHfZXOvkIZ/yoGaSi0AHVTafb3ZBGg4dr/bDwnaEKqCXzchMA==", + "dev": true, + "dependencies": { + "for-in": "^0.1.3", + "is-extendable": "^0.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mixin-object/node_modules/for-in": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", + "integrity": "sha512-F0to7vbBSHP8E3l6dCjxNOLuSFAACIxFy3UehTUlG7svlXi37HHsDkyVcHo0Pq8QwrE+pXvWSVX3ZT1T9wAZ9g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mnemonist": { + "version": "0.39.5", + "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.39.5.tgz", + "integrity": "sha512-FPUtkhtJ0efmEFGpU14x7jGbTB+s18LrzRL2KgoWz9YvcY3cPomz8tih01GbHwnGk/OmkOKfqd/RAQoc8Lm7DQ==", + "dependencies": { + "obliterator": "^2.0.1" + } + }, + "node_modules/moment": { + "version": "2.30.1", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.30.1.tgz", + "integrity": "sha512-uEmtNhbDOrWPFS+hdjFCBfy9f2YoyzRpwcl+DqpC6taX21FzsTLQVbMV/W7PzNSX6x/bhC1zA3c2UQ5NzH6how==", + "engines": { + "node": "*" + } + }, + "node_modules/mrmime": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", + "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/multicast-dns": { + "version": "7.2.5", + "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", + "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", + "dev": true, + "dependencies": { + "dns-packet": "^5.2.2", + "thunky": "^1.0.2" + }, + "bin": { + "multicast-dns": "cli.js" + } + }, + "node_modules/mute-stream": { + "version": "0.0.7", + "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", + "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", + "dev": true + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/nanomatch": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", + "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "dev": true, + "dependencies": { + "arr-diff": "^4.0.0", + "array-unique": "^0.3.2", + "define-property": "^2.0.2", + "extend-shallow": "^3.0.2", + "fragment-cache": "^0.2.1", + "is-windows": "^1.0.2", + "kind-of": "^6.0.2", + "object.pick": "^1.3.0", + "regex-not": "^1.0.0", + "snapdragon": "^0.8.1", + "to-regex": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nanomatch/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/napi-postinstall": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/napi-postinstall/-/napi-postinstall-0.3.4.tgz", + "integrity": "sha512-PHI5f1O0EP5xJ9gQmFGMS6IZcrVvTjpXjz7Na41gTE7eE2hK11lg04CECCYEEjdc17EV4DO+fkGEtt7TpTaTiQ==", + "dev": true, + "license": "MIT", + "bin": { + "napi-postinstall": "lib/cli.js" + }, + "engines": { + "node": "^12.20.0 || ^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/napi-postinstall" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true + }, + "node_modules/netmask": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.0.2.tgz", + "integrity": "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/next-applause": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/next-applause/-/next-applause-2.2.4.tgz", + "integrity": "sha512-ktqjWT512q6vzAYnmRfJcqqVCA7ft8VcqkfBzgWuqI9SDSHM//B+hvjrGlkNzOzDMzljc3flok01t79OGkRVXQ==", + "dev": true, + "dependencies": { + "cson-parser": "^1.2.0", + "js-yaml": "^3.3.0", + "lodash": "^4.17.11" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/nice-try": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", + "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", + "dev": true, + "optional": true + }, + "node_modules/nise": { + "version": "5.1.9", + "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.9.tgz", + "integrity": "sha512-qOnoujW4SV6e40dYxJOb3uvuoPHtmLzIk4TFo+j0jPJoC+5Z9xja5qH5JZobEPsa8+YYphMrOSwnrshEhG2qww==", + "dev": true, + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^11.2.2", + "@sinonjs/text-encoding": "^0.7.2", + "just-extend": "^6.2.0", + "path-to-regexp": "^6.2.1" + } + }, + "node_modules/nise/node_modules/@sinonjs/fake-timers": { + "version": "11.3.1", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-11.3.1.tgz", + "integrity": "sha512-EVJO7nW5M/F5Tur0Rf2z/QoMo+1Ia963RiMtapiQrEWvY0iBUvADo8Beegwjpnle5BHkyHuoxSTW3jF43H1XRA==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@sinonjs/commons": "^3.0.1" + } + }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-addon-api": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", + "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", + "dev": true, + "license": "MIT", + "optional": true + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-exports-info": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/node-exports-info/-/node-exports-info-1.6.0.tgz", + "integrity": "sha512-pyFS63ptit/P5WqUkt+UUfe+4oevH+bFeIiPPdfb0pFeYEu/1ELnJu5l+5EcTKYL5M7zaAa7S8ddywgXypqKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "array.prototype.flatmap": "^1.3.3", + "es-errors": "^1.3.0", + "object.entries": "^1.1.9", + "semver": "^6.3.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/node-exports-info/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/node-forge": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.3.tgz", + "integrity": "sha512-rLvcdSyRCyouf6jcOIPe/BgwG/d7hKjzMKOas33/pHEr6gbq18IK9zV7DiPvzsz0oBJPme6qr6H6kGZuI9/DZg==", + "dev": true, + "license": "(BSD-3-Clause OR GPL-2.0)", + "engines": { + "node": ">= 6.13.0" + } + }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "dev": true + }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-sarif-builder": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/node-sarif-builder/-/node-sarif-builder-3.4.0.tgz", + "integrity": "sha512-tGnJW6OKRii9u/b2WiUViTJS+h7Apxx17qsMUjsUeNDiMMX5ZFf8F8Fcz7PAQ6omvOxHZtvDTmOYKJQwmfpjeg==", + "license": "MIT", + "dependencies": { + "@types/sarif": "^2.1.7", + "fs-extra": "^11.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/node-sarif-builder/node_modules/fs-extra": { + "version": "11.3.4", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", + "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^6.0.1", + "universalify": "^2.0.0" + }, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/node-sarif-builder/node_modules/jsonfile": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", + "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", + "license": "MIT", + "dependencies": { + "universalify": "^2.0.0" + }, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/node-sarif-builder/node_modules/universalify": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", + "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", + "license": "MIT", + "engines": { + "node": ">= 10.0.0" + } + }, + "node_modules/node-watch": { + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.3.tgz", + "integrity": "sha512-3l4E8uMPY1HdMMryPRUAl+oIHtXtyiTlIiESNSVSNxcPfzAFzeTbXFQkZfAwBbo0B1qMSG8nUABx+Gd+YrbKrQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/nomnom": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/nomnom/-/nomnom-1.8.1.tgz", + "integrity": "sha512-5s0JxqhDx9/rksG2BTMVN1enjWSvPidpoSgViZU4ZXULyTe+7jxcCRLB6f42Z0l1xYJpleCBtSyY6Lwg3uu5CQ==", + "deprecated": "Package no longer supported. Contact support@npmjs.com for more info.", + "dependencies": { + "chalk": "~0.4.0", + "underscore": "~1.6.0" + } + }, + "node_modules/nomnom/node_modules/ansi-styles": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-1.0.0.tgz", + "integrity": "sha512-3iF4FIKdxaVYT3JqQuY3Wat/T2t7TRbbQ94Fu50ZUCbLy4TFbTzr90NOHQodQkNqmeEGCw8WbeP78WNi6SKYUA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nomnom/node_modules/chalk": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-0.4.0.tgz", + "integrity": "sha512-sQfYDlfv2DGVtjdoQqxS0cEZDroyG8h6TamA6rvxwlrU5BaSLDx9xhatBYl2pxZ7gmpNaPFVwBtdGdu5rQ+tYQ==", + "license": "MIT", + "dependencies": { + "ansi-styles": "~1.0.0", + "has-color": "~0.1.0", + "strip-ansi": "~0.1.0" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/nomnom/node_modules/underscore": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.6.0.tgz", + "integrity": "sha512-z4o1fvKUojIWh9XuaVLUDdf86RQiq13AC1dmHbTpoyuu+bquHms76v16CjycCbec87J7z0k//SiQVk0sMdFmpQ==" + }, + "node_modules/nopt": { + "version": "3.0.6", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", + "integrity": "sha512-4GUt3kSEYmk4ITxzB/b9vaIDfUVWN/Ml1Fwl11IlnIG2iaJ9O6WXZ9SrYM9NLI8OCBieN2Y8SWC2oJV0RQ7qYg==", + "dev": true, + "license": "ISC", + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + } + }, + "node_modules/normalize-package-data": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", + "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^2.1.4", + "resolve": "^1.10.0", + "semver": "2 || 3 || 4 || 5", + "validate-npm-package-license": "^3.0.1" + } + }, + "node_modules/normalize-package-data/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", + "dev": true, + "bin": { + "semver": "bin/semver" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/npm-bundled": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-1.1.2.tgz", + "integrity": "sha512-x5DHup0SuyQcmL3s7Rx/YQ8sbw/Hzg0rj48eN0dV7hf5cmQq5PXIeioroH3raV1QC1yh3uTYuMThvEQF3iKgGQ==", + "dev": true, + "dependencies": { + "npm-normalize-package-bin": "^1.0.1" + } + }, + "node_modules/npm-conf": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", + "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "dev": true, + "optional": true, + "dependencies": { + "config-chain": "^1.1.11", + "pify": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-conf/node_modules/pify": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", + "integrity": "sha512-C3FsVNH1udSEX48gGX1xfvwTWfsYWj5U+8/uK15BGzIGrKoUpghX8hWZwa/OFnakBiiVNmBvemTJR5mcy7iPcg==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/npm-normalize-package-bin": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-1.0.1.tgz", + "integrity": "sha512-EPfafl6JL5/rU+ot6P3gRSCpPDW5VmIzX959Ob1+ySFUuuYHWHekXpwdUZcKP5C+DS4GEtdJluwBjnsNDl+fSA==", + "dev": true + }, + "node_modules/npm-package-json-lint": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-6.4.0.tgz", + "integrity": "sha512-cuXAJJB1Rdqz0UO6w524matlBqDBjcNt7Ru+RDIu4y6RI1gVqiWBnylrK8sPRk81gGBA0X8hJbDXolVOoTc+sA==", + "dev": true, + "dependencies": { + "ajv": "^6.12.6", + "ajv-errors": "^1.0.1", + "chalk": "^4.1.2", + "cosmiconfig": "^8.0.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "ignore": "^5.2.0", + "is-plain-obj": "^3.0.0", + "jsonc-parser": "^3.2.0", + "log-symbols": "^4.1.0", + "meow": "^9.0.0", + "plur": "^4.0.0", + "semver": "^7.3.8", + "slash": "^3.0.0", + "strip-json-comments": "^3.1.1", + "type-fest": "^3.2.0", + "validate-npm-package-name": "^5.0.0" + }, + "bin": { + "npmPkgJsonLint": "dist/cli.js" + }, + "engines": { + "node": ">=14.0.0", + "npm": ">=6.0.0" + } + }, + "node_modules/npm-package-json-lint/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/npm-package-json-lint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/npm-package-json-lint/node_modules/camelcase-keys": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", + "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", + "dev": true, + "dependencies": { + "camelcase": "^5.3.1", + "map-obj": "^4.0.0", + "quick-lru": "^4.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/npm-package-json-lint/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/npm-package-json-lint/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/npm-package-json-lint/node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/npm-package-json-lint/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/hosted-git-info": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.1.0.tgz", + "integrity": "sha512-kyCuEOWjJqZuDbRHzL8V93NzQhwIB71oFWSyzVo+KPZI+pnQPPxucdkrOZvkLRnrf5URsQM+IJ09Dw29cRALIA==", + "dev": true, + "dependencies": { + "lru-cache": "^6.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/indent-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", + "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/is-plain-obj": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", + "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/npm-package-json-lint/node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/npm-package-json-lint/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/map-obj": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", + "integrity": "sha512-hdN1wVrZbb29eBGiGjJbeP8JbKjq1urkHJ/LIP/NY48MZ1QVXUsQBV1G1zvYFHn1XE06cwjBsOI2K3Ulnj1YXQ==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/meow": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", + "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", + "dev": true, + "dependencies": { + "@types/minimist": "^1.2.0", + "camelcase-keys": "^6.2.2", + "decamelize": "^1.2.0", + "decamelize-keys": "^1.1.0", + "hard-rejection": "^2.1.0", + "minimist-options": "4.1.0", + "normalize-package-data": "^3.0.0", + "read-pkg-up": "^7.0.1", + "redent": "^3.0.0", + "trim-newlines": "^3.0.0", + "type-fest": "^0.18.0", + "yargs-parser": "^20.2.3" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/meow/node_modules/type-fest": { + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", + "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/normalize-package-data": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", + "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", + "dev": true, + "dependencies": { + "hosted-git-info": "^4.0.1", + "is-core-module": "^2.5.0", + "semver": "^7.3.4", + "validate-npm-package-license": "^3.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-package-json-lint/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg-up": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", + "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", + "dev": true, + "dependencies": { + "find-up": "^4.1.0", + "read-pkg": "^5.2.0", + "type-fest": "^0.8.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/read-pkg-up/node_modules/type-fest": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", + "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/redent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", + "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", + "dev": true, + "dependencies": { + "indent-string": "^4.0.0", + "strip-indent": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/strip-indent": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", + "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", + "dev": true, + "dependencies": { + "min-indent": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/trim-newlines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", + "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/npm-package-json-lint/node_modules/type-fest": { + "version": "3.13.1", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-3.13.1.tgz", + "integrity": "sha512-tLq3bSNx+xSpwvAJnzrK0Ep5CLNWjvFTOp71URMaAEWBfRb9nnJiBoUe0tF8bI4ZFO3omgBR6NvnbzVUT3Ly4g==", + "dev": true, + "engines": { + "node": ">=14.16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/npm-package-json-lint/node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-packlist": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-3.0.0.tgz", + "integrity": "sha512-L/cbzmutAwII5glUcf2DBRNY/d0TFd4e/FnaZigJV6JD85RHZXJFGwCndjMWiiViiWSsWt3tiOLpI3ByTnIdFQ==", + "dev": true, + "dependencies": { + "glob": "^7.1.6", + "ignore-walk": "^4.0.1", + "npm-bundled": "^1.1.1", + "npm-normalize-package-bin": "^1.0.1" + }, + "bin": { + "npm-packlist": "bin/index.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/npm-run-path": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", + "integrity": "sha512-lJxZYlT4DW/bRUtFh1MQIWqmLwQfAxnqWG4HhEdjMlkrJYnJn0Jrr2u3mgxqaWsdiBc76TYkTG/mhrnYTuzfHw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "path-key": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/nth-check": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", + "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "dev": true, + "optional": true, + "dependencies": { + "boolbase": "~1.0.0" + } + }, + "node_modules/nwsapi": { + "version": "2.2.23", + "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.23.tgz", + "integrity": "sha512-7wfH4sLbt4M0gCDzGE6vzQBo0bfTKjU7Sfpqy/7gs1qBfYz2vEJH6vXcBKpO3+6Yu1telwd0t9HpyOoLEQQbIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/oauth-sign": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", + "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", + "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "dev": true, + "dependencies": { + "copy-descriptor": "^0.1.0", + "define-property": "^0.2.5", + "kind-of": "^3.0.3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-copy/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-filter": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", + "integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=", + "dev": true + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object-visit": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", + "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "dev": true, + "dependencies": { + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.defaults": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", + "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "dev": true, + "dependencies": { + "array-each": "^1.0.1", + "array-slice": "^1.0.0", + "for-own": "^1.0.0", + "isobject": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.defaults/node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.entries": { + "version": "1.1.9", + "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.9.tgz", + "integrity": "sha512-8u/hfXFRBD1O0hPUjioLhoWFHRmt6tKA4/vZPyckBr18l1KE9uHrFaFaUi8MDRTpi4uak2goyPTSNJLXX2k2Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.fromentries": { + "version": "2.0.8", + "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.8.tgz", + "integrity": "sha512-k6E21FzySsSK5a21KRADBd/NGneRegFO5pLHfdQLpRDETUNJueLXs3WCzyQ3tFRDYgbq3KHGXfTbi2bs8WQ6rQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.getownpropertydescriptors": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", + "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "dev": true, + "optional": true, + "dependencies": { + "call-bind": "^1.0.0", + "define-properties": "^1.1.3", + "es-abstract": "^1.18.0-next.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object.groupby": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.3.tgz", + "integrity": "sha512-+Lhy3TQTuzXI5hevh8sBGqbmurHbbIjAi0Z4S63nthVLmLxfbj4T54a4CfZrXIrt9iP4mVAPYMo/v99taj3wjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", + "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "dev": true, + "dependencies": { + "for-own": "^1.0.0", + "make-iterator": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.map/node_modules/for-own": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", + "integrity": "sha512-0OABksIGrxKK8K4kynWkQ7y1zounQxP+CWnyclVwj81KW3vlLlGUx57DKGcP/LH216GzqnstnPocF16Nxs0Ycg==", + "dev": true, + "license": "MIT", + "dependencies": { + "for-in": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.pick": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", + "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object.values": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.2.1.tgz", + "integrity": "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/objectFitPolyfill": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/objectFitPolyfill/-/objectFitPolyfill-2.3.5.tgz", + "integrity": "sha512-8Quz071ZmGi0QWEG4xB3Bv5Lpw6K0Uca87FLoLMKMWjB6qIq9IyBegP3b/VLNxv2WYvIMGoeUQ+c6ibUkNa8TA==" + }, + "node_modules/obliterator": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-2.0.4.tgz", + "integrity": "sha512-lgHwxlxV1qIg1Eap7LgIeoBWIMFibOjbrYPIPJZcI1mmGAI2m3lNYpK12Y+GBdPQ0U1hRwSord7GIaawz962qQ==" + }, + "node_modules/obuf": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", + "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", + "dev": true + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", + "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/onetime": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", + "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "dev": true, + "dependencies": { + "mimic-fn": "^2.1.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/open": { + "version": "8.4.2", + "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", + "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", + "dev": true, + "dependencies": { + "define-lazy-prop": "^2.0.0", + "is-docker": "^2.1.1", + "is-wsl": "^2.2.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/opener": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", + "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", + "dev": true, + "bin": { + "opener": "bin/opener-bin.js" + } + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/optipng-bin": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-5.1.0.tgz", + "integrity": "sha512-9baoqZTNNmXQjq/PQTWEXbVV3AMO2sI/GaaqZJZ8SExfAzjijeAP7FEeT+TtyumSw7gr0PZtSUYB/Ke7iHQVKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "bin-build": "^3.0.0", + "bin-wrapper": "^4.0.0", + "logalot": "^2.0.0" + }, + "bin": { + "optipng": "cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/os-filter-obj": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", + "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "dev": true, + "optional": true, + "dependencies": { + "arch": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/os-homedir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", + "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/os-tmpdir": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", + "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/osenv": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", + "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "dev": true, + "dependencies": { + "os-homedir": "^1.0.0", + "os-tmpdir": "^1.0.0" + } + }, + "node_modules/outlayer": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/outlayer/-/outlayer-2.1.1.tgz", + "integrity": "sha1-KYY7beEOpdrf/8rfoNcokHOH6aI=", + "dependencies": { + "ev-emitter": "^1.0.0", + "fizzy-ui-utils": "^2.0.0", + "get-size": "^2.0.2" + } + }, + "node_modules/own-keys": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/own-keys/-/own-keys-1.0.1.tgz", + "integrity": "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg==", + "dev": true, + "dependencies": { + "get-intrinsic": "^1.2.6", + "object-keys": "^1.1.1", + "safe-push-apply": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/p-cancelable": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", + "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-event": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", + "integrity": "sha512-hV1zbA7gwqPVFcapfeATaNjQ3J0NuzorHPyG8GPL9g/Y/TplWVBVoCKCXL6Ej2zscrCEv195QNWJXuBH6XZuzA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "p-timeout": "^1.1.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-finally": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", + "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-is-promise": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", + "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate/node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-map-series": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", + "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "dev": true, + "optional": true, + "dependencies": { + "p-reduce": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-pipe": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", + "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-reduce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", + "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "dev": true, + "optional": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-timeout": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", + "integrity": "sha512-gb0ryzr+K2qFqFv6qi3khoeqMZF/+ajxQipEF6NteZVnvz9tzdsfAVj3lYtn1gAXvH5lfLwfxEII799gt/mRIA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.1.0.tgz", + "integrity": "sha512-Z5FnLVVZSnX7WjBg0mhDtydeRZ1xMcATZThjySQUHqr+0ksP8kqaw23fNKkaaN/Z8gwLUs/W7xdl0I75eP2Xyw==", + "dev": true, + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/param-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", + "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse-cache-control": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parse-cache-control/-/parse-cache-control-1.0.1.tgz", + "integrity": "sha512-60zvsJReQPX5/QP0Kzfd/VrpjScIQ7SHBW6bFCYfEP+fp0Eppr1SHhIO5nd1PjZtvclzSzES9D/p5nFJurwfWg==", + "dev": true + }, + "node_modules/parse-filepath": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", + "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "dev": true, + "dependencies": { + "is-absolute": "^1.0.0", + "map-cache": "^0.2.0", + "path-root": "^0.1.1" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parse-passwd": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", + "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", "dev": true, - "requires": { - "chalk": "^4.1.0", - "is-unicode-supported": "^0.1.0" + "engines": { + "node": ">=0.10.0" } }, - "log4js": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.3.0.tgz", - "integrity": "sha512-Mc8jNuSFImQUIateBFwdOQcmC6Q5maU0VVvdC2R6XMb66/VnT+7WS4D/0EeNMZu1YODmJe5NIn2XftCzEocUgw==", - "requires": { - "date-format": "^3.0.0", - "debug": "^4.1.1", - "flatted": "^2.0.1", - "rfdc": "^1.1.4", - "streamroller": "^2.2.4" - }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "license": "MIT", "dependencies": { - "flatted": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-2.0.2.tgz", - "integrity": "sha512-r5wGx7YeOwNWNlCA0wQ86zKyDLMQr+/RB8xy74M4hTphfmjlijTSSXGuH8rnvKZnfT9i+75zmd8jcKdMR4O6jA==" - } + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "logalot": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/logalot/-/logalot-2.1.0.tgz", - "integrity": "sha1-X46MkNME7fElMJUaVVSruMXj9VI=", + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "optional": true, - "requires": { - "figures": "^1.3.5", - "squeak": "^1.0.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "loglevel": { - "version": "1.7.1", - "resolved": "https://registry.npmjs.org/loglevel/-/loglevel-1.7.1.tgz", - "integrity": "sha512-Hesni4s5UkWkwCGJMQGAh71PaLUmKFM60dHvq0zi/vDhhrzuk+4GgNbTXJ12YYQJn6ZKBDNIjYcuQGKudvqrIw==", - "dev": true - }, - "longest": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/longest/-/longest-1.0.1.tgz", - "integrity": "sha1-MKCy2jj3N3DoKUoNIuZiXtd9AJc=" - }, - "longest-streak": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-2.0.4.tgz", - "integrity": "sha512-vM6rUVCVUJJt33bnmHiZEvr7wPT78ztX7rojL+LW51bHtLh6HTjx84LA5W4+oa6aKEJA7jJu5LR6vQRBpA5DVg==", - "dev": true + "node_modules/parserlib": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/parserlib/-/parserlib-1.1.1.tgz", + "integrity": "sha512-e1HbF3+7ASJ/uOZirg5/8ZfPljTh100auNterbHB8TUs5egciuWQ2eX/2al8ko0RdV9Xh/5jDei3jqJAmbTDcg==", + "license": "MIT" }, - "loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "requires": { - "js-tokens": "^3.0.0 || ^4.0.0" + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" } }, - "loud-rejection": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/loud-rejection/-/loud-rejection-1.6.0.tgz", - "integrity": "sha1-W0b4AUft7leIcPCG0Eghz5mOVR8=", + "node_modules/pascal-case": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", + "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", "dev": true, - "optional": true, - "requires": { - "currently-unhandled": "^0.4.1", - "signal-exit": "^3.0.0" + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "lowercase-keys": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-1.0.1.tgz", - "integrity": "sha512-G2Lj61tXDnVFFOi8VZds+SoQjtQC3dgokKdDG2mTm1tx4m50NUHBOZSBwQQHyy0V12A0JTG4icfZQH+xPyh8VA==", + "node_modules/pascalcase": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", + "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", "dev": true, - "optional": true + "engines": { + "node": ">=0.10.0" + } }, - "lpad-align": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/lpad-align/-/lpad-align-1.1.2.tgz", - "integrity": "sha1-IfYArBwwlcPG5JfuZyce4ISB/p4=", + "node_modules/path-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/path-case/-/path-case-3.0.4.tgz", + "integrity": "sha512-qO4qCFjXqVTrcbPt/hQfhTQ+VhFsqNKOPtytgNKkKxSoEp3XPUQ8ObFuePylOIok5gjn69ry8XiULxCwot3Wfg==", "dev": true, - "optional": true, - "requires": { - "get-stdin": "^4.0.1", - "indent-string": "^2.1.0", - "longest": "^1.0.0", - "meow": "^3.3.0" - }, "dependencies": { - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true - } + "dot-case": "^3.0.4", + "tslib": "^2.0.3" } }, - "lru-cache": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", - "integrity": "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==", - "requires": { - "yallist": "^4.0.0" - } + "node_modules/path-dirname": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", + "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", + "dev": true }, - "make-dir": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-2.1.0.tgz", - "integrity": "sha512-LS9X+dc8KLxXCb8dni79fLIIUA5VyZoyjSMCwTluaXA0o27cCK0bhXkpgw+sTXVpPy/lSO57ilRixqk0vDmtRA==", + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", "dev": true, - "requires": { - "pify": "^4.0.1", - "semver": "^5.6.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=8" } }, - "make-iterator": { + "node_modules/path-is-absolute": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/make-iterator/-/make-iterator-1.0.1.tgz", - "integrity": "sha512-pxiuXh0iVEq7VM7KMIhs5gxsfxCux2URptUQaXo4iZZJxBAzTPOLE2BumO5dbfVYq/hBJFBR/a1mFDmOx5AGmw==", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", "dev": true, - "requires": { - "kind-of": "^6.0.2" - }, - "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "engines": { + "node": ">=0.10.0" } }, - "makeerror": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/makeerror/-/makeerror-1.0.11.tgz", - "integrity": "sha1-4BpckQnyr3lmDk6LlYd5AYT1qWw=", + "node_modules/path-key": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", + "integrity": "sha512-fEHGKCSmUSDPv4uoj8AlD+joPlq3peND+HRYyxFz4KPw4z926S/b8rIuFs2FYJg3BwsxJf6A9/3eIdLaYC+9Dw==", "dev": true, - "requires": { - "tmpl": "1.0.x" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=4" } }, - "map-cache": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", - "integrity": "sha1-wyq9C9ZSXZsFFkW7TyasXcmKDb8=", - "dev": true - }, - "map-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-1.0.1.tgz", - "integrity": "sha1-2TPOuSBdgr3PSIb2dCvcK03qFG0=", - "dev": true - }, - "map-values": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/map-values/-/map-values-1.0.1.tgz", - "integrity": "sha1-douOecAJvytk/ugG4ip7HEGQyZA=", + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", "dev": true }, - "map-visit": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/map-visit/-/map-visit-1.0.0.tgz", - "integrity": "sha1-7Nyo8TFE5mDxtb1B8S80edmN+48=", + "node_modules/path-root": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", + "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", "dev": true, - "requires": { - "object-visit": "^1.0.0" + "dependencies": { + "path-root-regex": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "markdown-escapes": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/markdown-escapes/-/markdown-escapes-1.0.4.tgz", - "integrity": "sha512-8z4efJYk43E0upd0NbVXwgSTQs6cT3T06etieCMEg7dRbzCbxUCK/GHlX8mhHRDcp+OLlHkPKsvqQTCvsRl2cg==", - "dev": true - }, - "markdown-it": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-10.0.0.tgz", - "integrity": "sha512-YWOP1j7UbDNz+TumYP1kpwnP0aEa711cJjrAQrzd0UXlbJfc5aAq0F/PZHjiioqDC1NKgvIMX+o+9Bk7yuM2dg==", + "node_modules/path-root-regex": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", + "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", "dev": true, - "requires": { - "argparse": "^1.0.7", - "entities": "~2.0.0", - "linkify-it": "^2.0.0", - "mdurl": "^1.0.1", - "uc.micro": "^1.0.5" - }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "license": "BlueOak-1.0.0", "dependencies": { - "entities": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.0.3.tgz", - "integrity": "sha512-MyoZ0jgnLvB2X3Lg5HqpFmn1kybDiIfEQmKzTb5apr51Rb+T3KdmMiqa70T+bhGnyv7bQ6WMj2QMHpGMmlrUYQ==", - "dev": true - } + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "markdown-it-anchor": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/markdown-it-anchor/-/markdown-it-anchor-5.3.0.tgz", - "integrity": "sha512-/V1MnLL/rgJ3jkMWo84UR+K+jF1cxNG1a+KwqeXqTIJ+jtA8aWSHuigx8lTzauiIjBDbwF3NcWQMotd0Dm39jA==", - "dev": true + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "license": "ISC" }, - "markdownlint": { - "version": "0.18.0", - "resolved": "https://registry.npmjs.org/markdownlint/-/markdownlint-0.18.0.tgz", - "integrity": "sha512-nQAfK9Pbq0ZRoMC/abNGterEnV3kL8MZmi0WHhw8WJKoIbsm3cXGufGsxzCRvjW15cxe74KWcxRSKqwplS26Bw==", - "dev": true, - "requires": { - "markdown-it": "10.0.0" - } - }, - "markdownlint-cli": { - "version": "0.21.0", - "resolved": "https://registry.npmjs.org/markdownlint-cli/-/markdownlint-cli-0.21.0.tgz", - "integrity": "sha512-gvnczz3W3Wgex851/cIQ/2y8GNhY+EVK8Ael8kRd8hoSQ0ps9xjhtwPwMyJPoiYbAoPxG6vSBFISiysaAbCEZg==", - "dev": true, - "requires": { - "commander": "~2.9.0", - "deep-extend": "~0.5.1", - "get-stdin": "~5.0.1", - "glob": "~7.1.2", - "ignore": "~5.1.4", - "js-yaml": "~3.13.1", - "jsonc-parser": "~2.2.0", - "lodash.differencewith": "~4.5.0", - "lodash.flatten": "~4.4.0", - "markdownlint": "~0.18.0", - "markdownlint-rule-helpers": "~0.6.0", - "minimatch": "~3.0.4", - "rc": "~1.2.7" - }, - "dependencies": { - "commander": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz", - "integrity": "sha1-nJkJQXbhIkDLItbFFGCYQA/g99Q=", - "dev": true, - "requires": { - "graceful-readlink": ">= 1.0.0" - } - }, - "js-yaml": { - "version": "3.13.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.13.1.tgz", - "integrity": "sha512-YfbcO7jXDdyj0DGxYVSlSeQNHbD7XPWvrVWeVUujrQEoZzWJIRrCPoyk6kL6IAjAG2IolMK4T0hNUe0HOUs5Jw==", - "dev": true, - "requires": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - } - } + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" } }, - "markdownlint-rule-helpers": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/markdownlint-rule-helpers/-/markdownlint-rule-helpers-0.6.0.tgz", - "integrity": "sha512-LiZVAbg9/cqkBHtLNNqHV3xuy4Y2L/KuGU6+ZXqCT9NnCdEkIoxeI5/96t+ExquBY0iHy2CVWxPH16nG1RKQVQ==", + "node_modules/path-to-regexp": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-6.3.0.tgz", + "integrity": "sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", "dev": true }, - "marked": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/marked/-/marked-2.0.6.tgz", - "integrity": "sha512-S2mYj0FzTQa0dLddssqwRVW4EOJOVJ355Xm2Vcbm+LU7GQRGWvwbO5K87OaPSOux2AwTSgtPPaXmc8sDPrhn2A==", + "node_modules/performance-now": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", + "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", "dev": true }, - "masonry-layout": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/masonry-layout/-/masonry-layout-4.2.2.tgz", - "integrity": "sha512-iGtAlrpHNyxaR19CvKC3npnEcAwszXoyJiI8ARV2ePi7fmYhIud25MHK8Zx4P0LCC4d3TNO9+rFa1KoK1OEOaA==", - "requires": { - "get-size": "^2.0.2", - "outlayer": "^2.1.0" + "node_modules/php-array-reader": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/php-array-reader/-/php-array-reader-2.1.3.tgz", + "integrity": "sha512-FjgMmNfnbi76wsbzO/dWEeySt0WZpxv8q/7RH0XFPyNLxsfJSf97KKe/4Rgdmx/XRDGlbl8THU5ayKwGE3Xqrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "php-parser": "^3.1.5" } }, - "matchdep": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/matchdep/-/matchdep-2.0.0.tgz", - "integrity": "sha1-xvNINKDY28OzfCfui7yyfHd1WC4=", - "dev": true, - "requires": { - "findup-sync": "^2.0.0", - "micromatch": "^3.0.4", - "resolve": "^1.4.0", - "stack-trace": "0.0.10" - }, - "dependencies": { - "expand-tilde": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/expand-tilde/-/expand-tilde-2.0.2.tgz", - "integrity": "sha1-l+gBqgUt8CRU3kawK/YhZCzchQI=", - "dev": true, - "requires": { - "homedir-polyfill": "^1.0.1" - } - }, - "findup-sync": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/findup-sync/-/findup-sync-2.0.0.tgz", - "integrity": "sha1-kyaxSIwi0aYIhlCoaQGy2akKLLw=", - "dev": true, - "requires": { - "detect-file": "^1.0.0", - "is-glob": "^3.1.0", - "micromatch": "^3.0.4", - "resolve-dir": "^1.0.1" - } - }, - "global-modules": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-1.0.0.tgz", - "integrity": "sha512-sKzpEkf11GpOFuw0Zzjzmt4B4UZwjOcG757PPvrfhxcLFbq0wpsgpOqxpxtxFiCG4DtG93M6XRVbF2oGdev7bg==", - "dev": true, - "requires": { - "global-prefix": "^1.0.1", - "is-windows": "^1.0.1", - "resolve-dir": "^1.0.0" - } - }, - "global-prefix": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-1.0.2.tgz", - "integrity": "sha1-2/dDxsFJklk8ZVVoy2btMsASLr4=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.2", - "homedir-polyfill": "^1.0.1", - "ini": "^1.3.4", - "is-windows": "^1.0.1", - "which": "^1.2.14" - } - }, - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - }, - "resolve-dir": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-1.0.1.tgz", - "integrity": "sha1-eaQGRMNivoLybv/nOcm7U4IEb0M=", - "dev": true, - "requires": { - "expand-tilde": "^2.0.0", - "global-modules": "^1.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - } - } + "node_modules/php-parser": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/php-parser/-/php-parser-3.5.0.tgz", + "integrity": "sha512-EHdzSckQNP86jQRCEsMYhs+YzS4BfvfxnyhvzHVhVRoRUGEMFi8f3xKfuS9xdChBazZSyvb10SZbqhYQLGBcQg==", + "dev": true, + "license": "BSD-3-Clause" }, - "mathml-tag-names": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/mathml-tag-names/-/mathml-tag-names-2.1.3.tgz", - "integrity": "sha512-APMBEanjybaPzUrfqU0IMU5I0AswKMH7k8OTLs0vvV4KZpExkTkY87nR/zpbuTPj+gARop7aGUbl11pnDfW6xg==", - "dev": true + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" }, - "maxmin": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/maxmin/-/maxmin-2.1.0.tgz", - "integrity": "sha1-TTsiCQPZXu5+t6x/qGTnLcCaMWY=", + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", "dev": true, - "requires": { - "chalk": "^1.0.0", - "figures": "^1.0.1", - "gzip-size": "^3.0.0", - "pretty-bytes": "^3.0.0" + "engines": { + "node": ">=8.6" }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "gzip-size": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-3.0.0.tgz", - "integrity": "sha1-VGGI6b3DN/Zzdy+BZgRks4nc5SA=", - "dev": true, - "requires": { - "duplexer": "^0.1.1" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" } }, - "md5.js": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", - "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "node_modules/pify": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", + "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1", - "safe-buffer": "^5.1.2" - } - }, - "mdast-util-from-markdown": { - "version": "0.8.5", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-0.8.5.tgz", - "integrity": "sha512-2hkTXtYYnr+NubD/g6KGBS/0mFmBcifAsI0yIWRiRo0PjVs6SSOSOdtzbp6kSGnShDN6G5aWZpKQ2lWRy27mWQ==", - "dev": true, - "requires": { - "@types/mdast": "^3.0.0", - "mdast-util-to-string": "^2.0.0", - "micromark": "~2.11.0", - "parse-entities": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - }, - "dependencies": { - "parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - }, - "unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.2" - } - } + "engines": { + "node": ">=6" } }, - "mdast-util-to-markdown": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-0.6.5.tgz", - "integrity": "sha512-XeV9sDE7ZlOQvs45C9UKMtfTcctcaj/pGwH8YLbMHoMOXNNCn2LsqVQOqrF1+/NU8lKDAqozme9SCXWyo9oAcQ==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "longest-streak": "^2.0.0", - "mdast-util-to-string": "^2.0.0", - "parse-entities": "^2.0.0", - "repeat-string": "^1.0.0", - "zwitch": "^1.0.0" - }, - "dependencies": { - "parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } - } + "node_modules/pinkie": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", + "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "mdast-util-to-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-2.0.0.tgz", - "integrity": "sha512-AW4DRS3QbBayY/jJmD8437V1Gombjf8RSOUCMFBuo5iHi58AGEgVCKQ+ezHkZZDpAQS75hcBMpLqjpJTjtUL7w==", - "dev": true + "node_modules/pinkie-promise": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", + "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "dev": true, + "optional": true, + "dependencies": { + "pinkie": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "mdn-data": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.4.tgz", - "integrity": "sha512-iV3XNKw06j5Q7mi6h+9vbx23Tv7JkjEVgKHW4pimwyDGWm0OIQntJJ+u1C6mg6mK1EaTv42XQ7w76yuzH7M2cA==", - "dev": true + "node_modules/pirates": { + "version": "4.0.6", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.6.tgz", + "integrity": "sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==", + "dev": true, + "engines": { + "node": ">= 6" + } }, - "mdurl": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", - "integrity": "sha1-/oWy7HWlkDfyrf7BAP1sYBdhFS4=", - "dev": true + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=" + "node_modules/pkg-dir/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } }, - "memize": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/memize/-/memize-1.1.0.tgz", - "integrity": "sha512-K4FcPETOMTwe7KL2LK0orMhpOmWD2wRGwWWpbZy0fyArwsyIKR8YJVz8+efBAh3BO4zPqlSICu4vsLTRRqtFAg==" + "node_modules/pkg-dir/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } }, - "memory-fs": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/memory-fs/-/memory-fs-0.4.1.tgz", - "integrity": "sha1-OpoguEYlI+RHz7x+i7gO1me/xVI=", + "node_modules/pkg-dir/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", "dev": true, - "requires": { - "errno": "^0.1.3", - "readable-stream": "^2.0.1" + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" } }, - "meow": { - "version": "3.7.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-3.7.0.tgz", - "integrity": "sha1-cstmi0JSKCkKu/qFaJJYcwioAfs=", + "node_modules/playwright": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.58.2.tgz", + "integrity": "sha512-vA30H8Nvkq/cPBnNw4Q8TWz1EJyqgpuinBcHET0YVJVFldr8JDNiU9LaWAE1KqSkRYazuaBhTpB5ZzShOezQ6A==", "dev": true, - "optional": true, - "requires": { - "camelcase-keys": "^2.0.0", - "decamelize": "^1.1.2", - "loud-rejection": "^1.0.0", - "map-obj": "^1.0.1", - "minimist": "^1.1.3", - "normalize-package-data": "^2.3.4", - "object-assign": "^4.0.1", - "read-pkg-up": "^1.0.1", - "redent": "^1.0.0", - "trim-newlines": "^1.0.0" + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.58.2" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" } }, - "merge-deep": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/merge-deep/-/merge-deep-3.0.3.tgz", - "integrity": "sha512-qtmzAS6t6grwEkNrunqTBdn0qKwFgNWvlxUbAV8es9M7Ot1EbyApytCnvE0jALPa46ZpKDUo527kKiaWplmlFA==", + "node_modules/playwright-core": { + "version": "1.58.2", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.58.2.tgz", + "integrity": "sha512-yZkEtftgwS8CsfYo7nm0KE8jsvm6i/PTgVtB8DL726wNf6H2IMsDuxCpJj59KDaxCtSnrWan2AeDqM7JBaultg==", "dev": true, - "requires": { - "arr-union": "^3.1.0", - "clone-deep": "^0.2.4", - "kind-of": "^3.0.2" + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" } }, - "merge-descriptors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", - "integrity": "sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=", - "dev": true + "node_modules/plur": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", + "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "dev": true, + "dependencies": { + "irregular-plurals": "^3.2.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "dev": true + "node_modules/polyfill-library": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-4.8.0.tgz", + "integrity": "sha512-7+EjQmy3C7WJRaqCcVFXDE1DLBAIZ9es3DdvBKBxl98kn4kZCjuFKUi13uKDd618DVqZDkvqorP+pBFs7v5feQ==", + "dependencies": { + "@financial-times/polyfill-useragent-normaliser": "^2.0.1", + "from2-string": "^1.1.0", + "graceful-fs": "^4.2.10", + "merge2": "^1.0.3", + "mnemonist": "^0.39.2", + "stream-from-promise": "^1.0.0", + "stream-to-string": "^1.1.0", + "toposort": "^2.0.2" + }, + "engines": { + "node": ">=12" + } }, - "merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==" + "node_modules/posix-character-classes": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", + "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=", - "dev": true + "node_modules/possible-typed-array-names": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.0.0.tgz", + "integrity": "sha512-d7Uw+eZoloe0EHDIYoe+bQ5WXnGMOpmiZFTuMWCwpjzzkL2nTjcKiAk4hh8TjnGye2TwWOk3UXucZ+3rbmBa8Q==", + "dev": true, + "engines": { + "node": ">= 0.4" + } }, - "micromark": { - "version": "2.11.4", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-2.11.4.tgz", - "integrity": "sha512-+WoovN/ppKolQOFIAajxi7Lu9kInbPxFuTBVEavFcL8eAfVstoc5MocPmqBeAdBOJV00uaVjegzH4+MA0DN/uA==", - "dev": true, - "requires": { - "debug": "^4.0.0", - "parse-entities": "^2.0.0" - }, - "dependencies": { - "parse-entities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-2.0.0.tgz", - "integrity": "sha512-kkywGpCcRYhqQIchaWqZ875wzpS/bMKhz5HnN3p7wveJTkTtyAB/AlnS0f8DFSqYW1T82t6yEAkEcB+A1I3MbQ==", - "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" - } + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" } }, - "micromatch": { - "version": "3.1.10", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-3.1.10.tgz", - "integrity": "sha512-MWikgl9n9M3w+bpsY3He8L+w9eF9338xRl8IAO5viDizwSzziFEyUzo2xrrloB64ADbTf8uA8vRqqttDTOmccg==", + "node_modules/postcss-calc": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-10.1.1.tgz", + "integrity": "sha512-NYEsLHh8DgG/PRH2+G9BTuUdtf9ViS+vdoQ0YA5OQdGsfN4ztiwtDWNtBl9EKeqNMFnIu8IKZ0cLxEQ5r5KVMw==", "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "braces": "^2.3.1", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "extglob": "^2.0.4", - "fragment-cache": "^0.2.1", - "kind-of": "^6.0.2", - "nanomatch": "^1.2.9", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.2" - }, + "license": "MIT", "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12 || ^20.9 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.38" } }, - "micromodal": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/micromodal/-/micromodal-0.4.6.tgz", - "integrity": "sha512-2VDso2a22jWPpqwuWT/4RomVpoU3Bl9qF9D01xzwlNp5UVsImeA0gY4nSpF44vqcQtQOtkiMUV9EZkAJSRxBsg==" - }, - "miller-rabin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", - "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "node_modules/postcss-calc/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "requires": { - "bn.js": "^4.0.0", - "brorand": "^1.0.1" - }, + "license": "MIT", "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "mime": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/mime/-/mime-2.4.7.tgz", - "integrity": "sha512-dhNd1uA2u397uQk3Nv5LM4lm93WYDUXFn3Fu291FJerns4jyTudqhIWe4W04YLy7Uk1tm1Ore04NpjRvQp/NPA==" + "node_modules/postcss-colormin": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-7.0.6.tgz", + "integrity": "sha512-oXM2mdx6IBTRm39797QguYzVEWzbdlFiMNfq88fCCN1Wepw3CYmJ/1/Ifa/KjWo+j5ZURDl2NTldLJIw51IeNQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-api": "^3.0.0", + "colord": "^2.9.3", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "mime-db": { - "version": "1.45.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.45.0.tgz", - "integrity": "sha512-CkqLUxUk15hofLoLyljJSrukZi8mAtgd+yE5uO4tqRZsdsAJKv0O+rFMhVDRJgozy+yG6md5KwuXhD4ocIoP+w==" + "node_modules/postcss-convert-values": { + "version": "7.0.9", + "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-7.0.9.tgz", + "integrity": "sha512-l6uATQATZaCa0bckHV+r6dLXfWtUBKXxO3jK+AtxxJJtgMPD+VhhPCCx51I4/5w8U5uHV67g3w7PXj+V3wlMlg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "mime-types": { - "version": "2.1.28", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.28.tgz", - "integrity": "sha512-0TO2yJ5YHYr7M2zzT7gDU1tbwHxEUWBCLt0lscSNpcdAfFyJOVEpRYNS7EXVcTLNj/25QO8gulHC5JtTzSE2UQ==", - "requires": { - "mime-db": "1.45.0" + "node_modules/postcss-discard-comments": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-7.0.6.tgz", + "integrity": "sha512-Sq+Fzj1Eg5/CPf1ERb0wS1Im5cvE2gDXCE+si4HCn1sf+jpQZxDI4DXEp8t77B/ImzDceWE2ebJQFXdqZ6GRJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "dev": true + "node_modules/postcss-discard-comments/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } }, - "mimic-response": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-1.0.1.tgz", - "integrity": "sha512-j5EctnkH7amfV/q5Hgmoal1g2QHFJRraOtmx0JpIqkxhBhI/lJSl1nMpQ45hVarwNETOoWEimndZ4QK0RHxuxQ==", + "node_modules/postcss-discard-duplicates": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-7.0.2.tgz", + "integrity": "sha512-eTonaQvPZ/3i1ASDHOKkYwAybiM45zFIc7KXils4mQmHLqIswXD9XNOKEVxtTFnsmwYzF66u4LMgSr0abDlh5w==", "dev": true, - "optional": true + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "min-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz", - "integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==", - "dev": true + "node_modules/postcss-discard-empty": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-7.0.1.tgz", + "integrity": "sha512-cFrJKZvcg/uxB6Ijr4l6qmn3pXQBna9zyrPC+sK0zjbkDUZew+6xDltSF7OeB7rAtzaaMVYSdbod+sZOCWnMOg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "mini-css-extract-plugin": { - "version": "1.6.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-1.6.2.tgz", - "integrity": "sha512-WhDvO3SjGm40oV5y26GjMJYjd2UMqrLAGKy5YS2/3QKJy2F7jgynuHTir/tgUUOiNQu5saXHdc8reo7YuhhT4Q==", + "node_modules/postcss-discard-overridden": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-7.0.1.tgz", + "integrity": "sha512-7c3MMjjSZ/qYrx3uc1940GSOzN1Iqjtlqe8uoSg+qdVPYyRb0TILSqqmtlSFuE4mTDECwsm397Ya7iXGzfF7lg==", "dev": true, - "requires": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "webpack-sources": "^1.1.0" + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-import": { + "version": "16.1.1", + "resolved": "https://registry.npmjs.org/postcss-import/-/postcss-import-16.1.1.tgz", + "integrity": "sha512-2xVS1NCZAfjtVdvXiyegxzJ447GyqCeEI5V7ApgQVOWnros1p5lGNovJNapwPpMombyFBfqDwt7AD3n2l0KOfQ==", + "dev": true, + "license": "MIT", "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "postcss-value-parser": "^4.0.0", + "read-cache": "^1.0.0", + "resolve": "^1.1.7" + }, + "engines": { + "node": ">=18.0.0" + }, + "peerDependencies": { + "postcss": "^8.0.0" } }, - "minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "dev": true + "node_modules/postcss-loader": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-6.2.1.tgz", + "integrity": "sha512-WbbYpmAaKcux/P66bZ40bpWsBucjx/TTgVVzRZ9yUO8yQfVBlameJ0ZGVaPfH64hNSBh63a+ICP5nqOpBA0w+Q==", + "dev": true, + "dependencies": { + "cosmiconfig": "^7.0.0", + "klona": "^2.0.5", + "semver": "^7.3.5" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "postcss": "^7.0.0 || ^8.0.1", + "webpack": "^5.0.0" + } }, - "minimalistic-crypto-utils": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", - "integrity": "sha1-9sAMHAsIIkblxNmd+4x8CDsrWCo=", - "dev": true + "node_modules/postcss-media-query-parser": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", + "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", + "dev": true, + "license": "MIT" }, - "minimatch": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", - "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", - "requires": { - "brace-expansion": "^1.1.7" + "node_modules/postcss-merge-longhand": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-7.0.5.tgz", + "integrity": "sha512-Kpu5v4Ys6QI59FxmxtNB/iHUVDn9Y9sYw66D6+SZoIk4QTz1prC4aYkhIESu+ieG1iylod1f8MILMs1Em3mmIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "stylehacks": "^7.0.5" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "minimist": { - "version": "1.2.5", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", - "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", - "dev": true - }, - "minimist-options": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/minimist-options/-/minimist-options-4.1.0.tgz", - "integrity": "sha512-Q4r8ghd80yhO/0j1O3B2BjweX3fiHg9cdOwjJd2J76Q135c+NDxGCqdYKQ1SKBuFfgWbAUzBfvYjPUEeNgqN1A==", + "node_modules/postcss-merge-rules": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-7.0.8.tgz", + "integrity": "sha512-BOR1iAM8jnr7zoQSlpeBmCsWV5Uudi/+5j7k05D0O/WP3+OFMPD86c1j/20xiuRtyt45bhxw/7hnhZNhW2mNFA==", "dev": true, - "requires": { - "arrify": "^1.0.1", - "is-plain-obj": "^1.1.0", - "kind-of": "^6.0.3" + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-api": "^3.0.0", + "cssnano-utils": "^5.0.1", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-merge-rules/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "minipass": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.1.3.tgz", - "integrity": "sha512-Mgd2GdMVzY+x3IJ+oHnVM+KG3lA5c8tnabyJKmHSaG2kAGpudxuOf8ToDkhumF7UzME7DecbQE9uOZhNm7PuJg==", + "node_modules/postcss-minify-font-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-7.0.1.tgz", + "integrity": "sha512-2m1uiuJeTplll+tq4ENOQSzB8LRnSUChBv7oSyFLsJRtUgAAJGP6LLz0/8lkinTgxrmJSPOEhgY1bMXOQ4ZXhQ==", "dev": true, - "requires": { - "yallist": "^4.0.0" + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "minipass-collect": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-1.0.2.tgz", - "integrity": "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA==", + "node_modules/postcss-minify-gradients": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-7.0.1.tgz", + "integrity": "sha512-X9JjaysZJwlqNkJbUDgOclyG3jZEpAMOfof6PUZjPnPrePnPG62pS17CjdM32uT1Uq1jFvNSff9l7kNbmMSL2A==", "dev": true, - "requires": { - "minipass": "^3.0.0" + "license": "MIT", + "dependencies": { + "colord": "^2.9.3", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "minipass-flush": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", - "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "node_modules/postcss-minify-params": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-7.0.6.tgz", + "integrity": "sha512-YOn02gC68JijlaXVuKvFSCvQOhTpblkcfDre2hb/Aaa58r2BIaK4AtE/cyZf2wV7YKAG+UlP9DT+By0ry1E4VQ==", "dev": true, - "requires": { - "minipass": "^3.0.0" + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "minipass-pipeline": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", - "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "node_modules/postcss-minify-selectors": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-7.0.6.tgz", + "integrity": "sha512-lIbC0jy3AAwDxEgciZlBullDiMBeBCT+fz5G8RcA9MWqh/hfUkpOI3vNDUNEZHgokaoiv0juB9Y8fGcON7rU/A==", "dev": true, - "requires": { - "minipass": "^3.0.0" + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "minizlib": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", - "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "node_modules/postcss-minify-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "requires": { - "minipass": "^3.0.0", - "yallist": "^4.0.0" + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "mississippi": { + "node_modules/postcss-modules-extract-imports": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mississippi/-/mississippi-3.0.0.tgz", - "integrity": "sha512-x471SsVjUtBRtcvd4BzKE9kFC+/2TeWgKCgw0bZcw1b9l2X3QX5vCWgF+KaZaYm87Ss//rHnWryupDrgLvmSkA==", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", + "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", "dev": true, - "requires": { - "concat-stream": "^1.5.0", - "duplexify": "^3.4.2", - "end-of-stream": "^1.1.0", - "flush-write-stream": "^1.0.0", - "from2": "^2.1.0", - "parallel-transform": "^1.1.0", - "pump": "^3.0.0", - "pumpify": "^1.3.3", - "stream-each": "^1.1.0", - "through2": "^2.0.0" + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "mitt": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mitt/-/mitt-2.1.0.tgz", - "integrity": "sha512-ILj2TpLiysu2wkBbWjAmww7TkZb65aiQO+DkVdUTBpBXq+MHYiETENkKFMtsJZX1Lf4pe4QOrTSjIfUwN5lRdg==", - "dev": true + "node_modules/postcss-modules-local-by-default": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", + "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "dev": true, + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^6.0.2", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } }, - "mixin-deep": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/mixin-deep/-/mixin-deep-1.3.2.tgz", - "integrity": "sha512-WRoDn//mXBiJ1H40rqa3vH0toePwSsGb45iInWlTySa+Uu4k3tYUSxa2v1KqAiLtvlrSzaExqS1gtk96A9zvEA==", + "node_modules/postcss-modules-scope": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", + "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", "dev": true, - "requires": { - "for-in": "^1.0.2", - "is-extendable": "^1.0.1" + "dependencies": { + "postcss-selector-parser": "^6.0.4" + }, + "engines": { + "node": "^10 || ^12 || >= 14" }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, "dependencies": { - "is-extendable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-1.0.1.tgz", - "integrity": "sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==", - "dev": true, - "requires": { - "is-plain-object": "^2.0.4" - } - } + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" } }, - "mixin-object": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mixin-object/-/mixin-object-2.0.1.tgz", - "integrity": "sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4=", + "node_modules/postcss-normalize-charset": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-7.0.1.tgz", + "integrity": "sha512-sn413ofhSQHlZFae//m9FTOfkmiZ+YQXsbosqOWRiVQncU2BA3daX3n0VF3cG6rGLSFVc5Di/yns0dFfh8NFgQ==", "dev": true, - "requires": { - "for-in": "^0.1.3", - "is-extendable": "^0.1.1" + "license": "MIT", + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, - "dependencies": { - "for-in": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/for-in/-/for-in-0.1.8.tgz", - "integrity": "sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE=", - "dev": true - } + "peerDependencies": { + "postcss": "^8.4.32" } }, - "mkdirp": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", - "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", + "node_modules/postcss-normalize-display-values": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-7.0.1.tgz", + "integrity": "sha512-E5nnB26XjSYz/mGITm6JgiDpAbVuAkzXwLzRZtts19jHDUBFxZ0BkXAehy0uimrOjYJbocby4FVswA/5noOxrQ==", "dev": true, - "requires": { - "minimist": "^1.2.5" + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "mkdirp-classic": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", - "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "dev": true + "node_modules/postcss-normalize-positions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-7.0.1.tgz", + "integrity": "sha512-pB/SzrIP2l50ZIYu+yQZyMNmnAcwyYb9R1fVWPRxm4zcUFCY2ign7rcntGFuMXDdd9L2pPNUgoODDk91PzRZuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "mnemonist": { - "version": "0.38.3", - "resolved": "https://registry.npmjs.org/mnemonist/-/mnemonist-0.38.3.tgz", - "integrity": "sha512-2K9QYubXx/NAjv4VLq1d1Ly8pWNC5L3BrixtdkyTegXWJIqY+zLNDhhX/A+ZwWt70tB1S8H4BE8FLYEFyNoOBw==", - "requires": { - "obliterator": "^1.6.1" + "node_modules/postcss-normalize-repeat-style": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-7.0.1.tgz", + "integrity": "sha512-NsSQJ8zj8TIDiF0ig44Byo3Jk9e4gNt9x2VIlJudnQQ5DhWAHJPF4Tr1ITwyHio2BUi/I6Iv0HRO7beHYOloYQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "moment": { - "version": "2.29.1", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", - "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" - }, - "moment-timezone": { - "version": "0.5.33", - "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", - "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", - "requires": { - "moment": ">= 2.9.0" + "node_modules/postcss-normalize-string": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-7.0.1.tgz", + "integrity": "sha512-QByrI7hAhsoze992kpbMlJSbZ8FuCEc1OT9EFbZ6HldXNpsdpZr+YXC5di3UEv0+jeZlHbZcoCADgb7a+lPmmQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "moo": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/moo/-/moo-0.5.1.tgz", - "integrity": "sha512-I1mnb5xn4fO80BH9BLcF0yLypy2UKl+Cb01Fu0hJRkJjlCRtxZMWkTdAtDd5ZqCOxtCkhmRwyI57vWT+1iZ67w==", - "dev": true - }, - "mousetrap": { - "version": "1.6.5", - "resolved": "https://registry.npmjs.org/mousetrap/-/mousetrap-1.6.5.tgz", - "integrity": "sha512-QNo4kEepaIBwiT8CDhP98umTetp+JNfQYBWvC1pc6/OAibuXtRcxZ58Qz8skvEHYvURne/7R8T5VoOI7rDsEUA==" - }, - "move-concurrently": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/move-concurrently/-/move-concurrently-1.0.1.tgz", - "integrity": "sha1-viwAX9oy4LKa8fBdfEszIUxwH5I=", + "node_modules/postcss-normalize-timing-functions": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-7.0.1.tgz", + "integrity": "sha512-bHifyuuSNdKKsnNJ0s8fmfLMlvsQwYVxIoUBnowIVl2ZAdrkYQNGVB4RxjfpvkMjipqvbz0u7feBZybkl/6NJg==", "dev": true, - "requires": { - "aproba": "^1.1.1", - "copy-concurrently": "^1.0.0", - "fs-write-stream-atomic": "^1.0.8", - "mkdirp": "^0.5.1", - "rimraf": "^2.5.4", - "run-queue": "^1.0.3" + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" - }, - "multicast-dns": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-6.2.3.tgz", - "integrity": "sha512-ji6J5enbMyGRHIAkAOu3WdV8nggqviKCEKtXcOqfphZZtQrmHKycfynJ2V7eVPUA4NhJ6V7Wf4TmGbTwKE9B6g==", + "node_modules/postcss-normalize-unicode": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-7.0.6.tgz", + "integrity": "sha512-z6bwTV84YW6ZvvNoaNLuzRW4/uWxDKYI1iIDrzk6D2YTL7hICApy+Q1LP6vBEsljX8FM7YSuV9qI79XESd4ddQ==", "dev": true, - "requires": { - "dns-packet": "^1.3.1", - "thunky": "^1.0.2" + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "multicast-dns-service-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/multicast-dns-service-types/-/multicast-dns-service-types-1.1.0.tgz", - "integrity": "sha1-iZ8R2WhuXgXLkbNdXw5jt3PPyQE=", - "dev": true - }, - "mutationobserver-shim": { - "version": "0.3.7", - "resolved": "https://registry.npmjs.org/mutationobserver-shim/-/mutationobserver-shim-0.3.7.tgz", - "integrity": "sha512-oRIDTyZQU96nAiz2AQyngwx1e89iApl2hN5AOYwyxLUB47UYsU3Wv9lJWqH5y/QdiYkc5HQLi23ZNB3fELdHcQ==" - }, - "mute-stream": { - "version": "0.0.7", - "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.7.tgz", - "integrity": "sha1-MHXOk7whuPq0PhvE2n6BFe0ee6s=", - "dev": true - }, - "nan": { - "version": "2.14.2", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.14.2.tgz", - "integrity": "sha512-M2ufzIiINKCuDfBSAUr1vWQ+vuVcA9kqx8JJUsbQi6yf1uGRyb7HfpdfUr5qLXf3B/t8dPvcjhKMmlfnP47EzQ==", + "node_modules/postcss-normalize-url": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-7.0.1.tgz", + "integrity": "sha512-sUcD2cWtyK1AOL/82Fwy1aIVm/wwj5SdZkgZ3QiUzSzQQofrbq15jWJ3BA7Z+yVRwamCjJgZJN0I9IS7c6tgeQ==", "dev": true, - "optional": true - }, - "nanoid": { - "version": "3.1.25", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.1.25.tgz", - "integrity": "sha512-rdwtIXaXCLFAQbnfqDRnI6jaRHp9fTcYBjtFKE8eezcZ7LuLjhUaQGNeMXf1HmRoCH32CLz6XwX0TtxEOS/A3Q==", - "dev": true + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "nanomatch": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/nanomatch/-/nanomatch-1.2.13.tgz", - "integrity": "sha512-fpoe2T0RbHwBTBUOftAfBPaDEi06ufaUai0mE6Yn1kacc3SnTErfb/h+X94VXzI64rKFHYImXSvdwGGCmwOqCA==", + "node_modules/postcss-normalize-whitespace": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-7.0.1.tgz", + "integrity": "sha512-vsbgFHMFQrJBJKrUFJNZ2pgBeBkC2IvvoHjz1to0/0Xk7sII24T0qFOiJzG6Fu3zJoq/0yI4rKWi7WhApW+EFA==", "dev": true, - "requires": { - "arr-diff": "^4.0.0", - "array-unique": "^0.3.2", - "define-property": "^2.0.2", - "extend-shallow": "^3.0.2", - "fragment-cache": "^0.2.1", - "is-windows": "^1.0.2", - "kind-of": "^6.0.2", - "object.pick": "^1.3.0", - "regex-not": "^1.0.0", - "snapdragon": "^0.8.1", - "to-regex": "^3.0.1" - }, + "license": "MIT", "dependencies": { - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "natural-compare": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", - "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", - "dev": true - }, - "nearley": { - "version": "2.20.1", - "resolved": "https://registry.npmjs.org/nearley/-/nearley-2.20.1.tgz", - "integrity": "sha512-+Mc8UaAebFzgV+KpI5n7DasuuQCHA89dmwm7JXw3TV43ukfNQ9DnBH3Mdb2g/I4Fdxc26pwimBWvjIw0UAILSQ==", + "node_modules/postcss-ordered-values": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-7.0.2.tgz", + "integrity": "sha512-AMJjt1ECBffF7CEON/Y0rekRLS6KsePU6PRP08UqYW4UGFRnTXNrByUzYK1h8AC7UWTZdQ9O3Oq9kFIhm0SFEw==", "dev": true, - "requires": { - "commander": "^2.19.0", - "moo": "^0.5.0", - "railroad-diagrams": "^1.0.0", - "randexp": "0.4.6" + "license": "MIT", + "dependencies": { + "cssnano-utils": "^5.0.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "negotiator": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.2.tgz", - "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" - }, - "neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "dev": true + "node_modules/postcss-reduce-initial": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-7.0.6.tgz", + "integrity": "sha512-G6ZyK68AmrPdMB6wyeA37ejnnRG2S8xinJrZJnOv+IaRKf6koPAVbQsiC7MfkmXaGmF1UO+QCijb27wfpxuRNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "caniuse-api": "^3.0.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "next-applause": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/next-applause/-/next-applause-2.2.4.tgz", - "integrity": "sha512-ktqjWT512q6vzAYnmRfJcqqVCA7ft8VcqkfBzgWuqI9SDSHM//B+hvjrGlkNzOzDMzljc3flok01t79OGkRVXQ==", + "node_modules/postcss-reduce-transforms": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-7.0.1.tgz", + "integrity": "sha512-MhyEbfrm+Mlp/36hvZ9mT9DaO7dbncU0CvWI8V93LRkY6IYlu38OPg3FObnuKTUxJ4qA8HpurdQOo5CyqqO76g==", "dev": true, - "requires": { - "cson-parser": "^1.2.0", - "js-yaml": "^3.3.0", - "lodash": "^4.17.11" + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "nice-try": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", - "integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==", - "dev": true + "node_modules/postcss-resolve-nested-selector": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.6.tgz", + "integrity": "sha512-0sglIs9Wmkzbr8lQwEyIzlDOOC9bGmfVKcJTaxv3vMmd3uo4o4DerC3En0bnmgceeql9BfC8hRkp7cg0fjdVqw==", + "dev": true, + "license": "MIT" }, - "nise": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/nise/-/nise-5.1.0.tgz", - "integrity": "sha512-W5WlHu+wvo3PaKLsJJkgPup2LrsXCcm7AWwyNZkUnn5rwPkuPBi3Iwk5SQtN0mv+K65k7nKKjwNQ30wg3wLAQQ==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0", - "@sinonjs/fake-timers": "^7.0.4", - "@sinonjs/text-encoding": "^0.7.1", - "just-extend": "^4.0.2", - "path-to-regexp": "^1.7.0" - }, - "dependencies": { - "@sinonjs/fake-timers": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.1.tgz", - "integrity": "sha512-am34LJf0N2nON/PT9G7pauA+xjcwX9P6x31m4hBgfUeSXYRZBRv/R6EcdWs8iV4XJjPO++NTsrj7ua/cN2s6ZA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } + "node_modules/postcss-safe-parser": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-7.0.1.tgz", + "integrity": "sha512-0AioNCJZ2DPYz5ABT6bddIqlhgwhpHZ/l65YAYo0BCIn0xiDpsnTHz0gnoTGk0OXZW0JRs+cDwL8u/teRdz+8A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" }, - "isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha1-ihis/Kmo9Bd+Cav8YDiTmwXR7t8=", - "dev": true + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-safe-parser" }, - "path-to-regexp": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.8.0.tgz", - "integrity": "sha512-n43JRhlUKUAlibEJhPeir1ncUID16QnEjNpwzNdO3Lm4ywrBpBZ5oLD0I6br9evr1Y9JTqwRtAh7JLoOzAQdVA==", - "dev": true, - "requires": { - "isarray": "0.0.1" - } + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "engines": { + "node": ">=18.0" + }, + "peerDependencies": { + "postcss": "^8.4.31" } }, - "node-fetch": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.1.tgz", - "integrity": "sha512-V4aYg89jEoVRxRb2fJdAg8FHvI7cEyYdVAh94HH0UIK8oJxUfkjlDQN9RbMx+bEjP7+ggMiFRprSti032Oipxw==", - "dev": true - }, - "node-forge": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.10.0.tgz", - "integrity": "sha512-PPmu8eEeG9saEUvI97fm4OYxXVB6bFvyNTyiUOBichBpFG8A1Ljw3bY62+5oOjDEMHRnd0Y7HQ+x7uzxOzC6JA==", - "dev": true - }, - "node-int64": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", - "integrity": "sha1-h6kGXNs1XTGC2PlM4RGIuCXGijs=", - "dev": true - }, - "node-libs-browser": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", - "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", - "dev": true, - "requires": { - "assert": "^1.1.1", - "browserify-zlib": "^0.2.0", - "buffer": "^4.3.0", - "console-browserify": "^1.1.0", - "constants-browserify": "^1.0.0", - "crypto-browserify": "^3.11.0", - "domain-browser": "^1.1.1", - "events": "^3.0.0", - "https-browserify": "^1.0.0", - "os-browserify": "^0.3.0", - "path-browserify": "0.0.1", - "process": "^0.11.10", - "punycode": "^1.2.4", - "querystring-es3": "^0.2.0", - "readable-stream": "^2.3.3", - "stream-browserify": "^2.0.1", - "stream-http": "^2.7.2", - "string_decoder": "^1.0.0", - "timers-browserify": "^2.0.4", - "tty-browserify": "0.0.0", - "url": "^0.11.0", - "util": "^0.11.0", - "vm-browserify": "^1.0.1" - }, - "dependencies": { - "buffer": { - "version": "4.9.2", - "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", - "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", - "dev": true, - "requires": { - "base64-js": "^1.0.2", - "ieee754": "^1.1.4", - "isarray": "^1.0.0" - } + "node_modules/postcss-scss": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-4.0.9.tgz", + "integrity": "sha512-AjKOeiwAitL/MXxQW2DliT28EKukvvbEWx3LBmJIRN8KfBGZbRTxNYW0kSqi1COiTZ57nZ9NW06S6ux//N1c9A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss-scss" }, - "punycode": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", - "integrity": "sha1-wNWmOycYgArY4esPpSachN1BhF4=", - "dev": true + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "engines": { + "node": ">=12.0" + }, + "peerDependencies": { + "postcss": "^8.4.29" } }, - "node-modules-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-modules-regexp/-/node-modules-regexp-1.0.0.tgz", - "integrity": "sha1-jZ2+KJZKSsVxLpExZCEHxx6Q7EA=", - "dev": true - }, - "node-notifier": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/node-notifier/-/node-notifier-8.0.2.tgz", - "integrity": "sha512-oJP/9NAdd9+x2Q+rfphB2RJCHjod70RcRLjosiPMMu5gjIfwVnOUGq2nbTjTUbmy0DJ/tFIVT30+Qe3nzl4TJg==", + "node_modules/postcss-selector-parser": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", + "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", "dev": true, - "optional": true, - "requires": { - "growly": "^1.3.0", - "is-wsl": "^2.2.0", - "semver": "^7.3.2", - "shellwords": "^0.1.1", - "uuid": "^8.3.0", - "which": "^2.0.2" - }, - "dependencies": { - "is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "dev": true, - "optional": true, - "requires": { - "is-docker": "^2.0.0" - } - } + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "node-releases": { - "version": "1.1.72", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", - "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", - "dev": true - }, - "node-watch": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/node-watch/-/node-watch-0.7.1.tgz", - "integrity": "sha512-UWblPYuZYrkCQCW5PxAwYSxaELNBLUckrTBBk8xr1/bUgyOkYYTsUcV4e3ytcazFEOyiRyiUrsG37pu6I0I05g==", - "dev": true + "node_modules/postcss-svgo": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-7.1.1.tgz", + "integrity": "sha512-zU9H9oEDrUFKa0JB7w+IYL7Qs9ey1mZyjhbf0KLxwJDdDRtoPvCmaEfknzqfHj44QS9VD6c5sJnBAVYTLRg/Sg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-value-parser": "^4.2.0", + "svgo": "^4.0.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >= 18" + }, + "peerDependencies": { + "postcss": "^8.4.32" + } }, - "nopt": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz", - "integrity": "sha1-xkZdvwirzU2zWTF/eaxopkayj/k=", + "node_modules/postcss-svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", "dev": true, - "requires": { - "abbrev": "1" + "license": "MIT", + "engines": { + "node": ">=16" } }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", + "node_modules/postcss-svgo/node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - }, + "license": "BSD-2-Clause", "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - } + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==" - }, - "normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha1-LRDAa9/TEuqXd2laTShDlFa3WUI=", - "dev": true - }, - "normalize-selector": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/normalize-selector/-/normalize-selector-0.2.0.tgz", - "integrity": "sha1-0LFF62kRicY6eNIB3E/bEpPvDAM=", - "dev": true - }, - "normalize-wheel": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/normalize-wheel/-/normalize-wheel-1.0.1.tgz", - "integrity": "sha1-rsiGr/2wRQcNhWRH32Ls+GFG7EU=" - }, - "npm-conf": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/npm-conf/-/npm-conf-1.1.3.tgz", - "integrity": "sha512-Yic4bZHJOt9RCFbRP3GgpqhScOY4HH3V2P8yBj6CeYq118Qr+BLXqT2JvpJ00mryLESpgOxf5XlFv4ZjXxLScw==", + "node_modules/postcss-svgo/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, - "optional": true, - "requires": { - "config-chain": "^1.1.11", - "pify": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true, - "optional": true - } + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "npm-package-json-lint": { - "version": "5.2.3", - "resolved": "https://registry.npmjs.org/npm-package-json-lint/-/npm-package-json-lint-5.2.3.tgz", - "integrity": "sha512-rSgc4eVhtfwrU7AWwovqFWy8OEkgQL99vD3vWJmqtU9gxxJxKzi6Wqgo3gF7lhrBpyVcnlKxy/L2JCsvjWruDA==", + "node_modules/postcss-svgo/node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", "dev": true, - "requires": { - "ajv": "^6.12.6", - "ajv-errors": "^1.0.1", - "chalk": "^4.1.2", - "cosmiconfig": "^6.0.0", - "debug": "^4.3.2", - "globby": "^11.0.4", - "ignore": "^5.1.8", - "is-plain-obj": "^3.0.0", - "jsonc-parser": "^2.3.1", - "log-symbols": "^4.1.0", - "meow": "^6.1.1", - "plur": "^4.0.0", - "semver": "^7.3.5", - "slash": "^3.0.0", - "strip-json-comments": "^3.1.1" + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" }, - "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "dev": true, - "requires": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "dev": true - }, - "jsonc-parser": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-2.3.1.tgz", - "integrity": "sha512-H8jvkz1O50L3dMZCsLqiuB2tA7muqbSg1AtGEkN0leAqGjsUzDJir3Zwr02BhqdcITPg3ei3mZ+HjMocAknhhg==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "map-obj": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", - "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", - "dev": true - }, - "meow": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/meow/-/meow-6.1.1.tgz", - "integrity": "sha512-3YffViIt2QWgTy6Pale5QpopX/IvU3LPL03jOTqp6pGj3VjesdO/U8CuHMKpnQr4shCNCM5fd5XFFvIIl6JBHg==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "^4.0.2", - "normalize-package-data": "^2.5.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.13.1", - "yargs-parser": "^18.1.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "semver": { - "version": "7.3.5", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.5.tgz", - "integrity": "sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.13.1.tgz", - "integrity": "sha512-34R7HTnG0XIJcBSn5XhDd7nNFPRcXYRZrBB2O2jdKqYODldSzBAqzsWoZYYvduky73toYS/ESqxPvkDf/F0XMg==", - "dev": true - }, - "yargs-parser": { - "version": "18.1.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", - "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "funding": { + "url": "https://github.com/sponsors/fb55" } }, - "npm-run-path": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-2.0.2.tgz", - "integrity": "sha1-NakjLfo11wZ7TLLd8jV7GHFTbF8=", + "node_modules/postcss-svgo/node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", "dev": true, - "requires": { - "path-key": "^2.0.0" + "license": "MIT", + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "nth-check": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-1.0.2.tgz", - "integrity": "sha512-WeBOdju8SnzPN5vTUJYxYUxLeXpCaVP5i5e0LF8fg7WORF2Wd7wFX/pk0tYZk7s8T+J7VLy0Da6J1+wCT0AtHg==", + "node_modules/postcss-svgo/node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", "dev": true, - "requires": { - "boolbase": "~1.0.0" + "license": "MIT", + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" } }, - "num2fraction": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/num2fraction/-/num2fraction-1.2.2.tgz", - "integrity": "sha1-b2gragJ6Tp3fpFZM0lidHU5mnt4=", - "dev": true - }, - "number-is-nan": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", - "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", - "dev": true - }, - "nwsapi": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/nwsapi/-/nwsapi-2.2.0.tgz", - "integrity": "sha512-h2AatdwYH+JHiZpv7pt/gSX1XoRGb7L/qSIeuqA6GwYoF9w1vP1cw42TO0aI2pNyshRK5893hNSl+1//vHK7hQ==", - "dev": true - }, - "oauth-sign": { - "version": "0.9.0", - "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", - "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", - "dev": true - }, - "object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=" + "node_modules/postcss-svgo/node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true, + "license": "CC0-1.0" }, - "object-copy": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/object-copy/-/object-copy-0.1.0.tgz", - "integrity": "sha1-fn2Fi3gb18mRpBupde04EnVOmYw=", + "node_modules/postcss-svgo/node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, - "requires": { - "copy-descriptor": "^0.1.0", - "define-property": "^0.2.5", - "kind-of": "^3.0.3" - }, + "license": "MIT", "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - } + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" } }, - "object-filter": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/object-filter/-/object-filter-1.0.2.tgz", - "integrity": "sha1-rwt5f/6+r4pSxmN87b6IFs/sG8g=", - "dev": true + "node_modules/postcss-svgo/node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" }, - "object-inspect": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.9.0.tgz", - "integrity": "sha512-i3Bp9iTqwhaLZBxGkRfo5ZbE07BQRT7MGu8+nNgwW9ItGp1TzCTw2DLEoWwjClxBjOFI/hWljTAmYGCEwmtnOw==" + "node_modules/postcss-svgo/node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } }, - "object-is": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.4.tgz", - "integrity": "sha512-1ZvAZ4wlF7IyPVOcE1Omikt7UpaFlOQq0HlSti+ZvDH3UiD2brwGMwDbyV43jao2bKJ+4+WdPJHSd7kgzKYVqg==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "node_modules/postcss-svgo/node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==" + "node_modules/postcss-svgo/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" }, - "object-visit": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object-visit/-/object-visit-1.0.1.tgz", - "integrity": "sha1-95xEk68MU3e1n+OdOV5BBC3QRbs=", + "node_modules/postcss-svgo/node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", "dev": true, - "requires": { - "isobject": "^3.0.0" + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" } }, - "object.assign": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.2.tgz", - "integrity": "sha512-ixT2L5THXsApyiUPYKmW+2EHpXXe5Ii3M+f4e+aJFAHao5amFRW6J0OO6c/LU8Be47utCx2GL89hxGB6XSmKuQ==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "has-symbols": "^1.0.1", - "object-keys": "^1.1.1" + "node_modules/postcss-svgo/node_modules/sax": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", + "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=11.0.0" } }, - "object.defaults": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/object.defaults/-/object.defaults-1.1.0.tgz", - "integrity": "sha1-On+GgzS0B96gbaFtiNXNKeQ1/s8=", + "node_modules/postcss-svgo/node_modules/svgo": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", + "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", "dev": true, - "requires": { - "array-each": "^1.0.1", - "array-slice": "^1.0.0", - "for-own": "^1.0.0", - "isobject": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } + "commander": "^11.1.0", + "css-select": "^5.1.0", + "css-tree": "^3.0.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.1.1", + "sax": "^1.5.0" + }, + "bin": { + "svgo": "bin/svgo.js" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" } }, - "object.entries": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/object.entries/-/object.entries-1.1.4.tgz", - "integrity": "sha512-h4LWKWE+wKQGhtMjZEBud7uLGhqyLwj8fpHOarZhD2uY3C9cRtk57VQ89ke3moByLXMedqs3XCHzyb4AmA2DjA==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2" + "node_modules/postcss-unique-selectors": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-7.0.5.tgz", + "integrity": "sha512-3QoYmEt4qg/rUWDn6Tc8+ZVPmbp4G1hXDtCNWDx0st8SjtCbRcxRXDDM1QrEiXGG3A45zscSJFb4QH90LViyxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" }, + "peerDependencies": { + "postcss": "^8.4.32" + } + }, + "node_modules/postcss-unique-selectors/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==" - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==" - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "object.fromentries": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/object.fromentries/-/object.fromentries-2.0.4.tgz", - "integrity": "sha512-EsFBshs5RUUpQEY1D4q/m59kMfz4YJvxuNCJcv/jWwOJr34EaVnG11ZrZa0UHB3wnzV1wx8m58T4hQL8IuNXlQ==", + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2", - "has": "^1.0.3" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "license": "MIT", + "engines": { + "node": ">= 0.8.0" } }, - "object.getownpropertydescriptors": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/object.getownpropertydescriptors/-/object.getownpropertydescriptors-2.1.1.tgz", - "integrity": "sha512-6DtXgZ/lIZ9hqx4GtZETobXLR/ZLaa0aqV0kzbn80Rf8Z2e/XFnhA0I7p07N2wH8bBBltr2xQPi6sbKWAY2Eng==", + "node_modules/prepend-http": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", + "integrity": "sha512-PhmXi5XmoyKw1Un4E+opM2KcsJInDvKyuOumcjjw3waw86ZNjHwVUOOWLc4bCzLdcKNaWBH9e99sbWzDQsVaYg==", "dev": true, - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/prettier": { + "name": "wp-prettier", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-3.0.3.tgz", + "integrity": "sha512-X4UlrxDTH8oom9qXlcjnydsjAOD2BmB6yFmvS4Z2zdTzqqpRWb+fbqrH412+l+OUXmbzJlSXjlMFYPgYG12IAA==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "dev": true, - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" } }, - "object.map": { + "node_modules/prettier-linter-helpers": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/object.map/-/object.map-1.0.1.tgz", - "integrity": "sha1-z4Plncj8wK1fQlDh94s7gb2AHTc=", + "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.1.tgz", + "integrity": "sha512-SxToR7P8Y2lWmv/kTzVLC1t/GDI2WGjMwNhLLE9qtH8Q13C+aEmuRlzDst4Up4s0Wc8sF2M+J57iB3cMLqftfg==", "dev": true, - "requires": { - "for-own": "^1.0.0", - "make-iterator": "^1.0.0" - }, + "license": "MIT", "dependencies": { - "for-own": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/for-own/-/for-own-1.0.0.tgz", - "integrity": "sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs=", - "dev": true, - "requires": { - "for-in": "^1.0.1" - } - } + "fast-diff": "^1.1.2" + }, + "engines": { + "node": ">=6.0.0" } }, - "object.pick": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/object.pick/-/object.pick-1.3.0.tgz", - "integrity": "sha1-h6EKxMFpS9Lhy/U1kaZhQftd10c=", + "node_modules/pretty-bytes": { + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-5.6.0.tgz", + "integrity": "sha512-FFw039TmrBqFK8ma/7OL3sDz/VytdtJr044/QUJtH0wK9lb9jLq9tJyIxUwtQJHwar2BqtiA4iCWSwo9JLkzFg==", "dev": true, - "requires": { - "isobject": "^3.0.1" + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "object.values": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/object.values/-/object.values-1.1.2.tgz", - "integrity": "sha512-MYC0jvJopr8EK6dPBiO8Nb9mvjdypOachO5REGk6MXzujbBrAisKo3HmdEI6kZDL6fC31Mwee/5YbtMebixeag==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.1", - "has": "^1.0.3" - }, - "dependencies": { - "es-abstract": { - "version": "1.18.0-next.1", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.0-next.1.tgz", - "integrity": "sha512-I4UGspA0wpZXWENrdA0uHbnhte683t3qT/1VFH9aX2dA5PPSf6QW5HHXf5HImaqPmjXaVeVk4RGWnaylmV7uAA==", - "requires": { - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1", - "is-callable": "^1.2.2", - "is-negative-zero": "^2.0.0", - "is-regex": "^1.1.1", - "object-inspect": "^1.8.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.1", - "string.prototype.trimend": "^1.0.1", - "string.prototype.trimstart": "^1.0.1" - } - } + "node_modules/pretty-format": { + "version": "29.7.0", + "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.7.0.tgz", + "integrity": "sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==", + "dev": true, + "dependencies": { + "@jest/schemas": "^29.6.3", + "ansi-styles": "^5.0.0", + "react-is": "^18.0.0" + }, + "engines": { + "node": "^14.15.0 || ^16.10.0 || >=18.0.0" } }, - "objectFitPolyfill": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/objectFitPolyfill/-/objectFitPolyfill-2.3.5.tgz", - "integrity": "sha512-8Quz071ZmGi0QWEG4xB3Bv5Lpw6K0Uca87FLoLMKMWjB6qIq9IyBegP3b/VLNxv2WYvIMGoeUQ+c6ibUkNa8TA==" - }, - "obliterator": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/obliterator/-/obliterator-1.6.1.tgz", - "integrity": "sha512-9WXswnqINnnhOG/5SLimUlzuU1hFJUc8zkwyD59Sd+dPOMf05PmnYG/d6Q7HZ+KmgkZJa1PxRso6QdM3sTNHig==" + "node_modules/pretty-format/node_modules/ansi-styles": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz", + "integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } }, - "obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "dev": true + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" }, - "on-finished": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", - "integrity": "sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=", - "requires": { - "ee-first": "1.1.1" + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" } }, - "on-headers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", - "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", - "dev": true + "node_modules/promise-polyfill": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-1.1.6.tgz", + "integrity": "sha512-7rrONfyLkDEc7OJ5QBkqa4KI4EBhCd340xRuIUPGCfu13znS+vx+VDdrT9ODAJHlXm7w4lbxN3DRjyv58EuzDg==" }, - "once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", - "requires": { - "wrappy": "1" + "node_modules/prompts": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", + "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", + "dev": true, + "dependencies": { + "kleur": "^3.0.3", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 6" } }, - "onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", "dev": true, - "requires": { - "mimic-fn": "^2.1.0" + "license": "MIT", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" } }, - "opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "dev": true + "node_modules/prop-types/node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", + "dev": true, + "license": "MIT" }, - "opn": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/opn/-/opn-5.5.0.tgz", - "integrity": "sha512-PqHpggC9bLV0VeWcdKhkpxY+3JTzetLSqTCWL/z/tFIbI6G8JCjondXklT1JinczLz2Xib62sSp0T/gKT4KksA==", + "node_modules/proto-list": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", + "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", "dev": true, - "requires": { - "is-wsl": "^1.1.0" - } + "optional": true }, - "optionator": { - "version": "0.9.1", - "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.1.tgz", - "integrity": "sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==", + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", "dev": true, - "requires": { - "deep-is": "^0.1.3", - "fast-levenshtein": "^2.0.6", - "levn": "^0.4.1", - "prelude-ls": "^1.2.1", - "type-check": "^0.4.0", - "word-wrap": "^1.2.3" + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" } }, - "optipng-bin": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/optipng-bin/-/optipng-bin-5.1.0.tgz", - "integrity": "sha512-9baoqZTNNmXQjq/PQTWEXbVV3AMO2sI/GaaqZJZ8SExfAzjijeAP7FEeT+TtyumSw7gr0PZtSUYB/Ke7iHQVKA==", + "node_modules/proxy-addr/node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", "dev": true, - "optional": true, - "requires": { - "bin-build": "^3.0.0", - "bin-wrapper": "^4.0.0", - "logalot": "^2.0.0" + "engines": { + "node": ">= 0.10" } }, - "original": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/original/-/original-1.0.2.tgz", - "integrity": "sha512-hyBVl6iqqUOJ8FqRe+l/gS8H+kKYjrEndd5Pm1MfBtsEKA038HkkdbAl/72EAXGyonD/PFsvmVG+EvcIpliMBg==", + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", "dev": true, - "requires": { - "url-parse": "^1.4.3" + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" } }, - "os-browserify": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", - "integrity": "sha1-hUNzx/XCMVkU/Jv8a9gjj92h7Cc=", - "dev": true - }, - "os-filter-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/os-filter-obj/-/os-filter-obj-2.0.0.tgz", - "integrity": "sha512-uksVLsqG3pVdzzPvmAHpBK0wKxYItuzZr7SziusRPoz67tGV8rL1szZ6IdeUrbqLjGDwApBtN29eEE3IqGHOjg==", + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", "dev": true, - "optional": true, - "requires": { - "arch": "^2.1.0" + "license": "ISC", + "engines": { + "node": ">=12" } }, - "os-homedir": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", - "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", "dev": true }, - "os-tmpdir": { + "node_modules/pseudomap": { "version": "1.0.2", - "resolved": "https://registry.npmjs.org/os-tmpdir/-/os-tmpdir-1.0.2.tgz", - "integrity": "sha1-u+Z0BseaqFxc/sdm/lc0VV36EnQ=", + "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", + "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", + "dev": true, + "optional": true + }, + "node_modules/psl": { + "version": "1.8.0", + "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", + "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", "dev": true }, - "osenv": { - "version": "0.1.5", - "resolved": "https://registry.npmjs.org/osenv/-/osenv-0.1.5.tgz", - "integrity": "sha512-0CWcCECdMVc2Rw3U5w9ZjqX6ga6ubk1xDVKxtBQPK7wis/0F2r9T6k4ydGYhecl7YUBxBVxhL5oisPsNxAPe2g==", + "node_modules/pump": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", + "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", "dev": true, - "requires": { - "os-homedir": "^1.0.0", - "os-tmpdir": "^1.0.0" + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" } }, - "outlayer": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/outlayer/-/outlayer-2.1.1.tgz", - "integrity": "sha1-KYY7beEOpdrf/8rfoNcokHOH6aI=", - "requires": { - "ev-emitter": "^1.0.0", - "fizzy-ui-utils": "^2.0.0", - "get-size": "^2.0.2" + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" } }, - "p-cancelable": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-0.3.0.tgz", - "integrity": "sha512-RVbZPLso8+jFeq1MfNvgXtCRED2raz/dKpacfTNxsx6pLEpEomM7gah6VeHSYV3+vo0OAi4MkArtQcWWXuQoyw==", + "node_modules/puppeteer": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-22.15.0.tgz", + "integrity": "sha512-XjCY1SiSEi1T7iSYuxS82ft85kwDJUS7wj1Z0eGVXKdtr5g4xnVcbjwxhq5xBnpK/E7x1VZZoJDxpjAOasHT4Q==", + "deprecated": "< 24.15.0 is no longer supported", "dev": true, - "optional": true + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "cosmiconfig": "^9.0.0", + "devtools-protocol": "0.0.1312386", + "puppeteer-core": "22.15.0" + }, + "bin": { + "puppeteer": "lib/esm/puppeteer/node/cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core": { + "version": "23.11.1", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-23.11.1.tgz", + "integrity": "sha512-3HZ2/7hdDKZvZQ7dhhITOUg4/wOrDRjyK2ZBllRB0ZCOi9u0cwq1ACHDjBB+nX+7+kltHjQvBRdeY7+W0T+7Gg==", + "dev": true, + "dependencies": { + "@puppeteer/browsers": "2.6.1", + "chromium-bidi": "0.11.0", + "debug": "^4.4.0", + "devtools-protocol": "0.0.1367902", + "typed-query-selector": "^2.12.0", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/@puppeteer/browsers": { + "version": "2.6.1", + "resolved": "https://registry.npmjs.org/@puppeteer/browsers/-/browsers-2.6.1.tgz", + "integrity": "sha512-aBSREisdsGH890S2rQqK82qmQYU3uFpSH8wcZWHgHzl3LfzsxAKbLNiAG9mO8v1Y0UICBeClICxPJvyr0rcuxg==", + "dev": true, + "dependencies": { + "debug": "^4.4.0", + "extract-zip": "^2.0.1", + "progress": "^2.0.3", + "proxy-agent": "^6.5.0", + "semver": "^7.6.3", + "tar-fs": "^3.0.6", + "unbzip2-stream": "^1.4.3", + "yargs": "^17.7.2" + }, + "bin": { + "browsers": "lib/cjs/main-cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/puppeteer-core/node_modules/chromium-bidi": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/chromium-bidi/-/chromium-bidi-0.11.0.tgz", + "integrity": "sha512-6CJWHkNRoyZyjV9Rwv2lYONZf1Xm0IuDyNq97nwSsxxP3wf5Bwy15K5rOvVKMtJ127jJBmxFUanSAOjgFRxgrA==", + "dev": true, + "dependencies": { + "mitt": "3.0.1", + "zod": "3.23.8" + }, + "peerDependencies": { + "devtools-protocol": "*" + } }, - "p-each-series": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-each-series/-/p-each-series-2.2.0.tgz", - "integrity": "sha512-ycIL2+1V32th+8scbpTvyHNaHe02z0sjgh91XXjAk+ZeXoPN4Z46DVUnzdso0aX4KckKw0FNNFHdjZ2UsZvxiA==", + "node_modules/puppeteer-core/node_modules/devtools-protocol": { + "version": "0.0.1367902", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1367902.tgz", + "integrity": "sha512-XxtPuC3PGakY6PD7dG66/o8KwJ/LkH2/EKe19Dcw58w53dv4/vSQEkn/SzuyhHE2q4zPgCkxQBxus3VV4ql+Pg==", "dev": true }, - "p-event": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-event/-/p-event-1.3.0.tgz", - "integrity": "sha1-jmtPT2XHK8W2/ii3XtqHT5akoIU=", + "node_modules/puppeteer-core/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", "dev": true, - "optional": true, - "requires": { - "p-timeout": "^1.1.1" + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } } }, - "p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha1-P7z7FbiZpEEjs0ttzBi3JDNqLK4=", + "node_modules/puppeteer/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true }, - "p-is-promise": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/p-is-promise/-/p-is-promise-1.1.0.tgz", - "integrity": "sha1-nJRWmJ6fZYgBewQ01WCXZ1w9oF4=", + "node_modules/puppeteer/node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", - "requires": { - "p-try": "^2.0.0" + "node_modules/puppeteer/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "p-locate": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", - "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", - "requires": { - "p-limit": "^2.0.0" + "node_modules/puppeteer/node_modules/puppeteer-core": { + "version": "22.15.0", + "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-22.15.0.tgz", + "integrity": "sha512-cHArnywCiAAVXa3t4GGL2vttNxh7GqXtIYGym99egkNJ3oG//wL9LkvO4WE8W1TJe95t1F1ocu9X4xWaGsOKOA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@puppeteer/browsers": "2.3.0", + "chromium-bidi": "0.6.3", + "debug": "^4.3.6", + "devtools-protocol": "0.0.1312386", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=18" } }, - "p-map": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-2.1.0.tgz", - "integrity": "sha512-y3b8Kpd8OAN444hxfBbFfj1FY/RjtTd8tzYwhUqNYXx0fXx2iX4maP4Qr6qhIKbQXI02wTLAda4fYUbDagTUFw==", - "dev": true + "node_modules/puppeteer/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, - "p-map-series": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-map-series/-/p-map-series-1.0.0.tgz", - "integrity": "sha1-v5j+V1cFZYqeE1G++4WuTB8Hvco=", + "node_modules/pure-rand": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.0.3.tgz", + "integrity": "sha512-KddyFewCsO0j3+np81IQ+SweXLDnDQTs5s67BOnrYmYe/yNmUhttQyGsYzy8yUnoljGAQ9sl38YB4vH8ur7Y+w==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/q": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", + "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", "dev": true, "optional": true, - "requires": { - "p-reduce": "^1.0.0" + "engines": { + "node": ">=0.6.0", + "teleport": ">=0.2.0" } }, - "p-pipe": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/p-pipe/-/p-pipe-1.2.0.tgz", - "integrity": "sha1-SxoROZoRUgpneQ7loMHViB1r7+k=", - "dev": true + "node_modules/qified": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.9.0.tgz", + "integrity": "sha512-4q61YgkHbY6gmwkqm0BsxyLDO3UYdrdiJTJ7JiaZb3xpW1duxn135SB7KqUEkCiuu5O4W+TtwEWP2VjmSRanvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.0" + }, + "engines": { + "node": ">=20" + } }, - "p-reduce": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-reduce/-/p-reduce-1.0.0.tgz", - "integrity": "sha1-GMKw3ZNqRpClKfgjH1ig/bakffo=", + "node_modules/qified/node_modules/hookified": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.1.0.tgz", + "integrity": "sha512-ootKng4eaxNxa7rx6FJv2YKef3DuhqbEj3l70oGXwddPQEEnISm50TEZQclqiLTAtilT2nu7TErtCO523hHkyg==", "dev": true, - "optional": true + "license": "MIT" }, - "p-retry": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-3.0.1.tgz", - "integrity": "sha512-XE6G4+YTTkT2a0UWb2kjZe8xNwf8bIbnqpc/IS/idOBVhyves0mK5OJgeocjx7q5pvX/6m23xuzVPYT1uGM73w==", + "node_modules/qs": { + "version": "6.14.2", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", + "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", "dev": true, - "requires": { - "retry": "^0.12.0" + "license": "BSD-3-Clause", + "dependencies": { + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "p-timeout": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-1.2.1.tgz", - "integrity": "sha1-XrOzU7f86Z8QGhA4iAuwVOu+o4Y=", + "node_modules/quick-lru": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", "dev": true, - "optional": true, - "requires": { - "p-finally": "^1.0.0" + "engines": { + "node": ">=8" } }, - "p-try": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", - "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==" + "node_modules/qunit": { + "version": "2.25.0", + "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.25.0.tgz", + "integrity": "sha512-MONPKgjavgTqArCwZOEz8nEMbA19zNXIp5ZOW9rPYj5cbgQp0fiI36c9dPTSzTRRzx+KcfB5eggYB/ENqxi0+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "commander": "7.2.0", + "node-watch": "0.7.3", + "tiny-glob": "0.2.9" + }, + "bin": { + "qunit": "bin/qunit.js" + }, + "engines": { + "node": ">=10" + } }, - "pako": { - "version": "1.0.11", - "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", - "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==", - "dev": true + "node_modules/qunit/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } }, - "parallel-transform": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/parallel-transform/-/parallel-transform-1.2.0.tgz", - "integrity": "sha512-P2vSmIu38uIlvdcU7fDkyrxj33gTUy/ABO5ZUbGowxNCopBq/OoD42bP4UmMrJoPyk4Uqf0mu3mtWBhHCZD8yg==", + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", "dev": true, - "requires": { - "cyclist": "^1.0.1", - "inherits": "^2.0.3", - "readable-stream": "^2.1.5" + "dependencies": { + "safe-buffer": "^5.1.0" } }, - "parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "requires": { - "callsites": "^3.0.0" + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" } }, - "parse-asn1": { - "version": "5.1.6", - "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.6.tgz", - "integrity": "sha512-RnZRo1EPU6JBnra2vGHj0yhp6ebyjBZpmUCLHWiFhxlzvBCCpAuZ7elsBp1PVAbQN0/04VD/19rfzlBSwLstMw==", + "node_modules/raw-body": { + "version": "2.5.3", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.3.tgz", + "integrity": "sha512-s4VSOf6yN0rvbRZGxs8Om5CWj6seneMwK3oDb4lWDH0UPhWcxwOWw5+qk24bxq87szX1ydrwylIOp2uG1ojUpA==", "dev": true, - "requires": { - "asn1.js": "^5.2.0", - "browserify-aes": "^1.0.0", - "evp_bytestokey": "^1.0.0", - "pbkdf2": "^3.0.3", - "safe-buffer": "^5.1.1" + "license": "MIT", + "dependencies": { + "bytes": "~3.1.2", + "http-errors": "~2.0.1", + "iconv-lite": "~0.4.24", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" } }, - "parse-entities": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-1.2.2.tgz", - "integrity": "sha512-NzfpbxW/NPrzZ/yYSoQxyqUZMZXIdCfE0OIN4ESsnptHJECoUk3FZktxNuzQf4tjt5UEopnxpYJbvYuxIFDdsg==", + "node_modules/raw-body/node_modules/http-errors": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz", + "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==", "dev": true, - "requires": { - "character-entities": "^1.0.0", - "character-entities-legacy": "^1.0.0", - "character-reference-invalid": "^1.0.0", - "is-alphanumerical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-hexadecimal": "^1.0.0" + "license": "MIT", + "dependencies": { + "depd": "~2.0.0", + "inherits": "~2.0.4", + "setprototypeof": "~1.2.0", + "statuses": "~2.0.2", + "toidentifier": "~1.0.1" + }, + "engines": { + "node": ">= 0.8" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, - "parse-filepath": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-filepath/-/parse-filepath-1.0.2.tgz", - "integrity": "sha1-pjISf1Oq89FYdvWHLz/6x2PWyJE=", + "node_modules/raw-body/node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", "dev": true, - "requires": { - "is-absolute": "^1.0.0", - "map-cache": "^0.2.0", - "path-root": "^0.1.1" + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" } }, - "parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "requires": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" + "node_modules/raw-body/node_modules/statuses": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz", + "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "parse-passwd": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/parse-passwd/-/parse-passwd-1.0.0.tgz", - "integrity": "sha1-bVuTSkVpk7I9N/QKOC1vFmao5cY=", - "dev": true + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "parse-srcset": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/parse-srcset/-/parse-srcset-1.0.2.tgz", - "integrity": "sha1-8r0iH2zJcKk42IVWq8WJyqqiveE=", - "dev": true + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } }, - "parse5": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", - "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", - "dev": true + "node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" }, - "parse5-htmlparser2-tree-adapter": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", - "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "node_modules/react-refresh": { + "version": "0.14.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.14.0.tgz", + "integrity": "sha512-wViHqhAd8OHeLS/IRMJjTSDHF3U9eWi62F/MledQGPdJGDhodXJ9PBLNGr6WWL7qlH12Mt3TyTpbS+hGXMjCzQ==", "dev": true, - "requires": { - "parse5": "^6.0.1" + "engines": { + "node": ">=0.10.0" } }, - "parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==" - }, - "pascalcase": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/pascalcase/-/pascalcase-0.1.1.tgz", - "integrity": "sha1-s2PlXoAGym/iF4TS2yK9FdeRfxQ=", - "dev": true - }, - "path-browserify": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", - "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==", - "dev": true + "node_modules/read-cache": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/read-cache/-/read-cache-1.0.0.tgz", + "integrity": "sha512-Owdv/Ft7IjOgm/i0xvNDZ1LrRANRfew4b2prF3OWMQLxLfu3bS8FVhCsrSCMK4lR56Y9ya+AThoTpDCTxCmpRA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pify": "^2.3.0" + } }, - "path-dirname": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-dirname/-/path-dirname-1.0.2.tgz", - "integrity": "sha1-zDPSTVJeCZpTiMAzbG4yuRYGCeA=", - "dev": true + "node_modules/read-cache/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "path-exists": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", - "integrity": "sha1-zg6+ql94yxiSXqfYENe1mwEP1RU=" + "node_modules/read-pkg": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", + "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", + "dev": true, + "dependencies": { + "@types/normalize-package-data": "^2.4.0", + "normalize-package-data": "^2.5.0", + "parse-json": "^5.0.0", + "type-fest": "^0.6.0" + }, + "engines": { + "node": ">=8" + } }, - "path-is-absolute": { + "node_modules/read-pkg-up": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=" - }, - "path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", - "dev": true - }, - "path-key": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-2.0.1.tgz", - "integrity": "sha1-QRyttXTFoUDTpLGRDUDYDMn0C0A=", - "dev": true - }, - "path-parse": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.6.tgz", - "integrity": "sha512-GSmOT2EbHrINBf9SR7CDELwlJ8AENk3Qn7OikK4nFYAu3Ote2+JYNVvkpAEQm3/TLNEJFD/xZJjzyxg3KBWOzw==" - }, - "path-root": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/path-root/-/path-root-0.1.1.tgz", - "integrity": "sha1-mkpoFMrBwM1zNgqV8yCDyOpHRbc=", + "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", + "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", "dev": true, - "requires": { - "path-root-regex": "^0.1.0" + "license": "MIT", + "optional": true, + "dependencies": { + "find-up": "^1.0.0", + "read-pkg": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "path-root-regex": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/path-root-regex/-/path-root-regex-0.1.2.tgz", - "integrity": "sha1-v8zcjfWxLcUsi0PsONGNcsBLqW0=", - "dev": true - }, - "path-to-regexp": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", - "integrity": "sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=", - "dev": true - }, - "path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==" - }, - "pbkdf2": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.1.tgz", - "integrity": "sha512-4Ejy1OPxi9f2tt1rRV7Go7zmfDQ+ZectEQz3VGUQhgq62HtIRPDyG/JtnwIxs6x3uNMwo2V7q1fMvKjb+Tnpqg==", + "node_modules/read-pkg-up/node_modules/find-up": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", + "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", "dev": true, - "requires": { - "create-hash": "^1.1.2", - "create-hmac": "^1.1.4", - "ripemd160": "^2.0.1", - "safe-buffer": "^5.0.1", - "sha.js": "^2.4.8" + "license": "MIT", + "optional": true, + "dependencies": { + "path-exists": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "pend": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", - "integrity": "sha1-elfrVQpng/kRUzH89GY9XI4AelA=", - "dev": true - }, - "performance-now": { + "node_modules/read-pkg-up/node_modules/path-exists": { "version": "2.1.0", - "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", - "integrity": "sha1-Ywn04OX6kT7BxpMHrjZLSzd8nns=", - "dev": true - }, - "picomatch": { - "version": "2.2.2", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.2.2.tgz", - "integrity": "sha512-q0M/9eZHzmr0AulXyPwNfZjtwZ/RBZlbN3K3CErVrk50T2ASYI7Bye0EvekFY3IP1Nt2DHu0re+V2ZHIpMkuWg==" - }, - "picturefill": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/picturefill/-/picturefill-3.0.3.tgz", - "integrity": "sha512-JDdx+3i4fs2pkqwWZJgGEM2vFWsq+01YsQFT9CKPGuv2Q0xSdrQZoxi9XwyNARTgxiOdgoAwWQRluLRe/JQX2g==" - }, - "pify": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pify/-/pify-4.0.1.tgz", - "integrity": "sha512-uB80kBFb/tfd68bVleG9T5GGsGPjJrLAUpR5PZIrhBnIaRTQRjqdJSsIKkOP6OAIFbj7GOrcudc5pNjZ+geV2g==", - "dev": true - }, - "pinkie": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", - "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", - "dev": true + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", + "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "pinkie-promise": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", - "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", + "node_modules/read-pkg-up/node_modules/path-type": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", + "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", "dev": true, - "requires": { - "pinkie": "^2.0.0" + "license": "MIT", + "optional": true, + "dependencies": { + "graceful-fs": "^4.1.2", + "pify": "^2.0.0", + "pinkie-promise": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "pirates": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.1.tgz", - "integrity": "sha512-WuNqLTbMI3tmfef2TKxlQmAiLHKtFhlsCZnPIpuv2Ow0RDVO8lfy1Opf4NUzlMXLjPl+Men7AuVdX6TA+s+uGA==", + "node_modules/read-pkg-up/node_modules/pify": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", + "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", "dev": true, - "requires": { - "node-modules-regexp": "^1.0.0" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "pixelmatch": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/pixelmatch/-/pixelmatch-5.2.1.tgz", - "integrity": "sha512-WjcAdYSnKrrdDdqTcVEY7aB7UhhwjYQKYhHiBXdJef0MOaQeYpUdQ+iVyBLa5YBKS8MPVPPMX7rpOByISLpeEQ==", + "node_modules/read-pkg-up/node_modules/read-pkg": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", + "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", "dev": true, - "requires": { - "pngjs": "^4.0.1" - }, + "license": "MIT", + "optional": true, "dependencies": { - "pngjs": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-4.0.1.tgz", - "integrity": "sha512-rf5+2/ioHeQxR6IxuYNYGFytUyG3lma/WW1nsmjeHlWwtb2aByla6dkVc8pmJ9nplzkTA0q2xx7mMWrOTqT4Gg==", - "dev": true - } + "load-json-file": "^1.0.0", + "normalize-package-data": "^2.3.2", + "path-type": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "pkg-dir": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-3.0.0.tgz", - "integrity": "sha512-/E57AYkoeQ25qkxMj5PBOVgF8Kiu/h7cYS30Z5+R7WaiCCBfLq58ZI/dSeaEKb9WVJV5n/03QwrN3IeWIFllvw==", + "node_modules/read-pkg/node_modules/type-fest": { + "version": "0.6.0", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", + "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", "dev": true, - "requires": { - "find-up": "^3.0.0" + "engines": { + "node": ">=8" } }, - "pkg-up": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-2.0.0.tgz", - "integrity": "sha1-yBmscoBZpGHKscOImivjxJoATX8=", - "dev": true, - "requires": { - "find-up": "^2.1.0" - }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", "dependencies": { - "find-up": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-2.1.0.tgz", - "integrity": "sha1-RdG35QbHF93UgndaK3eSCjwMV6c=", - "dev": true, - "requires": { - "locate-path": "^2.0.0" - } - }, - "locate-path": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-2.0.0.tgz", - "integrity": "sha1-K1aLJl7slExtnA3pw9u7ygNUzY4=", - "dev": true, - "requires": { - "p-locate": "^2.0.0", - "path-exists": "^3.0.0" - } - }, - "p-limit": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-1.3.0.tgz", - "integrity": "sha512-vvcXsLAJ9Dr5rQOPk7toZQZJApBl2K4J6dANSsEuh6QI41JYcsS/qhTGa9ErIUUgK3WNQoJYvylxvjqmiqEA9Q==", - "dev": true, - "requires": { - "p-try": "^1.0.0" - } - }, - "p-locate": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-2.0.0.tgz", - "integrity": "sha1-IKAQOyIqcMj9OcwuWAaA893l7EM=", - "dev": true, - "requires": { - "p-limit": "^1.1.0" - } - }, - "p-try": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-try/-/p-try-1.0.0.tgz", - "integrity": "sha1-y8ec26+P1CKOE/Yh8rGiN8GyB7M=", - "dev": true - } + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" } }, - "plur": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/plur/-/plur-4.0.0.tgz", - "integrity": "sha512-4UGewrYgqDFw9vV6zNV+ADmPAUAfJPKtGvb/VdpQAx25X5f3xXdGdyOEVFwkl8Hl/tl7+xbeHqSEM+D5/TirUg==", + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", "dev": true, - "requires": { - "irregular-plurals": "^3.2.0" + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" } }, - "pngjs": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-3.4.0.tgz", - "integrity": "sha512-NCrCHhWmnQklfH4MtJMRjZ2a8c80qXeMlQMv2uVp9ISJMTt562SbGd6n2oq0PaPgKm7Z6pL9E2UlLIhC+SHL3w==", - "dev": true - }, - "polyfill-library": { - "version": "3.105.0", - "resolved": "https://registry.npmjs.org/polyfill-library/-/polyfill-library-3.105.0.tgz", - "integrity": "sha512-Bt10kl+5I/k+F8U0/HYEw2RiHUyUYGz7KtzwhPVltngxcJjzm0pWTQi2Z8pYMI5ahE5B5WZv8CXgCVDxz/H4UA==", - "requires": { - "@financial-times/polyfill-useragent-normaliser": "^1.7.0", - "@formatjs/intl-datetimeformat": "3.2.9", - "@formatjs/intl-displaynames": "4.0.7", - "@formatjs/intl-getcanonicallocales": "1.5.3", - "@formatjs/intl-listformat": "5.0.6", - "@formatjs/intl-locale": "2.4.16", - "@formatjs/intl-numberformat": "6.1.5", - "@formatjs/intl-pluralrules": "4.0.7", - "@formatjs/intl-relativetimeformat": "8.0.5", - "@juggle/resize-observer": "^3.2.0", - "@webcomponents/template": "^1.4.0", - "Base64": "^1.0.0", - "abort-controller": "^3.0.0", - "audio-context-polyfill": "^1.0.0", - "current-script-polyfill": "^1.0.0", - "diff": "4.0.2", - "event-source-polyfill": "^1.0.12", - "fastestsmallesttextencoderdecoder": "1.0.22", - "from2-string": "^1.1.0", - "glob": "^7.1.1", - "graceful-fs": "^4.1.10", - "html5shiv": "^3.7.3", - "js-polyfills": "^0.1.40", - "json3": "^3.3.2", - "merge2": "^1.0.3", - "mkdirp": "^1.0.4", - "mnemonist": "^0.38.0", - "mutationobserver-shim": "^0.3.2", - "picturefill": "^3.0.1", - "rimraf": "^3.0.0", - "seamless-scroll-polyfill": "1.2.3", - "spdx-licenses": "^1.0.0", - "stream-cache": "^0.0.2", - "stream-from-promise": "^1.0.0", - "stream-to-string": "^1.1.0", - "toposort": "^2.0.2", - "uglify-js": "^2.7.5", - "unorm": "^1.6.0", - "usertiming": "^0.1.8", - "web-animations-js": "^2.2.5", - "whatwg-fetch": "^3.0.0", - "wicg-inert": "^3.0.0", - "yaku": "1.0.1" - }, - "dependencies": { - "camelcase": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-1.2.1.tgz", - "integrity": "sha1-m7UwTS4LVmmLLHWLCKPqqdqlijk=" - }, - "cliui": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cliui/-/cliui-2.1.0.tgz", - "integrity": "sha1-S0dXYP+AJkx2LDoXGQMukcf+oNE=", - "requires": { - "center-align": "^0.1.1", - "right-align": "^0.1.1", - "wordwrap": "0.0.2" - } - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==" - }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "requires": { - "glob": "^7.1.3" - } - }, - "uglify-js": { - "version": "2.8.29", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-2.8.29.tgz", - "integrity": "sha1-KcVzMUgFe7Th913zW3qcty5qWd0=", - "requires": { - "source-map": "~0.5.1", - "uglify-to-browserify": "~1.0.0", - "yargs": "~3.10.0" - } - }, - "yargs": { - "version": "3.10.0", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-3.10.0.tgz", - "integrity": "sha1-9+572FfdfB0tOMDnTvvWgdFDH9E=", - "requires": { - "camelcase": "^1.0.2", - "cliui": "^2.1.0", - "decamelize": "^1.0.0", - "window-size": "0.1.0" - } - } + "node_modules/rechoir": { + "version": "0.7.1", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.1.tgz", + "integrity": "sha512-/njmZ8s1wVeR6pjTZ+0nCnv8SpZNRMT2D1RLOJQESlYFDBvwpTA4KWJpZ+sBJ4+vhjILRcK7JIFdGCdxEAAitg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.9.0" + }, + "engines": { + "node": ">= 0.10" } }, - "popmotion": { - "version": "9.3.6", - "resolved": "https://registry.npmjs.org/popmotion/-/popmotion-9.3.6.tgz", - "integrity": "sha512-ZTbXiu6zIggXzIliMi8LGxXBF5ST+wkpXGEjeTUDUOCdSQ356hij/xjeUdv0F8zCQNeqB1+PR5/BB+gC+QLAPw==", - "requires": { - "framesync": "5.3.0", - "hey-listen": "^1.0.8", - "style-value-types": "4.1.4", - "tslib": "^2.1.0" - }, + "node_modules/redent": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", + "integrity": "sha512-qtW5hKzGQZqKoh6JNSD+4lfitfPKGz42e6QwiRmPM5mmKtR0N41AbJRYu0xJi7nhOJ4WDgRkKvAk6tw4WIwR4g==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } + "indent-string": "^2.1.0", + "strip-indent": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "portfinder": { - "version": "1.0.28", - "resolved": "https://registry.npmjs.org/portfinder/-/portfinder-1.0.28.tgz", - "integrity": "sha512-Se+2isanIcEqf2XMHjyUKskczxbPH7dQnlMjXX6+dybayyHvAf/TCgyMRlzf/B6QDhAEFOGes0pzRo3by4AbMA==", + "node_modules/reflect.getprototypeof": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", + "integrity": "sha512-00o4I+DVrefhv+nX0ulyi3biSHCPDe+yLv5o/p6d/UVlirijB8E16FtfwSAi4g3tcqrQ4lRAqQSoFEZJehYEcw==", "dev": true, - "requires": { - "async": "^2.6.2", - "debug": "^3.1.1", - "mkdirp": "^0.5.5" - }, "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.9", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.7", + "get-proto": "^1.0.1", + "which-builtin-type": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "posix-character-classes": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/posix-character-classes/-/posix-character-classes-0.1.1.tgz", - "integrity": "sha1-AerA/jta9xoqbAL+q7jB/vfgDqs=", + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", "dev": true }, - "postcss": { - "version": "7.0.35", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz", - "integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==", + "node_modules/regenerate-unicode-properties": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.0.tgz", + "integrity": "sha512-DqHn3DwbmmPVzeKj9woBadqmXxLvQoQIwu7nopMc72ztvxVmVk2SBhSnx67zuye5TP+lJsb/TBQsjLKhnDf3MA==", "dev": true, - "requires": { - "chalk": "^2.4.2", - "source-map": "^0.6.1", - "supports-color": "^6.1.0" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" } }, - "postcss-calc": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz", - "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==", + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", "dev": true, - "requires": { - "postcss": "^7.0.27", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.0.2" - }, "dependencies": { - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - } + "@babel/runtime": "^7.8.4" } }, - "postcss-colormin": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-4.0.3.tgz", - "integrity": "sha512-WyQFAdDZpExQh32j0U0feWisZ0dmOtPl44qYmJKkq9xFWY3p+4qnRzCHeNrkeRhwPHz9bQ3mo0/yVkaply0MNw==", + "node_modules/regex-not": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", + "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "color": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "dependencies": { + "extend-shallow": "^3.0.2", + "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "postcss-convert-values": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-4.0.1.tgz", - "integrity": "sha512-Kisdo1y77KUC0Jmn0OXU/COOJbzM8cImvw1ZFsBgBgMgb1iL23Zs/LXRe3r+EZqM3vGYKdQ2YJVQ5VkJI+zEJQ==", + "node_modules/regexp.prototype.flags": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.4.tgz", + "integrity": "sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==", "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "dependencies": { + "call-bind": "^1.0.8", + "define-properties": "^1.2.1", + "es-errors": "^1.3.0", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "set-function-name": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "postcss-discard-comments": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz", - "integrity": "sha512-RJutN259iuRf3IW7GZyLM5Sw4GLTOH8FmsXBnv8Ab/Tc2k4SR4qbV4DNbyyY4+Sjo362SyDmW2DQ7lBSChrpkg==", + "node_modules/regexpu-core": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.2.0.tgz", + "integrity": "sha512-H66BPQMrv+V16t8xtmq+UC0CBpiTBA60V8ibS1QVReIp8T1z8hwFxqcGzm9K6lgsN7sB5edVH8a+ze6Fqm4weA==", "dev": true, - "requires": { - "postcss": "^7.0.0" + "dependencies": { + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.2.0", + "regjsgen": "^0.8.0", + "regjsparser": "^0.12.0", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" } }, - "postcss-discard-duplicates": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-4.0.2.tgz", - "integrity": "sha512-ZNQfR1gPNAiXZhgENFfEglF93pciw0WxMkJeVmw8eF+JZBbMD7jp6C67GqJAXVZP2BWbOztKfbsdmMp/k8c6oQ==", + "node_modules/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", + "dev": true + }, + "node_modules/regjsparser": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", + "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", "dev": true, - "requires": { - "postcss": "^7.0.0" + "dependencies": { + "jsesc": "~3.0.2" + }, + "bin": { + "regjsparser": "bin/parser" } }, - "postcss-discard-empty": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-4.0.1.tgz", - "integrity": "sha512-B9miTzbznhDjTfjvipfHoqbWKwd0Mj+/fL5s1QOz06wufguil+Xheo4XpOnc4NqKYBCNqqEzgPv2aPBIJLox0w==", + "node_modules/repeat-element": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", + "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", "dev": true, - "requires": { - "postcss": "^7.0.0" + "engines": { + "node": ">=0.10.0" } }, - "postcss-discard-overridden": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-4.0.1.tgz", - "integrity": "sha512-IYY2bEDD7g1XM1IDEsUT4//iEYCxAmP5oDSFMVU/JVvT7gh+l4fmjciLqGgwjdWpQIdb0Che2VX00QObS5+cTg==", + "node_modules/repeat-string": { + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", + "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=", "dev": true, - "requires": { - "postcss": "^7.0.0" + "engines": { + "node": ">=0.10" } }, - "postcss-html": { - "version": "0.36.0", - "resolved": "https://registry.npmjs.org/postcss-html/-/postcss-html-0.36.0.tgz", - "integrity": "sha512-HeiOxGcuwID0AFsNAL0ox3mW6MHH5cstWN1Z3Y+n6H+g12ih7LHdYxWwEA/QmrebctLjo79xz9ouK3MroHwOJw==", + "node_modules/repeating": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", + "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", "dev": true, - "requires": { - "htmlparser2": "^3.10.0" - }, + "optional": true, "dependencies": { - "domhandler": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-2.4.2.tgz", - "integrity": "sha512-JiK04h0Ht5u/80fdLMCEmV4zkNh2BcoMFBmZ/91WtYZ8qVXSKjiw7fXMgFPnHcSZgOo3XdinHvmnDUeMf5R4wA==", - "dev": true, - "requires": { - "domelementtype": "1" - } - }, - "entities": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/entities/-/entities-1.1.2.tgz", - "integrity": "sha512-f2LZMYl1Fzu7YSBKg+RoROelpOaNrcGmE9AZubeDfrCEia483oW4MI4VyFd5VNHIgQ/7qm1I0wUHK1eJnn2y2w==", - "dev": true - }, - "htmlparser2": { - "version": "3.10.1", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-3.10.1.tgz", - "integrity": "sha512-IgieNijUMbkDovyoKObU1DUhm1iwNYE/fuifEoEHfd1oZKZDaONBSkal7Y01shxsM49R4XaMdGez3WnF9UfiCQ==", - "dev": true, - "requires": { - "domelementtype": "^1.3.1", - "domhandler": "^2.3.0", - "domutils": "^1.5.1", - "entities": "^1.1.1", - "inherits": "^2.0.1", - "readable-stream": "^3.1.1" - } - }, - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "is-finite": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "postcss-less": { - "version": "3.1.4", - "resolved": "https://registry.npmjs.org/postcss-less/-/postcss-less-3.1.4.tgz", - "integrity": "sha512-7TvleQWNM2QLcHqvudt3VYjULVB49uiW6XzEUFmvwHzvsOEF5MwBrIXZDJQvJNFGjJQTzSzZnDoCJ8h/ljyGXA==", + "node_modules/replace-ext": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", + "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", "dev": true, - "requires": { - "postcss": "^7.0.14" + "engines": { + "node": ">= 0.10" } }, - "postcss-loader": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-4.3.0.tgz", - "integrity": "sha512-M/dSoIiNDOo8Rk0mUqoj4kpGq91gcxCfb9PoyZVdZ76/AuhxylHDYZblNE8o+EQ9AMSASeMFEKxZf5aU6wlx1Q==", + "node_modules/request": { + "version": "2.88.2", + "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", + "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", "dev": true, - "requires": { - "cosmiconfig": "^7.0.0", - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "semver": "^7.3.4" - }, "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "aws-sign2": "~0.7.0", + "aws4": "^1.8.0", + "caseless": "~0.12.0", + "combined-stream": "~1.0.6", + "extend": "~3.0.2", + "forever-agent": "~0.6.1", + "form-data": "~2.3.2", + "har-validator": "~5.1.3", + "http-signature": "~1.2.0", + "is-typedarray": "~1.0.0", + "isstream": "~0.1.2", + "json-stringify-safe": "~5.0.1", + "mime-types": "~2.1.19", + "oauth-sign": "~0.9.0", + "performance-now": "^2.1.0", + "qs": "~6.5.2", + "safe-buffer": "^5.1.2", + "tough-cookie": "~2.5.0", + "tunnel-agent": "^0.6.0", + "uuid": "^3.3.2" + }, + "engines": { + "node": ">= 6" } }, - "postcss-media-query-parser": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", - "integrity": "sha1-J7Ocb02U+Bsac7j3Y1HGCeXO8kQ=", - "dev": true - }, - "postcss-merge-longhand": { - "version": "4.0.11", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-4.0.11.tgz", - "integrity": "sha512-alx/zmoeXvJjp7L4mxEMjh8lxVlDFX1gqWHzaaQewwMZiVhLo42TEClKaeHbRf6J7j82ZOdTJ808RtN0ZOZwvw==", + "node_modules/request/node_modules/qs": { + "version": "6.5.5", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.5.tgz", + "integrity": "sha512-mzR4sElr1bfCaPJe7m8ilJ6ZXdDaGoObcYR0ZHSsktM/Lt21MVHj5De30GQH2eiZ1qGRTO7LCAzQsUeXTNexWQ==", "dev": true, - "requires": { - "css-color-names": "0.0.4", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "stylehacks": "^4.0.0" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.6" } }, - "postcss-merge-rules": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-4.0.3.tgz", - "integrity": "sha512-U7e3r1SbvYzO0Jr3UT/zKBVgYYyhAz0aitvGIYOYK5CPmkNih+WDSsS5tvPrJ8YMQYlEMvsZIiqmn7HdFUaeEQ==", + "node_modules/request/node_modules/tough-cookie": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", + "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "cssnano-util-same-parent": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0", - "vendors": "^1.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } + "dependencies": { + "psl": "^1.1.28", + "punycode": "^2.1.1" + }, + "engines": { + "node": ">=0.8" } }, - "postcss-minify-font-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-4.0.2.tgz", - "integrity": "sha512-j85oO6OnRU9zPf04+PZv1LYIYOprWm6IA6zkXkrJXyRveDEuQggG6tvoy8ir8ZwjLxLuGfNkCZEQG7zan+Hbtg==", + "node_modules/request/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "bin": { + "uuid": "bin/uuid" } }, - "postcss-minify-gradients": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-4.0.2.tgz", - "integrity": "sha512-qKPfwlONdcf/AndP1U8SJ/uzIJtowHlMaSioKzebAXSG4iJthlWC9iSWznQcX4f66gIWX44RSA841HTHj3wK+Q==", + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "is-color-stop": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">=0.10.0" } }, - "postcss-minify-params": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-4.0.2.tgz", - "integrity": "sha512-G7eWyzEx0xL4/wiBBJxJOz48zAKV2WG3iZOqVhPet/9geefm/Px5uo1fzlHu+DOjT+m0Mmiz3jkQzVHe6wxAWg==", + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "browserslist": "^4.0.0", - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "uniqs": "^2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "postcss-minify-selectors": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-4.0.2.tgz", - "integrity": "sha512-D5S1iViljXBj9kflQo4YutWnJmwm8VvIsU1GeXJGiG9j8CIg9zs4voPMdQDUmIxetUOh60VilsNzCiAFTOqu3g==", - "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } + "node_modules/requireindex": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", + "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.5" } }, - "postcss-modules-extract-imports": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.0.0.tgz", - "integrity": "sha512-bdHleFnP3kZ4NYDhuGlVK+CMrQ/pqUm8bx/oGL93K6gVwiclvX5x0n76fYMKuIGKzlABOy13zsvqjb0f92TEXw==", + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", "dev": true }, - "postcss-modules-local-by-default": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.0.0.tgz", - "integrity": "sha512-sT7ihtmGSF9yhm6ggikHdV0hlziDTX7oFoXtuVWeDd3hHObNkcHRo9V3yg7vCAY7cONyxJC/XXCmmiHHcvX7bQ==", + "node_modules/requizzle": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", + "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", "dev": true, - "requires": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-bin": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/resolve-bin/-/resolve-bin-0.4.3.tgz", + "integrity": "sha512-9u8TMpc+SEHXxQXblXHz5yRvRZERkCZimFN9oz85QI3uhkh7nqfjm6OGTLg+8vucpXGcY4jLK6WkylPmt7GSvw==", + "dev": true, "dependencies": { - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - } + "find-parent-dir": "~0.3.0" } }, - "postcss-modules-scope": { + "node_modules/resolve-cwd": { "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.0.0.tgz", - "integrity": "sha512-hncihwFA2yPath8oZ15PZqvWGkWf+XUfQgUGamS4LqoP1anQLOsOJw0vr7J7IwLpoY9fatA2qiGUGmuZL0Iqlg==", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", "dev": true, - "requires": { - "postcss-selector-parser": "^6.0.4" + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" } }, - "postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "node_modules/resolve-cwd/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", "dev": true, - "requires": { - "icss-utils": "^5.0.0" + "engines": { + "node": ">=8" } }, - "postcss-normalize-charset": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-4.0.1.tgz", - "integrity": "sha512-gMXCrrlWh6G27U0hF3vNvR3w8I1s2wOBILvA87iNXaPvSNo5uZAMYsZG7XjCUf1eVxuPfyL4TJ7++SGZLc9A3g==", + "node_modules/resolve-dir": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", + "integrity": "sha512-QxMPqI6le2u0dCLyiGzgy92kjkkL6zO0XyvHzjdTNH3zM6e5Hz3BwG6+aEyNgiQ5Xz6PwTwgQEj3U50dByPKIA==", "dev": true, - "requires": { - "postcss": "^7.0.0" + "dependencies": { + "expand-tilde": "^1.2.2", + "global-modules": "^0.2.3" + }, + "engines": { + "node": ">=0.10.0" } }, - "postcss-normalize-display-values": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.2.tgz", - "integrity": "sha512-3F2jcsaMW7+VtRMAqf/3m4cPFhPD3EFRgNs18u+k3lTJJlVe7d0YPO+bnwqo2xg8YiRpDXJI2u8A0wqJxMsQuQ==", + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">=4" } }, - "postcss-normalize-positions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-4.0.2.tgz", - "integrity": "sha512-Dlf3/9AxpxE+NF1fJxYDeggi5WwV35MXGFnnoccP/9qDtFrTArZ0D0R+iKcg5WsUd8nUYMIl8yXDCtcrT8JrdA==", + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } }, - "postcss-normalize-repeat-style": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-4.0.2.tgz", - "integrity": "sha512-qvigdYYMpSuoFs3Is/f5nHdRLJN/ITA7huIoCyqqENJe9PvPmLhNLMu7QTjPdtnVf6OcYYO5SHonx4+fbJE1+Q==", + "node_modules/resolve-url": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", + "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", + "deprecated": "https://github.com/lydell/resolve-url#deprecated", + "dev": true + }, + "node_modules/resolve.exports": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/resolve.exports/-/resolve.exports-2.0.2.tgz", + "integrity": "sha512-X2UW6Nw3n/aMgDVy+0rSqgHlv39WZAlZrXCdnbyEiKm17DSqHX4MmQMaST3FbeWR5FTuRcUwYAziZajji0Y7mg==", "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">=10" } }, - "postcss-normalize-string": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-4.0.2.tgz", - "integrity": "sha512-RrERod97Dnwqq49WNz8qo66ps0swYZDSb6rM57kN2J+aoyEAJfZ6bMx0sx/F9TIEX0xthPGCmeyiam/jXif0eA==", + "node_modules/responselike": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", + "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", "dev": true, - "requires": { - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "optional": true, + "dependencies": { + "lowercase-keys": "^1.0.0" } }, - "postcss-normalize-timing-functions": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-4.0.2.tgz", - "integrity": "sha512-acwJY95edP762e++00Ehq9L4sZCEcOPyaHwoaFOhIwWCDfik6YvqsYNxckee65JHLKzuNSSmAdxwD2Cud1Z54A==", + "node_modules/restore-cursor": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", + "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "dependencies": { + "onetime": "^2.0.0", + "signal-exit": "^3.0.2" + }, + "engines": { + "node": ">=4" } }, - "postcss-normalize-unicode": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-4.0.1.tgz", - "integrity": "sha512-od18Uq2wCYn+vZ/qCOeutvHjB5jm57ToxRaMeNuf0nWVHaP9Hua56QyMF6fs/4FSUnVIw0CBPsU0K4LnBPwYwg==", + "node_modules/restore-cursor/node_modules/mimic-fn": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", + "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">=4" } }, - "postcss-normalize-url": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-4.0.1.tgz", - "integrity": "sha512-p5oVaF4+IHwu7VpMan/SSpmpYxcJMtkGppYf0VbdH5B6hN8YNmVyJLuY9FmLQTzY3fag5ESUUHDqM+heid0UVA==", + "node_modules/restore-cursor/node_modules/onetime": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", + "integrity": "sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==", "dev": true, - "requires": { - "is-absolute-url": "^2.0.0", - "normalize-url": "^3.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "normalize-url": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-3.3.0.tgz", - "integrity": "sha512-U+JJi7duF1o+u2pynbp2zXDW2/PADgC30f0GsHZtRh+HOcXHnw137TrNlyxxRvWW5fjKd3bcLHPxofWuCjaeZg==", - "dev": true - } + "mimic-fn": "^1.0.0" + }, + "engines": { + "node": ">=4" } }, - "postcss-normalize-whitespace": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-4.0.2.tgz", - "integrity": "sha512-tO8QIgrsI3p95r8fyqKV+ufKlSHh9hMJqACqbv2XknufqEDhDvbguXGBBqxw9nsQoXWf0qOqppziKJKHMD4GtA==", + "node_modules/ret": { + "version": "0.1.15", + "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", + "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">=0.12" } }, - "postcss-ordered-values": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-4.1.2.tgz", - "integrity": "sha512-2fCObh5UanxvSxeXrtLtlwVThBvHn6MQcu4ksNT2tsaV2Fg76R2CV98W7wNSlX+5/pFwEyaDwKLLoEV7uRybAw==", + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", "dev": true, - "requires": { - "cssnano-util-get-arguments": "^4.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "engines": { + "node": ">= 4" } }, - "postcss-reduce-initial": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-4.0.3.tgz", - "integrity": "sha512-gKWmR5aUulSjbzOfD9AlJiHCGH6AEVLaM0AV+aSioxUDd16qXP1PCh8d1/BGVvpdWn8k/HiK7n6TjeoXN1F7DA==", + "node_modules/reusify": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", + "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "caniuse-api": "^3.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0" + "engines": { + "iojs": ">=1.0.0", + "node": ">=0.10.0" } }, - "postcss-reduce-transforms": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-4.0.2.tgz", - "integrity": "sha512-EEVig1Q2QJ4ELpJXMZR8Vt5DQx8/mo+dGWSR7vWXqcob2gQLyQGsionYcGKATXvQzMPn6DSN1vTN7yFximdIAg==", + "node_modules/rimraf": { + "version": "2.7.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", + "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", "dev": true, - "requires": { - "cssnano-util-get-match": "^4.0.0", - "has": "^1.0.0", - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0" + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" } }, - "postcss-resolve-nested-selector": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/postcss-resolve-nested-selector/-/postcss-resolve-nested-selector-0.1.1.tgz", - "integrity": "sha1-Kcy8fDfe36wwTp//C/FZaz9qDk4=", - "dev": true - }, - "postcss-safe-parser": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/postcss-safe-parser/-/postcss-safe-parser-4.0.2.tgz", - "integrity": "sha512-Uw6ekxSWNLCPesSv/cmqf2bY/77z11O7jZGPax3ycZMFU/oi2DMH9i89AdHc1tRwFg/arFoEwX0IS3LCUxJh1g==", + "node_modules/robots-parser": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/robots-parser/-/robots-parser-3.0.1.tgz", + "integrity": "sha512-s+pyvQeIKIZ0dx5iJiQk1tPLJAWln39+MI5jtM8wnyws+G5azk+dMnMX0qfbqNetKKNgcWWOdi0sfm+FbQbgdQ==", "dev": true, - "requires": { - "postcss": "^7.0.26" + "engines": { + "node": ">=10.0.0" } }, - "postcss-sass": { - "version": "0.4.4", - "resolved": "https://registry.npmjs.org/postcss-sass/-/postcss-sass-0.4.4.tgz", - "integrity": "sha512-BYxnVYx4mQooOhr+zer0qWbSPYnarAy8ZT7hAQtbxtgVf8gy+LSLT/hHGe35h14/pZDTw1DsxdbrwxBN++H+fg==", + "node_modules/rrweb-cssom": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rrweb-cssom/-/rrweb-cssom-0.8.0.tgz", + "integrity": "sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==", "dev": true, - "requires": { - "gonzales-pe": "^4.3.0", - "postcss": "^7.0.21" - } + "license": "MIT" }, - "postcss-scss": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/postcss-scss/-/postcss-scss-2.1.1.tgz", - "integrity": "sha512-jQmGnj0hSGLd9RscFw9LyuSVAa5Bl1/KBPqG1NQw9w8ND55nY4ZEsdlVuYJvLPpV+y0nwTV5v/4rHPzZRihQbA==", + "node_modules/rtlcss": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.6.2.tgz", + "integrity": "sha512-06LFAr+GAPo+BvaynsXRfoYTJvSaWRyOhURCQ7aeI1MKph9meM222F+Zkt3bDamyHHJuGi3VPtiRkpyswmQbGA==", "dev": true, - "requires": { - "postcss": "^7.0.6" + "dependencies": { + "@choojs/findup": "^0.2.1", + "chalk": "^2.4.2", + "mkdirp": "^0.5.1", + "postcss": "^6.0.23", + "strip-json-comments": "^2.0.0" + }, + "bin": { + "rtlcss": "bin/rtlcss.js" } }, - "postcss-selector-parser": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.4.tgz", - "integrity": "sha512-gjMeXBempyInaBqpp8gODmwZ52WaYsVOsfr4L4lDQ7n3ncD6mEyySiDtgzCT+NYC0mmeOLvtsF8iaEf0YT6dBw==", + "node_modules/rtlcss/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", "dev": true, - "requires": { - "cssesc": "^3.0.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1", - "util-deprecate": "^1.0.2" + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "postcss-svgo": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-4.0.3.tgz", - "integrity": "sha512-NoRbrcMWTtUghzuKSoIm6XV+sJdvZ7GZSc3wdBN0W19FTtp2ko8NqLsgoh/m9CzNhU3KLPvQmjIwtaNFkaFTvw==", + "node_modules/rtlcss/node_modules/postcss": { + "version": "6.0.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", + "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", "dev": true, - "requires": { - "postcss": "^7.0.0", - "postcss-value-parser": "^3.0.0", - "svgo": "^1.0.0" + "dependencies": { + "chalk": "^2.4.1", + "source-map": "^0.6.1", + "supports-color": "^5.4.0" + }, + "engines": { + "node": ">=4.0.0" } }, - "postcss-syntax": { - "version": "0.36.2", - "resolved": "https://registry.npmjs.org/postcss-syntax/-/postcss-syntax-0.36.2.tgz", - "integrity": "sha512-nBRg/i7E3SOHWxF3PpF5WnJM/jQ1YpY9000OaVXlAQj6Zp/kIqJxEDWIZ67tAd7NLuk7zqN4yqe9nc0oNAOs1w==", - "dev": true - }, - "postcss-unique-selectors": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-4.0.1.tgz", - "integrity": "sha512-+JanVaryLo9QwZjKrmJgkI4Fn8SBgRO6WXQBJi7KiAVPlmxikB5Jzc4EvXMT2H0/m0RjrVVm9rGNhZddm/8Spg==", + "node_modules/rtlcss/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, - "requires": { - "alphanum-sort": "^1.0.0", - "postcss": "^7.0.0", - "uniqs": "^2.0.0" + "engines": { + "node": ">=0.10.0" } }, - "postcss-value-parser": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-3.3.1.tgz", - "integrity": "sha512-pISE66AbVkp4fDQ7VHBwRNXzAAKJjw4Vw7nWI/+Q3vuly7SNfgYXvm6i5IgFylHGK5sP/xHAbB7N49OS4gWNyQ==", - "dev": true - }, - "prelude-ls": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", - "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", - "dev": true - }, - "prepend-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/prepend-http/-/prepend-http-1.0.4.tgz", - "integrity": "sha1-1PRWKwzjaW5BrFLQ4ALlemNdxtw=", + "node_modules/rtlcss/node_modules/strip-json-comments": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", + "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", "dev": true, - "optional": true + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "prettier": { - "version": "npm:wp-prettier@2.0.5", - "resolved": "https://registry.npmjs.org/wp-prettier/-/wp-prettier-2.0.5.tgz", - "integrity": "sha512-5GCgdeevIXwR3cW4Qj5XWC5MO1iSCz8+IPn0mMw6awAt/PBiey8yyO7MhePRsaMqghJAhg6Q3QLYWSnUHWkG6A==", - "dev": true + "node_modules/run-async": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", + "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } }, - "prettier-linter-helpers": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/prettier-linter-helpers/-/prettier-linter-helpers-1.0.0.tgz", - "integrity": "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w==", + "node_modules/run-con": { + "version": "1.2.11", + "resolved": "https://registry.npmjs.org/run-con/-/run-con-1.2.11.tgz", + "integrity": "sha512-NEMGsUT+cglWkzEr4IFK21P4Jca45HqiAbIIZIBdX5+UZTB24Mb/21iNGgz9xZa8tL6vbW7CXmq7MFN42+VjNQ==", "dev": true, - "requires": { - "fast-diff": "^1.1.2" + "dependencies": { + "deep-extend": "^0.6.0", + "ini": "~3.0.0", + "minimist": "^1.2.6", + "strip-json-comments": "~3.1.1" + }, + "bin": { + "run-con": "cli.js" } }, - "pretty-bytes": { + "node_modules/run-con/node_modules/ini": { "version": "3.0.1", - "resolved": "https://registry.npmjs.org/pretty-bytes/-/pretty-bytes-3.0.1.tgz", - "integrity": "sha1-J9AAjXeAY6C0gRuzXHnxvV1fvM8=", + "resolved": "https://registry.npmjs.org/ini/-/ini-3.0.1.tgz", + "integrity": "sha512-it4HyVAUTKBc6m8e1iXWvXSTdndF7HbdN713+kvLrymxTaU4AUBWrJ4vEooP+V7fexnVD3LKcBshjGGPefSMUQ==", "dev": true, - "requires": { - "number-is-nan": "^1.0.0" + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "pretty-format": { - "version": "26.6.2", - "resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-26.6.2.tgz", - "integrity": "sha512-7AeGuCYNGmycyQbCqd/3PWH4eOoX/OiCa0uphp57NVTeAGdJGaAliecxwBDHYQCIvrW7aDBZCYeNTP/WX69mkg==", + "node_modules/run-parallel": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", + "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", "dev": true, - "requires": { - "@jest/types": "^26.6.2", - "ansi-regex": "^5.0.0", - "ansi-styles": "^4.0.0", - "react-is": "^17.0.1" - }, - "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } + { + "type": "patreon", + "url": "https://www.patreon.com/feross" }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true + { + "type": "consulting", + "url": "https://feross.org/support" } - } - }, - "process": { - "version": "0.11.10", - "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", - "integrity": "sha1-czIwDoQBYb2j5podHZGn1LwW8YI=", - "dev": true - }, - "process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" - }, - "progress": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", - "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", - "dev": true - }, - "promise-inflight": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz", - "integrity": "sha1-mEcocL8igTL8vdhoEputEsPAKeM=", - "dev": true + ] }, - "promise-polyfill": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/promise-polyfill/-/promise-polyfill-1.1.6.tgz", - "integrity": "sha1-zQTv9G9clcOn0EVZHXm14+AfEtc=" - }, - "prompts": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.1.tgz", - "integrity": "sha512-EQyfIuO2hPDsX1L/blblV+H7I0knhgAd82cVneCwcdND9B8AuCDuRcBH6yIcG4dFzlOUqbazQqwGjx5xmsNLuQ==", + "node_modules/rxjs": { + "version": "5.5.12", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", + "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", "dev": true, - "requires": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - } - }, - "prop-types": { - "version": "15.7.2", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.7.2.tgz", - "integrity": "sha512-8QQikdH7//R2vurIJSutZ1smHYTcLpRWEOlHnzcWHmBYrOGUysKwSsrC89BCiFj3CbrfJ/nXFdJepOVrY1GCHQ==", - "requires": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.8.1" - } - }, - "prop-types-exact": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/prop-types-exact/-/prop-types-exact-1.2.0.tgz", - "integrity": "sha512-K+Tk3Kd9V0odiXFP9fwDHUYRyvK3Nun3GVyPapSIs5OBkITAm15W0CPFD/YKTkMUAbc0b9CUwRQp2ybiBIq+eA==", - "requires": { - "has": "^1.0.3", - "object.assign": "^4.1.0", - "reflect.ownkeys": "^0.2.0" + "dependencies": { + "symbol-observable": "1.0.1" + }, + "engines": { + "npm": ">=2.0.0" } }, - "proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha1-IS1b/hMYMGpCD2QCuOJv85ZHqEk=", + "node_modules/rxjs/node_modules/symbol-observable": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", + "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", "dev": true, - "optional": true + "engines": { + "node": ">=0.10.0" + } }, - "proxy-addr": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.6.tgz", - "integrity": "sha512-dh/frvCBVmSsDYzw6n926jv974gddhkFPfiN8hPOi30Wax25QZyZEGveluCgliBnqmuM+UJmBErbAUFIoDbjOw==", + "node_modules/safe-array-concat": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/safe-array-concat/-/safe-array-concat-1.1.3.tgz", + "integrity": "sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==", "dev": true, - "requires": { - "forwarded": "~0.1.2", - "ipaddr.js": "1.9.1" + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "get-intrinsic": "^1.2.6", + "has-symbols": "^1.1.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">=0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "proxy-from-env": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", - "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "node_modules/safe-array-concat/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "prr": { + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-json-parse": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/prr/-/prr-1.0.1.tgz", - "integrity": "sha1-0/wRS6BplaRexok/SEzrHXj19HY=", + "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", + "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", "dev": true }, - "pseudomap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/pseudomap/-/pseudomap-1.0.2.tgz", - "integrity": "sha1-8FKijacOYYkX7wqKw0wa5aaChrM=", - "dev": true + "node_modules/safe-push-apply": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/safe-push-apply/-/safe-push-apply-1.0.0.tgz", + "integrity": "sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "isarray": "^2.0.5" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "psl": { - "version": "1.8.0", - "resolved": "https://registry.npmjs.org/psl/-/psl-1.8.0.tgz", - "integrity": "sha512-RIdOzyoavK+hA18OGGWDqUTsCLhtA7IcZ/6NCs4fFJaHBDab+pDDmDIByWFRQJq2Cd7r1OoQxBGKOaztq+hjIQ==", + "node_modules/safe-push-apply/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", "dev": true }, - "public-encrypt": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", - "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", - "dev": true, - "requires": { - "bn.js": "^4.1.0", - "browserify-rsa": "^4.0.0", - "create-hash": "^1.1.0", - "parse-asn1": "^5.0.0", - "randombytes": "^2.0.1", - "safe-buffer": "^5.1.2" - }, - "dependencies": { - "bn.js": { - "version": "4.11.9", - "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.11.9.tgz", - "integrity": "sha512-E6QoYqCKZfgatHTdHzs1RRKP7ip4vvm+EyRUeE2RF0NblwVvb0p6jSVeNTOFxPn26QXN2o6SMfNxKp6kU8zQaw==", - "dev": true - } + "node_modules/safe-regex": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", + "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "dev": true, + "dependencies": { + "ret": "~0.1.10" } }, - "pump": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", - "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "pumpify": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", - "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/sass": { + "version": "1.98.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.98.0.tgz", + "integrity": "sha512-+4N/u9dZ4PrgzGgPlKnaaRQx64RO0JBKs9sDhQ2pLgN6JQZ25uPQZKQYaBJU48Kd5BxgXoJ4e09Dq7nMcOUW3A==", "dev": true, - "requires": { - "duplexify": "^3.6.0", - "inherits": "^2.0.3", - "pump": "^2.0.0" - }, - "dependencies": { - "pump": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", - "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", - "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "once": "^1.3.1" - } - } + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.0", + "immutable": "^5.1.5", + "source-map-js": ">=0.6.2 <2.0.0" + }, + "bin": { + "sass": "sass.js" + }, + "engines": { + "node": ">=14.0.0" + }, + "optionalDependencies": { + "@parcel/watcher": "^2.4.1" } }, - "punycode": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.1.1.tgz", - "integrity": "sha512-XRsRjdf+j5ml+y/6GKHPZbrF/8p2Yga0JPtdqTIY2Xe5ohJPD9saDJJLPvp9+NSBprVvevdXZybnj2cv8OEd0A==" - }, - "puppeteer-core": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/puppeteer-core/-/puppeteer-core-9.1.1.tgz", - "integrity": "sha512-zbedbitVIGhmgz0nt7eIdLsnaoVZSlNJfBivqm2w67T8LR2bU1dvnruDZ8nQO0zn++Iet7zHbAOdnuS5+H2E7A==", + "node_modules/sass-loader": { + "version": "16.0.4", + "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", + "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", "dev": true, - "requires": { - "debug": "^4.1.0", - "devtools-protocol": "0.0.869402", - "extract-zip": "^2.0.0", - "https-proxy-agent": "^5.0.0", - "node-fetch": "^2.6.1", - "pkg-dir": "^4.2.0", - "progress": "^2.0.1", - "proxy-from-env": "^1.1.0", - "rimraf": "^3.0.2", - "tar-fs": "^2.0.0", - "unbzip2-stream": "^1.3.3", - "ws": "^7.2.3" - }, - "dependencies": { - "agent-base": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", - "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", - "dev": true, - "requires": { - "debug": "4" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "https-proxy-agent": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.0.tgz", - "integrity": "sha512-EkYm5BcKUGiduxzSt3Eppko+PiNWNEpa4ySk9vTC6wDsQJW9rHSa+UhGNJoRYp7bz6Ht1eaRIa6QaJqO5rCFbA==", - "dev": true, - "requires": { - "agent-base": "6", - "debug": "4" - } - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } + "dependencies": { + "neo-async": "^2.6.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", + "sass": "^1.3.0", + "sass-embedded": "*", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } + "node-sass": { + "optional": true }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true + "sass": { + "optional": true }, - "pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "requires": { - "find-up": "^4.0.0" - } + "sass-embedded": { + "optional": true }, - "rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "dev": true, - "requires": { - "glob": "^7.1.3" - } + "webpack": { + "optional": true } } }, - "q": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/q/-/q-1.5.1.tgz", - "integrity": "sha1-fjL3W0E4EpHQRhHxvxQQmsAGUdc=", + "node_modules/sass/node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sass/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/sax": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", + "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", "dev": true }, - "qjobs": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", - "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==" + "node_modules/saxes": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/saxes/-/saxes-6.0.0.tgz", + "integrity": "sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==", + "dev": true, + "license": "ISC", + "dependencies": { + "xmlchars": "^2.2.0" + }, + "engines": { + "node": ">=v12.22.7" + } }, - "qs": { - "version": "6.9.4", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.9.4.tgz", - "integrity": "sha512-A1kFqHekCTM7cz0udomYUoYNWjBebHm/5wzU/XqrBRBNWectVH0QIiN+NEcZ0Dte5hvzHwbr8+XQmguPhJ6WdQ==", - "dev": true + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } }, - "querystring": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/querystring/-/querystring-0.2.0.tgz", - "integrity": "sha1-sgmEkgO7Jd+CDadW50cAWHhSFiA=", - "dev": true + "node_modules/schema-utils": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", + "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", + "dev": true, + "dependencies": { + "@types/json-schema": "^7.0.8", + "ajv": "^6.12.5", + "ajv-keywords": "^3.5.2" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } }, - "querystring-es3": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", - "integrity": "sha1-nsYfeQSYdXB9aUFFlv2Qek1xHnM=", - "dev": true + "node_modules/seek-bzip": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", + "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "dev": true, + "optional": true, + "dependencies": { + "commander": "^2.8.1" + }, + "bin": { + "seek-bunzip": "bin/seek-bunzip", + "seek-table": "bin/seek-bzip-table" + } }, - "querystringify": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/querystringify/-/querystringify-2.2.0.tgz", - "integrity": "sha512-FIqgj2EUvTa7R50u0rGsyTftzjYmv/a3hO345bZNrqabNqjtgiDMgmo4mkUjd+nzU5oF3dClKqFIPUKybUyqoQ==", - "dev": true + "node_modules/select": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", + "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" }, - "quick-lru": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", + "node_modules/select-hose": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", + "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", "dev": true }, - "qunit": { - "version": "2.16.0", - "resolved": "https://registry.npmjs.org/qunit/-/qunit-2.16.0.tgz", - "integrity": "sha512-88x9t+rRMbB6IrCIUZvYU4pJy7NiBEv7SX8jD4LZAsIj+dV+kwGnFStOmPNvqa6HM96VZMD8CIIFKH2+3qvluA==", + "node_modules/selfsigned": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", + "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", "dev": true, - "requires": { - "commander": "7.1.0", - "node-watch": "0.7.1", - "tiny-glob": "0.2.8" - }, "dependencies": { - "commander": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.1.0.tgz", - "integrity": "sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==", - "dev": true - } + "@types/node-forge": "^1.3.0", + "node-forge": "^1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" } }, - "raf": { - "version": "3.4.1", - "resolved": "https://registry.npmjs.org/raf/-/raf-3.4.1.tgz", - "integrity": "sha512-Sq4CW4QhwOHE8ucn6J34MqtZCeWFP2aQSmrlroYgqAV1PjStIhJXxYuTgUIfkEk7zTLjmIjLmU5q+fbD1NnOJA==", + "node_modules/semver-regex": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", + "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", "dev": true, - "requires": { - "performance-now": "^2.1.0" + "optional": true, + "engines": { + "node": ">=6" } }, - "railroad-diagrams": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/railroad-diagrams/-/railroad-diagrams-1.0.0.tgz", - "integrity": "sha1-635iZ1SN3t+4mcG5Dlc3RVnN234=", - "dev": true + "node_modules/semver-truncate": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", + "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "dev": true, + "optional": true, + "dependencies": { + "semver": "^5.3.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "randexp": { - "version": "0.4.6", - "resolved": "https://registry.npmjs.org/randexp/-/randexp-0.4.6.tgz", - "integrity": "sha512-80WNmd9DA0tmZrw9qQa62GPPWfuXJknrmVmLcxvq4uZBdYqb1wYoKTmnlGUchvVWe0XiLupYkBoXVOxz3C8DYQ==", + "node_modules/semver-truncate/node_modules/semver": { + "version": "5.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", + "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", "dev": true, - "requires": { - "discontinuous-range": "1.0.0", - "ret": "~0.1.10" + "optional": true, + "bin": { + "semver": "bin/semver" } }, - "randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "node_modules/send": { + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", + "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", "dev": true, - "requires": { - "safe-buffer": "^5.1.0" + "license": "MIT", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "randomfill": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", - "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "node_modules/send/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "requires": { - "randombytes": "^2.0.5", - "safe-buffer": "^5.1.0" + "license": "MIT", + "dependencies": { + "ms": "2.0.0" } }, - "range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==" + "node_modules/send/node_modules/debug/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true, + "license": "MIT" }, - "raw-body": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.4.0.tgz", - "integrity": "sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==", - "requires": { - "bytes": "3.1.0", - "http-errors": "1.7.2", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "node_modules/send/node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "node_modules/sentence-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/sentence-case/-/sentence-case-3.0.4.tgz", + "integrity": "sha512-8LS0JInaQMCRoQ7YUytAo/xUu5W2XnQxV2HI/6uM6U7CITS1RqPElr30V6uIqyMKM9lJGRVFy5/4CuzcixNYSg==", "dev": true, - "requires": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, "dependencies": { - "deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } + "no-case": "^3.0.4", + "tslib": "^2.0.3", + "upper-case-first": "^2.0.2" } }, - "re-resizable": { - "version": "6.9.0", - "resolved": "https://registry.npmjs.org/re-resizable/-/re-resizable-6.9.0.tgz", - "integrity": "sha512-3cUDG81ylyqI0Pdgle/RHwwRYq0ORZzsUaySOCO8IbEtNyaRtrIHYm/jMQ5pjcNiKCxR3vsSymIQZHwJq4gg2Q==", - "requires": { - "fast-memoize": "^2.5.1" + "node_modules/serialize-javascript": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", + "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" } }, - "react": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react/-/react-16.13.1.tgz", - "integrity": "sha512-YMZQQq32xHLX0bz5Mnibv1/LHb3Sqzngu7xstSM+vrkE5Kzr9xE0yMByK5kMoTK30YVJE61WfbxIFFvfeDKT1w==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2" + "node_modules/serve-index": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", + "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "batch": "0.6.1", + "debug": "2.6.9", + "escape-html": "~1.0.3", + "http-errors": "~1.6.2", + "mime-types": "~2.1.17", + "parseurl": "~1.3.2" + }, + "engines": { + "node": ">= 0.8.0" } }, - "react-addons-shallow-compare": { - "version": "15.6.3", - "resolved": "https://registry.npmjs.org/react-addons-shallow-compare/-/react-addons-shallow-compare-15.6.3.tgz", - "integrity": "sha512-EDJbgKTtGRLhr3wiGDXK/+AEJ59yqGS+tKE6mue0aNXT6ZMR7VJbbzIiT6akotmHg1BLj46ElJSb+NBMp80XBg==", - "requires": { - "object-assign": "^4.1.0" + "node_modules/serve-index/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" } }, - "react-autosize-textarea": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/react-autosize-textarea/-/react-autosize-textarea-7.1.0.tgz", - "integrity": "sha512-BHpjCDkuOlllZn3nLazY2F8oYO1tS2jHnWhcjTWQdcKiiMU6gHLNt/fzmqMSyerR0eTdKtfSIqtSeTtghNwS+g==", - "requires": { - "autosize": "^4.0.2", - "line-height": "^0.3.1", - "prop-types": "^15.5.6" - } - }, - "react-dates": { - "version": "17.2.0", - "resolved": "https://registry.npmjs.org/react-dates/-/react-dates-17.2.0.tgz", - "integrity": "sha512-RDlerU8DdRRrlYS0MQ7Z9igPWABGLDwz6+ykBNff67RM3Sset2TDqeuOr+R5o00Ggn5U47GeLsGcSDxlZd9cHw==", - "requires": { - "airbnb-prop-types": "^2.10.0", - "consolidated-events": "^1.1.1 || ^2.0.0", - "is-touch-device": "^1.0.1", - "lodash": "^4.1.1", - "object.assign": "^4.1.0", - "object.values": "^1.0.4", - "prop-types": "^15.6.1", - "react-addons-shallow-compare": "^15.6.2", - "react-moment-proptypes": "^1.6.0", - "react-outside-click-handler": "^1.2.0", - "react-portal": "^4.1.5", - "react-with-styles": "^3.2.0", - "react-with-styles-interface-css": "^4.0.2" - } - }, - "react-dom": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-16.13.1.tgz", - "integrity": "sha512-81PIMmVLnCNLO/fFOQxdQkvEq/+Hfpv24XNJfpyZhTRfO0QcmQIF/PgCa1zCOj2w1hrn12MFLyaJ/G0+Mxtfag==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1", - "prop-types": "^15.6.2", - "scheduler": "^0.19.1" + "node_modules/serve-index/node_modules/depd": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", + "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", + "dev": true, + "engines": { + "node": ">= 0.6" } }, - "react-easy-crop": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/react-easy-crop/-/react-easy-crop-3.5.2.tgz", - "integrity": "sha512-cwSGO/wk42XDpEyrdAcnQ6OJetVDZZO2ry1i19+kSGZQ750aN06RU9y9z95B5QI6sW3SnaWQRKv5r5GSqVV//g==", - "requires": { - "normalize-wheel": "^1.0.1", - "tslib": "2.0.1" - }, + "node_modules/serve-index/node_modules/http-errors": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", + "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", + "dev": true, "dependencies": { - "tslib": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.0.1.tgz", - "integrity": "sha512-SgIkNheinmEBgx1IUNirK0TUD4X9yjjBRTqqjggWCU3pUEqIk3/Uwl3yRixYKT6WjQuGiwDv4NomL3wqRCj+CQ==" - } + "depd": "~1.1.2", + "inherits": "2.0.3", + "setprototypeof": "1.1.0", + "statuses": ">= 1.4.0 < 2" + }, + "engines": { + "node": ">= 0.6" } }, - "react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + "node_modules/serve-index/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", + "dev": true }, - "react-moment-proptypes": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/react-moment-proptypes/-/react-moment-proptypes-1.8.1.tgz", - "integrity": "sha512-Er940DxWoObfIqPrZNfwXKugjxMIuk1LAuEzn23gytzV6hKS/sw108wibi9QubfMN4h+nrlje8eUCSbQRJo2fQ==", - "requires": { - "moment": ">=1.6.0" - } + "node_modules/serve-index/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true }, - "react-native-url-polyfill": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-native-url-polyfill/-/react-native-url-polyfill-1.3.0.tgz", - "integrity": "sha512-w9JfSkvpqqlix9UjDvJjm1EjSt652zVQ6iwCIj1cVVkwXf4jQhQgTNXY6EVTwuAmUjg6BC6k9RHCBynoLFo3IQ==", - "requires": { - "whatwg-url-without-unicode": "8.0.0-3" - } + "node_modules/serve-index/node_modules/setprototypeof": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", + "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", + "dev": true }, - "react-outside-click-handler": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/react-outside-click-handler/-/react-outside-click-handler-1.3.0.tgz", - "integrity": "sha512-Te/7zFU0oHpAnctl//pP3hEAeobfeHMyygHB8MnjP6sX5OR8KHT1G3jmLsV3U9RnIYo+Yn+peJYWu+D5tUS8qQ==", - "requires": { - "airbnb-prop-types": "^2.15.0", - "consolidated-events": "^1.1.1 || ^2.0.0", - "document.contains": "^1.0.1", - "object.values": "^1.1.0", - "prop-types": "^15.7.2" + "node_modules/serve-index/node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" } }, - "react-portal": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/react-portal/-/react-portal-4.2.1.tgz", - "integrity": "sha512-fE9kOBagwmTXZ3YGRYb4gcMy+kSA+yLO0xnPankjRlfBv4uCpFXqKPfkpsGQQR15wkZ9EssnvTOl1yMzbkxhPQ==", - "requires": { - "prop-types": "^15.5.8" + "node_modules/serve-static": { + "version": "1.16.2", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", + "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "encodeurl": "~2.0.0", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.19.0" + }, + "engines": { + "node": ">= 0.8.0" } }, - "react-resize-aware": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/react-resize-aware/-/react-resize-aware-3.1.0.tgz", - "integrity": "sha512-bIhHlxVTX7xKUz14ksXMEHjzCZPTpQZKZISY3nbTD273pDKPABGFNFBP6Tr42KECxzC5YQiKpMchjTVJCqaxpA==" - }, - "react-shallow-renderer": { - "version": "16.14.1", - "resolved": "https://registry.npmjs.org/react-shallow-renderer/-/react-shallow-renderer-16.14.1.tgz", - "integrity": "sha512-rkIMcQi01/+kxiTE9D3fdS959U1g7gs+/rborw++42m1O9FAQiNI/UNRZExVUoAOprn4umcXf+pFRou8i4zuBg==", + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^16.12.0 || ^17.0.0" + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "react-spring": { - "version": "8.0.27", - "resolved": "https://registry.npmjs.org/react-spring/-/react-spring-8.0.27.tgz", - "integrity": "sha512-nDpWBe3ZVezukNRandTeLSPcwwTMjNVu1IDq9qA/AMiUqHuRN4BeSWvKr3eIxxg1vtiYiOLy4FqdfCP5IoP77g==", - "requires": { - "@babel/runtime": "^7.3.1", - "prop-types": "^15.5.8" + "node_modules/set-function-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/set-function-name/-/set-function-name-2.0.2.tgz", + "integrity": "sha512-7PGFlmtwsEADb0WYyvCMa1t+yke6daIG4Wirafur5kcf+MhUnPms1UeR0CKQdTZD81yESwMHbtn+TR+dMviakQ==", + "dev": true, + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "functions-have-names": "^1.2.3", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" } }, - "react-test-renderer": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-test-renderer/-/react-test-renderer-17.0.2.tgz", - "integrity": "sha512-yaQ9cB89c17PUb0x6UfWRs7kQCorVdHlutU1boVPEsB8IDZH6n9tHxMacc3y0JoXOJUsZb/t/Mb8FUWMKaM7iQ==", + "node_modules/set-proto": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/set-proto/-/set-proto-1.0.0.tgz", + "integrity": "sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==", "dev": true, - "requires": { - "object-assign": "^4.1.1", - "react-is": "^17.0.2", - "react-shallow-renderer": "^16.13.1", - "scheduler": "^0.20.2" - }, "dependencies": { - "react-is": { - "version": "17.0.2", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-17.0.2.tgz", - "integrity": "sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==", - "dev": true - }, - "scheduler": { - "version": "0.20.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.20.2.tgz", - "integrity": "sha512-2eWfGgAqqWFGqtdMmcL5zCMK1U8KlXv8SQFGglL3CEtd0aDVDWgeF/YoCmvln55m5zSk3J/20hTaSBeSObsQDQ==", - "dev": true, - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" - } - } + "dunder-proto": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" } }, - "react-use-gesture": { - "version": "9.1.3", - "resolved": "https://registry.npmjs.org/react-use-gesture/-/react-use-gesture-9.1.3.tgz", - "integrity": "sha512-CdqA2SmS/fj3kkS2W8ZU8wjTbVBAIwDWaRprX7OKaj7HlGwBasGEFggmk5qNklknqk9zK/h8D355bEJFTpqEMg==" - }, - "react-with-direction": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/react-with-direction/-/react-with-direction-1.3.1.tgz", - "integrity": "sha512-aGcM21ZzhqeXFvDCfPj0rVNYuaVXfTz5D3Rbn0QMz/unZe+CCiLHthrjQWO7s6qdfXORgYFtmS7OVsRgSk5LXQ==", - "requires": { - "airbnb-prop-types": "^2.10.0", - "brcast": "^2.0.2", - "deepmerge": "^1.5.2", - "direction": "^1.0.2", - "hoist-non-react-statics": "^3.3.0", - "object.assign": "^4.1.0", - "object.values": "^1.0.4", - "prop-types": "^15.6.2" - } - }, - "react-with-styles": { - "version": "3.2.3", - "resolved": "https://registry.npmjs.org/react-with-styles/-/react-with-styles-3.2.3.tgz", - "integrity": "sha512-MTI1UOvMHABRLj5M4WpODfwnveHaip6X7QUMI2x6zovinJiBXxzhA9AJP7MZNaKqg1JRFtHPXZdroUC8KcXwlQ==", - "requires": { - "hoist-non-react-statics": "^3.2.1", - "object.assign": "^4.1.0", - "prop-types": "^15.6.2", - "react-with-direction": "^1.3.0" + "node_modules/set-value": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", + "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "dev": true, + "dependencies": { + "extend-shallow": "^2.0.1", + "is-extendable": "^0.1.1", + "is-plain-object": "^2.0.3", + "split-string": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "react-with-styles-interface-css": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/react-with-styles-interface-css/-/react-with-styles-interface-css-4.0.3.tgz", - "integrity": "sha512-wE43PIyjal2dexxyyx4Lhbcb+E42amoYPnkunRZkb9WTA+Z+9LagbyxwsI352NqMdFmghR0opg29dzDO4/YXbw==", - "requires": { - "array.prototype.flat": "^1.2.1", - "global-cache": "^1.2.1" + "node_modules/set-value/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "read-pkg": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-3.0.0.tgz", - "integrity": "sha1-nLxoaXj+5l0WwA4rGcI3/Pbjg4k=", + "node_modules/set-value/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", "dev": true, - "requires": { - "load-json-file": "^4.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^3.0.0" - }, "dependencies": { - "load-json-file": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-4.0.0.tgz", - "integrity": "sha1-L19Fq5HjMhYjT9U62rZo607AmTs=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "parse-json": "^4.0.0", - "pify": "^3.0.0", - "strip-bom": "^3.0.0" - } - }, - "parse-json": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-4.0.0.tgz", - "integrity": "sha1-vjX1Qlvh9/bHRxhPmKeIy5lHfuA=", - "dev": true, - "requires": { - "error-ex": "^1.3.1", - "json-parse-better-errors": "^1.0.1" - } - }, - "path-type": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-3.0.0.tgz", - "integrity": "sha512-T2ZUsdZFHgA3u4e5PfPbjd7HDDpxPnQb5jN0SrDsjNSuVXHJqtwTnWqG0B1jZrgmJ/7lj1EmVIByWt1gxGkWvg==", - "dev": true, - "requires": { - "pify": "^3.0.0" - } - }, - "pify": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-3.0.0.tgz", - "integrity": "sha1-5aSs0sEB/fPZpNB/DbxNtJ3SgXY=", - "dev": true - } + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "read-pkg-up": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", - "integrity": "sha1-nWPBMnbAZZGNV/ACpX9AobZD+wI=", + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true, + "license": "ISC" + }, + "node_modules/shallow-clone": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", + "integrity": "sha512-J1zdXCky5GmNnuauESROVu31MQSnLoYvlyEn6j2Ztk6Q5EHFIhxkMhYcv6vuDzl2XEzoRr856QwzMgWM/TmZgw==", "dev": true, - "requires": { - "find-up": "^1.0.0", - "read-pkg": "^1.0.0" - }, "dependencies": { - "find-up": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", - "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", - "dev": true, - "requires": { - "path-exists": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "path-exists": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", - "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", - "dev": true, - "requires": { - "pinkie-promise": "^2.0.0" - } - }, - "path-type": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", - "integrity": "sha1-WcRPfuSR2nBNpBXaWkBwuk+P5EE=", - "dev": true, - "requires": { - "graceful-fs": "^4.1.2", - "pify": "^2.0.0", - "pinkie-promise": "^2.0.0" - } - }, - "pify": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", - "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", - "dev": true - }, - "read-pkg": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", - "integrity": "sha1-9f+qXs0pyzHAR0vKfXVra7KePyg=", - "dev": true, - "requires": { - "load-json-file": "^1.0.0", - "normalize-package-data": "^2.3.2", - "path-type": "^1.0.0" - } - } + "is-extendable": "^0.1.1", + "kind-of": "^2.0.1", + "lazy-cache": "^0.2.3", + "mixin-object": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "readable-stream": { - "version": "2.3.7", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz", - "integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==", - "requires": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" + "node_modules/shallow-clone/node_modules/kind-of": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", + "integrity": "sha512-0u8i1NZ/mg0b+W3MGGw5I7+6Eib2nx72S/QvXa0hYjEkjTknYmEYQJwGu3mLC0BrhtJjtQafTkyRUQ75Kx0LVg==", + "dev": true, + "dependencies": { + "is-buffer": "^1.0.2" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shallow-clone/node_modules/lazy-cache": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", + "integrity": "sha512-gkX52wvU/R8DVMMt78ATVPFMJqfW8FPz1GZ1sVHBVQHmu/WvhIWE4cE1GBzhJNFicDeYhnwp6Rl35BcAIM3YOQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/shebang-command": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", + "integrity": "sha512-EV3L1+UQWGor21OmnvojK36mhg+TyIKDh3iFBKBohr5xeXIhNBcx8oWdgkTEEQ+BEFFYdLRuqMfd5L84N1V5Vg==", + "dev": true, + "license": "MIT", + "optional": true, "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "shebang-regex": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "readdirp": { - "version": "3.5.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.5.0.tgz", - "integrity": "sha512-cMhu7c/8rdhkHXWsY+osBhfSy0JikwpHK/5+imo+LpeasTF8ouErHrlYkwT0++njiyuDvc7OFY5T3ukvZ8qmFQ==", - "requires": { - "picomatch": "^2.2.1" + "node_modules/shebang-regex": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", + "integrity": "sha512-wpoSFAxys6b2a2wHZ1XpDSgD7N9iVjg29Ph9uV/uaP9Ex/KXlkTZTeddxDPSYQpgvzKLGJke2UU0AzoGCjNIvQ==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "reakit": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/reakit/-/reakit-1.3.8.tgz", - "integrity": "sha512-8SVejx6FUaFi2+Q9eXoDAd4wWi/xAn6v8JgXH8x2xnzye8pb6v5bYvegACVpYVZnrS5w/JUgMTGh1Xy8MkkPww==", - "requires": { - "@popperjs/core": "^2.5.4", - "body-scroll-lock": "^3.1.5", - "reakit-system": "^0.15.1", - "reakit-utils": "^0.15.1", - "reakit-warning": "^0.6.1" - } - }, - "reakit-system": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/reakit-system/-/reakit-system-0.15.1.tgz", - "integrity": "sha512-PkqfAyEohtcEu/gUvKriCv42NywDtUgvocEN3147BI45dOFAB89nrT7wRIbIcKJiUT598F+JlPXAZZVLWhc1Kg==", - "requires": { - "reakit-utils": "^0.15.1" - } - }, - "reakit-utils": { - "version": "0.15.1", - "resolved": "https://registry.npmjs.org/reakit-utils/-/reakit-utils-0.15.1.tgz", - "integrity": "sha512-6cZgKGvOkAMQgkwU9jdYbHfkuIN1Pr+vwcB19plLvcTfVN0Or10JhIuj9X+JaPZyI7ydqTDFaKNdUcDP69o/+Q==" - }, - "reakit-warning": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/reakit-warning/-/reakit-warning-0.6.1.tgz", - "integrity": "sha512-poFUV0EyxB+CcV9uTNBAFmcgsnR2DzAbOTkld4Ul+QOKSeEHZB3b3+MoZQgcYHmbvG19Na1uWaM7ES+/Eyr8tQ==", - "requires": { - "reakit-utils": "^0.15.1" + "node_modules/shell-quote": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.1.tgz", + "integrity": "sha512-6j1W9l1iAs/4xYBI1SYOVZyFcCis9b4KCLQ8fgAGG07QvzaRLVVRQvAy85yNmmZSjYjg4MWh4gNvlPujU/5LpA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "rechoir": { - "version": "0.7.0", - "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.7.0.tgz", - "integrity": "sha512-ADsDEH2bvbjltXEP+hTIAmeFekTFK0V2BTxMkok6qILyAJEXV0AFfoWcAq4yfll5VdIMd/RVXq0lR+wQi5ZU3Q==", + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", "dev": true, - "requires": { - "resolve": "^1.9.0" + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "redent": { + "node_modules/side-channel-list": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-1.0.0.tgz", - "integrity": "sha1-z5Fqsf1fHxbfsggi3W7H9zDCr94=", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", "dev": true, - "optional": true, - "requires": { - "indent-string": "^2.1.0", - "strip-indent": "^1.0.1" + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "redux": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/redux/-/redux-4.1.1.tgz", - "integrity": "sha512-hZQZdDEM25UY2P493kPYuKqviVwZ58lEmGQNeQ+gXa+U0gYPUBf7NKYazbe3m+bs/DzM/ahN12DbF+NG8i0CWw==", - "requires": { - "@babel/runtime": "^7.9.2" + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "redux-multi": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/redux-multi/-/redux-multi-0.1.12.tgz", - "integrity": "sha1-KOH+XklnLLxb2KB/Cyrq8O+DVcI=" - }, - "reflect.ownkeys": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/reflect.ownkeys/-/reflect.ownkeys-0.2.0.tgz", - "integrity": "sha1-dJrO7H8/34tj+SegSAnpDFwLNGA=" + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, - "regenerate-unicode-properties": { - "version": "8.2.0", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-8.2.0.tgz", - "integrity": "sha512-F9DjY1vKLo/tPePDycuH3dn9H1OTPIkVD9Kz4LODu+F2C75mgjAJ7x/gwy6ZcSNRAAkhNlJSOHRe8k3p+K9WhA==", + "node_modules/sinon": { + "version": "16.1.3", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-16.1.3.tgz", + "integrity": "sha512-mjnWWeyxcAf9nC0bXcPmiDut+oE8HYridTNzBbF98AYVLmWwGRp2ISEpyhYflG1ifILT+eNn3BmKUJPxjXUPlA==", "dev": true, - "requires": { - "regenerate": "^1.4.0" + "dependencies": { + "@sinonjs/commons": "^3.0.0", + "@sinonjs/fake-timers": "^10.3.0", + "@sinonjs/samsam": "^8.0.0", + "diff": "^5.1.0", + "nise": "^5.1.4", + "supports-color": "^7.2.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/sinon" } }, - "regenerator-runtime": { - "version": "0.13.7", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.7.tgz", - "integrity": "sha512-a54FxoJDIr27pgf7IgeQGxmqUNYrcV338lf/6gH456HZ/PhX+5BcwHXG9ajESmwe6WRO0tAzRUrRmNONWgkrew==" - }, - "regenerator-transform": { - "version": "0.14.5", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.14.5.tgz", - "integrity": "sha512-eOf6vka5IO151Jfsw2NO9WpGX58W6wWmefK3I1zEGr0lOD0u8rwPaNqQL1aRxUaxLeKO3ArNh3VYg1KbaD+FFw==", + "node_modules/sinon-test": { + "version": "3.1.6", + "resolved": "https://registry.npmjs.org/sinon-test/-/sinon-test-3.1.6.tgz", + "integrity": "sha512-3jBJGf61sS2EN3M+YuIiIbeutKrubP6SFolceTcJrubG+4s+zq3rey/y0huSEwU2ECKOcyCs7EkzANnwqHWPjA==", "dev": true, - "requires": { - "@babel/runtime": "^7.8.4" + "peerDependencies": { + "sinon": ">= 2.x" } }, - "regex-not": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/regex-not/-/regex-not-1.0.2.tgz", - "integrity": "sha512-J6SDjUgDxQj5NusnOtdFxDwN/+HWykR8GELwctJ7mdqhcyy1xEc4SRFHUXvxTp661YaVKAjfRLZ9cCqS6tn32A==", + "node_modules/sinon/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", "dev": true, - "requires": { - "extend-shallow": "^3.0.2", - "safe-regex": "^1.1.0" + "engines": { + "node": ">=8" } }, - "regexp.prototype.flags": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.0.tgz", - "integrity": "sha512-2+Q0C5g951OlYlJz6yu5/M33IcsESLlLfsyIaLJaG4FA2r4yP8MvVMJUUP/fVBkSpbbbZlS5gynbEWLipiiXiQ==", + "node_modules/sinon/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", "dev": true, - "requires": { - "define-properties": "^1.1.3", - "es-abstract": "^1.17.0-next.1" + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "regexpp": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/regexpp/-/regexpp-3.2.0.tgz", - "integrity": "sha512-pq2bWo9mVD43nbts2wGv17XLiNLya+GklZ8kaDLV2Z08gDCsGpnKn9BFMepvWuHCbyVvY7J5o5+BVvoQbmlJLg==", - "dev": true - }, - "regexpu-core": { - "version": "4.7.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-4.7.1.tgz", - "integrity": "sha512-ywH2VUraA44DZQuRKzARmw6S66mr48pQVva4LBeRhcOltJ6hExvWly5ZjFLYo67xbIxb6W1q4bAGtgfEl20zfQ==", + "node_modules/sirv": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", + "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", "dev": true, - "requires": { - "regenerate": "^1.4.0", - "regenerate-unicode-properties": "^8.2.0", - "regjsgen": "^0.5.1", - "regjsparser": "^0.6.4", - "unicode-match-property-ecmascript": "^1.0.4", - "unicode-match-property-value-ecmascript": "^1.2.0" + "dependencies": { + "@polka/url": "^1.0.0-next.24", + "mrmime": "^2.0.0", + "totalist": "^3.0.0" + }, + "engines": { + "node": ">= 10" } }, - "regextras": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/regextras/-/regextras-0.7.1.tgz", - "integrity": "sha512-9YXf6xtW+qzQ+hcMQXx95MOvfqXFgsKDZodX3qZB0x2n5Z94ioetIITsBtvJbiOyxa/6s9AtyweBLCdPmPko/w==", + "node_modules/sisteransi": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", + "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", "dev": true }, - "regjsgen": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.5.2.tgz", - "integrity": "sha512-OFFT3MfrH90xIW8OOSyUrk6QHD5E9JOTeGodiJeBS3J6IwlgzJMNE/1bZklWz5oTg+9dCMyEetclvCVXOPoN3A==", - "dev": true + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "regjsparser": { - "version": "0.6.9", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.6.9.tgz", - "integrity": "sha512-ZqbNRz1SNjLAiYuwY0zoXW8Ne675IX5q+YHioAGbCw4X96Mjl2+dcX9B2ciaeyYjViDAfvIjFpQjJgLttTEERQ==", + "node_modules/slice-ansi": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", + "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", "dev": true, - "requires": { - "jsesc": "~0.5.0" - }, + "license": "MIT", "dependencies": { - "jsesc": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", - "integrity": "sha1-597mbjXW/Bb3EP6R1c9p9w8IkR0=", - "dev": true - } + "ansi-styles": "^4.0.0", + "astral-regex": "^2.0.0", + "is-fullwidth-code-point": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/slice-ansi?sponsor=1" } }, - "remark": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/remark/-/remark-13.0.0.tgz", - "integrity": "sha512-HDz1+IKGtOyWN+QgBiAT0kn+2s6ovOxHyPAFGKVE81VSzJ+mq7RwHFledEvB5F1p4iJvOah/LOKdFuzvRnNLCA==", + "node_modules/slice-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", "dev": true, - "requires": { - "remark-parse": "^9.0.0", - "remark-stringify": "^9.0.0", - "unified": "^9.1.0" - }, + "license": "MIT", "dependencies": { - "is-buffer": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/is-buffer/-/is-buffer-2.0.5.tgz", - "integrity": "sha512-i2R6zNFDwgEHJyQUtJEk0XFi1i0dPFn/oqjK3/vPCcDeJvW5NQ83V8QbicfF1SupOaB0h8ntgBC2YiE7dfyctQ==", - "dev": true - }, - "is-plain-obj": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", - "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", - "dev": true - }, - "remark-parse": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-9.0.0.tgz", - "integrity": "sha512-geKatMwSzEXKHuzBNU1z676sGcDcFoChMK38TgdHJNAYfFtsfHDQG7MoJAjs6sgYMqyLduCYWDIWZIxiPeafEw==", - "dev": true, - "requires": { - "mdast-util-from-markdown": "^0.8.0" - } - }, - "unified": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/unified/-/unified-9.2.2.tgz", - "integrity": "sha512-Sg7j110mtefBD+qunSLO1lqOEKdrwBFBrR6Qd8f4uwkhWNlbkaqwHse6e7QvD3AP/MNoJdEDLaf8OxYyoWgorQ==", - "dev": true, - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-buffer": "^2.0.0", - "is-plain-obj": "^2.0.0", - "trough": "^1.0.0", - "vfile": "^4.0.0" - } - }, - "unist-util-stringify-position": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-2.0.3.tgz", - "integrity": "sha512-3faScn5I+hy9VleOq/qNbAd6pAx7iH5jYBMS9I1HgQVijz/4mv5Bvw5iw1sC/90CODiKo81G/ps8AJrISn687g==", - "dev": true, - "requires": { - "@types/unist": "^2.0.2" - } - }, - "vfile": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-4.2.1.tgz", - "integrity": "sha512-O6AE4OskCG5S1emQ/4gl8zK586RqA3srz3nfK/Viy0UPToBc5Trp9BVFb1u0CjsKrAWwnpr4ifM/KBXPWwJbCA==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "is-buffer": "^2.0.0", - "unist-util-stringify-position": "^2.0.0", - "vfile-message": "^2.0.0" - } - }, - "vfile-message": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-2.0.4.tgz", - "integrity": "sha512-DjssxRGkMvifUOJre00juHoP9DPWuzjxKuMDrhNbk2TdaYYBNMStsNhEOt3idrtI12VQYM/1+iM0KOzXi4pxwQ==", - "dev": true, - "requires": { - "@types/unist": "^2.0.0", - "unist-util-stringify-position": "^2.0.0" - } - } + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "remark-parse": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-5.0.0.tgz", - "integrity": "sha512-b3iXszZLH1TLoyUzrATcTQUZrwNl1rE70rVdSruJFlDaJ9z5aMkhrG43Pp68OgfHndL/ADz6V69Zow8cTQu+JA==", - "dev": true, - "requires": { - "collapse-white-space": "^1.0.2", - "is-alphabetical": "^1.0.0", - "is-decimal": "^1.0.0", - "is-whitespace-character": "^1.0.0", - "is-word-character": "^1.0.0", - "markdown-escapes": "^1.0.0", - "parse-entities": "^1.1.0", - "repeat-string": "^1.5.4", - "state-toggle": "^1.0.0", - "trim": "0.0.1", - "trim-trailing-lines": "^1.0.0", - "unherit": "^1.0.4", - "unist-util-remove-position": "^1.0.0", - "vfile-location": "^2.0.0", - "xtend": "^4.0.1" - } - }, - "remark-stringify": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-9.0.1.tgz", - "integrity": "sha512-mWmNg3ZtESvZS8fv5PTvaPckdL4iNlCHTt8/e/8oN08nArHRHjNZMKzA/YW3+p7/lYqIw4nx1XsjCBo/AxNChg==", + "node_modules/slice-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", "dev": true, - "requires": { - "mdast-util-to-markdown": "^0.6.0" + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" } }, - "rememo": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/rememo/-/rememo-3.0.0.tgz", - "integrity": "sha512-eWtut/7pqMRnSccbexb647iPjN7ir6Tmf4RG92ZVlykFEkHqGYy9tWnpHH3I+FS+WQ6lQ1i1iDgarYzGKgTcRQ==" - }, - "remove-trailing-separator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/remove-trailing-separator/-/remove-trailing-separator-1.1.0.tgz", - "integrity": "sha1-wkvOKig62tW8P1jg1IJJuSN52O8=", - "dev": true - }, - "repeat-element": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/repeat-element/-/repeat-element-1.1.3.tgz", - "integrity": "sha512-ahGq0ZnV5m5XtZLMb+vP76kcAM5nkLqk0lpqAuojSKGgQtn4eRi4ZZGm2olo2zKFH+sMsWaqOCW1dqAnOru72g==", - "dev": true + "node_modules/slice-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" }, - "repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha1-jcrkcOHIirwtYA//Sndihtp15jc=" + "node_modules/slice-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "repeating": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/repeating/-/repeating-2.0.1.tgz", - "integrity": "sha1-UhTFOpJtNVJwdSf7q0FdvAjQbdo=", + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", "dev": true, - "optional": true, - "requires": { - "is-finite": "^1.0.0" + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" } }, - "replace-ext": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/replace-ext/-/replace-ext-1.0.0.tgz", - "integrity": "sha1-3mMSg3P8v3w8z6TeWkgMRaZ5WOs=", - "dev": true + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } }, - "request": { - "version": "2.88.2", - "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", - "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", + "node_modules/snapdragon": { + "version": "0.8.2", + "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", + "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", "dev": true, - "requires": { - "aws-sign2": "~0.7.0", - "aws4": "^1.8.0", - "caseless": "~0.12.0", - "combined-stream": "~1.0.6", - "extend": "~3.0.2", - "forever-agent": "~0.6.1", - "form-data": "~2.3.2", - "har-validator": "~5.1.3", - "http-signature": "~1.2.0", - "is-typedarray": "~1.0.0", - "isstream": "~0.1.2", - "json-stringify-safe": "~5.0.1", - "mime-types": "~2.1.19", - "oauth-sign": "~0.9.0", - "performance-now": "^2.1.0", - "qs": "~6.5.2", - "safe-buffer": "^5.1.2", - "tough-cookie": "~2.5.0", - "tunnel-agent": "^0.6.0", - "uuid": "^3.3.2" - }, "dependencies": { - "qs": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.2.tgz", - "integrity": "sha512-N5ZAX4/LxJmF+7wN74pUD6qAh9/wnvdQcjq9TZjevvXzSUo7bfmw91saqMjzGS2xq91/odN2dW/WOl7qQHNDGA==", - "dev": true - }, - "tough-cookie": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", - "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", - "dev": true, - "requires": { - "psl": "^1.1.28", - "punycode": "^2.1.1" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } + "base": "^0.11.1", + "debug": "^2.2.0", + "define-property": "^0.2.5", + "extend-shallow": "^2.0.1", + "map-cache": "^0.2.2", + "source-map": "^0.5.6", + "source-map-resolve": "^0.5.0", + "use": "^3.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "require-directory": { + "node_modules/snapdragon-node": { "version": "2.1.1", - "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", - "integrity": "sha1-jGStX9MNqxyXbiNE/+f3kqam30I=" - }, - "require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "dev": true - }, - "require-main-filename": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", - "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==" - }, - "requireindex": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/requireindex/-/requireindex-1.2.0.tgz", - "integrity": "sha512-L9jEkOi3ASd9PYit2cwRfyppc9NoABujTP8/5gFcbERmo5jUoAKovIC3fsF17pkTnGsrByysqX+Kxd2OTNI1ww==", - "dev": true + "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", + "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "dev": true, + "dependencies": { + "define-property": "^1.0.0", + "isobject": "^3.0.0", + "snapdragon-util": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } }, - "requires-port": { + "node_modules/snapdragon-node/node_modules/define-property": { "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha1-kl0mAdOaxIXgkc8NpcbmlNw9yv8=" + "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", + "integrity": "sha512-cZTYKFWspt9jZsMscWo8sc/5lbPC9Q0N5nBLgb+Yd915iL3udB1uFgS3B8YCx66UVHq018DAVFoee7x+gxggeA==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-descriptor": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "requizzle": { - "version": "0.2.3", - "resolved": "https://registry.npmjs.org/requizzle/-/requizzle-0.2.3.tgz", - "integrity": "sha512-YanoyJjykPxGHii0fZP0uUPEXpvqfBDxWV7s6GKAiiOsiqhX6vHNyW3Qzdmqp/iq/ExbhaGbVrjB4ruEVSM4GQ==", + "node_modules/snapdragon-node/node_modules/is-descriptor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.3.tgz", + "integrity": "sha512-JCNNGbwWZEVaSPtS45mdtrneRWJFp07LLmykxeFV5F6oBvNF8vHSfJuJgoT472pSfk+Mf8VnlrspaFBHWM8JAw==", "dev": true, - "requires": { - "lodash": "^4.17.14" + "license": "MIT", + "dependencies": { + "is-accessor-descriptor": "^1.0.1", + "is-data-descriptor": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" } }, - "resolve": { - "version": "1.19.0", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.19.0.tgz", - "integrity": "sha512-rArEXAgsBG4UgRGcynxWIWKFvh/XZCcS8UJdHhwy91zwAvCZIbcs+vAbflgBnNjYMs/i/i+/Ux6IZhML1yPvxg==", - "requires": { - "is-core-module": "^2.1.0", - "path-parse": "^1.0.6" + "node_modules/snapdragon-util": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", + "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "dev": true, + "dependencies": { + "kind-of": "^3.2.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "resolve-bin": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/resolve-bin/-/resolve-bin-0.4.1.tgz", - "integrity": "sha512-cPOo/AQjgGONYhFbAcJd1+nuVHKs5NZ8K96Zb6mW+nDl55a7+ya9MWkeYuSMDv/S+YpksZ3EbeAnGWs5x04x8w==", + "node_modules/snapdragon/node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", "dev": true, - "requires": { - "find-parent-dir": "~0.3.0" + "dependencies": { + "ms": "2.0.0" } }, - "resolve-cwd": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", - "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "node_modules/snapdragon/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, - "requires": { - "resolve-from": "^5.0.0" + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/snapdragon/node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "dev": true, + "license": "MIT", "dependencies": { - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - } + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "resolve-dir": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/resolve-dir/-/resolve-dir-0.1.1.tgz", - "integrity": "sha1-shklmlYC+sXFxJatiUpujMQwJh4=", + "node_modules/snapdragon/node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", "dev": true, - "requires": { - "expand-tilde": "^1.2.2", - "global-modules": "^0.2.3" + "license": "MIT" + }, + "node_modules/sockjs": { + "version": "0.3.24", + "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", + "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", + "dev": true, + "dependencies": { + "faye-websocket": "^0.11.3", + "uuid": "^8.3.2", + "websocket-driver": "^0.7.4" } }, - "resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==" + "node_modules/sockjs/node_modules/faye-websocket": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", + "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", + "dev": true, + "dependencies": { + "websocket-driver": ">=0.5.1" + }, + "engines": { + "node": ">=0.8.0" + } }, - "resolve-url": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/resolve-url/-/resolve-url-0.2.1.tgz", - "integrity": "sha1-LGN/53yJOv0qZj/iGqkIAGjiBSo=", - "dev": true + "node_modules/sockjs/node_modules/uuid": { + "version": "8.3.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", + "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", + "dev": true, + "bin": { + "uuid": "dist/bin/uuid" + } }, - "responselike": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-1.0.2.tgz", - "integrity": "sha1-kYcg7ztjHFZCvgaPFa3lpG9Loec=", + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", "dev": true, - "optional": true, - "requires": { - "lowercase-keys": "^1.0.0" + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" } }, - "restore-cursor": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-2.0.0.tgz", - "integrity": "sha1-n37ih/gv0ybU/RYpI9YhKe7g368=", + "node_modules/sort-keys": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", + "integrity": "sha512-vzn8aSqKgytVik0iwdBEi+zevbTYZogewTUM6dtpmGwEcdzbub/TX4bCzRhebDCRC3QzXgJsLRKB2V/Oof7HXg==", "dev": true, - "requires": { - "onetime": "^2.0.0", - "signal-exit": "^3.0.2" - }, + "license": "MIT", + "optional": true, "dependencies": { - "mimic-fn": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-1.2.0.tgz", - "integrity": "sha512-jf84uxzwiuiIVKiOLpfYk7N46TSy8ubTonmneY9vrpHNAnp0QBt2BxWV9dO3/j+BoVAb+a5G6YDPW3M5HOdMWQ==", - "dev": true - }, - "onetime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-2.0.1.tgz", - "integrity": "sha1-BnQoIw/WdEOyeUsiu6UotoZ5YtQ=", - "dev": true, - "requires": { - "mimic-fn": "^1.0.0" - } - } + "is-plain-obj": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "ret": { - "version": "0.1.15", - "resolved": "https://registry.npmjs.org/ret/-/ret-0.1.15.tgz", - "integrity": "sha512-TTlYpa+OL+vMMNG24xSlQGEJ3B/RzEfUlLct7b5G/ytav+wPrplCpVMFuwzXbkecJrb6IYo1iFb0S9v37754mg==", - "dev": true - }, - "retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", - "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=", - "dev": true - }, - "reusify": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", - "integrity": "sha512-U9nH88a3fc/ekCF1l0/UP1IosiuIjyTh7hBvXVMHYgVcfGvt897Xguj2UOLDeI5BG2m7/uwyaLVT6fbtCwTyzw==", - "dev": true - }, - "rfdc": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.3.0.tgz", - "integrity": "sha512-V2hovdzFbOi77/WajaSMXk2OLm+xNIeQdMMuB7icj7bk6zi2F8GGAxigcnDFpJHbNyNcgyJDiP+8nOrY5cZGrA==" - }, - "rgb-regex": { + "node_modules/sort-keys-length": { "version": "1.0.1", - "resolved": "https://registry.npmjs.org/rgb-regex/-/rgb-regex-1.0.1.tgz", - "integrity": "sha1-wODWiC3w4jviVKR16O3UGRX+rrE=", - "dev": true - }, - "rgba-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rgba-regex/-/rgba-regex-1.0.0.tgz", - "integrity": "sha1-QzdOLiyglosO8VI0YLfXMP8i7rM=", - "dev": true - }, - "right-align": { - "version": "0.1.3", - "resolved": "https://registry.npmjs.org/right-align/-/right-align-0.1.3.tgz", - "integrity": "sha1-YTObci/mo1FWiSENJOFMlhSGE+8=", - "requires": { - "align-text": "^0.1.1" + "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", + "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "dev": true, + "optional": true, + "dependencies": { + "sort-keys": "^1.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "rimraf": { - "version": "2.7.1", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.7.1.tgz", - "integrity": "sha512-uWjbaKIK3T1OSVptzX7Nl6PvQ3qAGtKEtVRjRuazjfL3Bx5eI409VZSqgND+4UNnmzLVdPj9FqFJNPqBZFve4w==", + "node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", "dev": true, - "requires": { - "glob": "^7.1.3" + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" } }, - "ripemd160": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", - "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", "dev": true, - "requires": { - "hash-base": "^3.0.0", - "inherits": "^2.0.1" + "engines": { + "node": ">=0.10.0" } }, - "rst-selector-parser": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/rst-selector-parser/-/rst-selector-parser-2.2.3.tgz", - "integrity": "sha1-gbIw6i/MYGbInjRy3nlChdmwPZE=", + "node_modules/source-map-loader": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", + "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", "dev": true, - "requires": { - "lodash.flattendeep": "^4.4.0", - "nearley": "^2.7.10" + "dependencies": { + "iconv-lite": "^0.6.3", + "source-map-js": "^1.0.2" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.72.1" } }, - "rsvp": { - "version": "4.8.5", - "resolved": "https://registry.npmjs.org/rsvp/-/rsvp-4.8.5.tgz", - "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", - "dev": true + "node_modules/source-map-resolve": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", + "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "deprecated": "See https://github.com/lydell/source-map-resolve#deprecated", + "dev": true, + "dependencies": { + "atob": "^2.1.2", + "decode-uri-component": "^0.2.0", + "resolve-url": "^0.2.1", + "source-map-url": "^0.4.0", + "urix": "^0.1.0" + } }, - "rtlcss": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-2.6.2.tgz", - "integrity": "sha512-06LFAr+GAPo+BvaynsXRfoYTJvSaWRyOhURCQ7aeI1MKph9meM222F+Zkt3bDamyHHJuGi3VPtiRkpyswmQbGA==", + "node_modules/source-map-support": { + "version": "0.5.13", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.13.tgz", + "integrity": "sha512-SHSKFHadjVA5oR4PPqhtAVdcBWwRYVd6g6cAXnIbRiIwc2EhPrTuKUBdSLvlEKyIP3GCf89fltvcZiP9MMFA1w==", "dev": true, - "requires": { - "@choojs/findup": "^0.2.1", - "chalk": "^2.4.2", - "mkdirp": "^0.5.1", - "postcss": "^6.0.23", - "strip-json-comments": "^2.0.0" - }, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "postcss": { - "version": "6.0.23", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-6.0.23.tgz", - "integrity": "sha512-soOk1h6J3VMTZtVeVpv15/Hpdl2cBLX3CAw4TAbkpTJiNPk9YP/zWcD1ND+xEtvyuuvKzbxliTOIyvkSeSJ6ag==", - "dev": true, - "requires": { - "chalk": "^2.4.1", - "source-map": "^0.6.1", - "supports-color": "^5.4.0" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - }, - "strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", - "dev": true - } + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "run-async": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/run-async/-/run-async-2.4.1.tgz", - "integrity": "sha512-tvVnVv01b8c1RrA6Ep7JkStj85Guv/YrMcwqYQnwjsAS2cTmmPGBBjAjpCW7RrSodNSoE2/qg9O4bceNvUuDgQ==", - "dev": true + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "run-parallel": { - "version": "1.1.10", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.1.10.tgz", - "integrity": "sha512-zb/1OuZ6flOlH6tQyMPUrE3x3Ulxjlo9WIVXR4yVYi4H9UXQaeIsPbLn2R3O3vQCnDKkAl2qHiuocKKX4Tz/Sw==", + "node_modules/source-map-url": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", + "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", + "deprecated": "See https://github.com/lydell/source-map-url#deprecated", "dev": true }, - "run-queue": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/run-queue/-/run-queue-1.0.3.tgz", - "integrity": "sha1-6Eg5bwV9Ij8kOGkkYY4laUFh7Ec=", + "node_modules/spawnd": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-10.1.4.tgz", + "integrity": "sha512-drqHc0mKJmtMsiGMOCwzlc5eZ0RPtRvT7tQAluW2A0qUc0G7TQ8KLcn3E6K5qzkLkH2UkS3nYQiVGULvvsD9dw==", "dev": true, - "requires": { - "aproba": "^1.1.1" + "dependencies": { + "signal-exit": "^4.1.0", + "tree-kill": "^1.2.2" + }, + "engines": { + "node": ">=16" } }, - "rungen": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/rungen/-/rungen-0.3.2.tgz", - "integrity": "sha1-QAwJ6+kU57F+C27zJjQA/Cq8fLM=" - }, - "rx": { + "node_modules/spawnd/node_modules/signal-exit": { "version": "4.1.0", - "resolved": "https://registry.npmjs.org/rx/-/rx-4.1.0.tgz", - "integrity": "sha1-pfE/957zt0D+MKqAP7CfmIBdR4I=", - "dev": true - }, - "rxjs": { - "version": "5.5.12", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-5.5.12.tgz", - "integrity": "sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw==", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, - "requires": { - "symbol-observable": "1.0.1" + "engines": { + "node": ">=14" }, - "dependencies": { - "symbol-observable": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.0.1.tgz", - "integrity": "sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ=", - "dev": true - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==" + "node_modules/spdx-correct": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", + "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "dev": true, + "dependencies": { + "spdx-expression-parse": "^3.0.0", + "spdx-license-ids": "^3.0.0" + } }, - "safe-json-parse": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/safe-json-parse/-/safe-json-parse-1.0.1.tgz", - "integrity": "sha1-PnZyPjjf3aE8mx0poeB//uSzC1c=", + "node_modules/spdx-exceptions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", + "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", "dev": true }, - "safe-regex": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/safe-regex/-/safe-regex-1.1.0.tgz", - "integrity": "sha1-QKNmnzsHfR6UPURinhV91IAjvy4=", + "node_modules/spdx-expression-parse": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", + "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", "dev": true, - "requires": { - "ret": "~0.1.10" + "dependencies": { + "spdx-exceptions": "^2.1.0", + "spdx-license-ids": "^3.0.0" } }, - "safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + "node_modules/spdx-license-ids": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", + "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", + "dev": true }, - "sane": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/sane/-/sane-4.1.0.tgz", - "integrity": "sha512-hhbzAgTIX8O7SHfp2c8/kREfEn4qO/9q8C9beyY6+tvZ87EpoZ3i1RIEvp27YBswnNbY9mWd6paKVmKbAgLfZA==", - "dev": true, - "requires": { - "@cnakazawa/watch": "^1.0.3", - "anymatch": "^2.0.0", - "capture-exit": "^2.0.0", - "exec-sh": "^0.3.2", - "execa": "^1.0.0", - "fb-watchman": "^2.0.0", - "micromatch": "^3.1.4", - "minimist": "^1.1.1", - "walker": "~1.0.5" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - } - }, - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } + "node_modules/spdy": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", + "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "dev": true, + "dependencies": { + "debug": "^4.1.0", + "handle-thing": "^2.0.0", + "http-deceiver": "^1.2.7", + "select-hose": "^2.0.0", + "spdy-transport": "^3.0.0" + }, + "engines": { + "node": ">=6.0.0" } }, - "sanitize-html": { - "version": "1.27.5", - "resolved": "https://registry.npmjs.org/sanitize-html/-/sanitize-html-1.27.5.tgz", - "integrity": "sha512-M4M5iXDAUEcZKLXkmk90zSYWEtk5NH3JmojQxKxV371fnMh+x9t1rqdmXaGoyEHw3z/X/8vnFhKjGL5xFGOJ3A==", + "node_modules/spdy-transport": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", + "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", "dev": true, - "requires": { - "htmlparser2": "^4.1.0", - "lodash": "^4.17.15", - "parse-srcset": "^1.0.2", - "postcss": "^7.0.27" - }, - "dependencies": { - "dom-serializer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.2.0.tgz", - "integrity": "sha512-n6kZFH/KlCrqs/1GHMOd5i2fd/beQHuehKdWvNNffbGHTr/almdhuVvTVFb3V7fglz+nC50fFusu3lY33h12pA==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "entities": "^2.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", - "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", - "dev": true, - "requires": { - "domelementtype": "^2.1.0" - } - } - } - }, - "domelementtype": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.1.0.tgz", - "integrity": "sha512-LsTgx/L5VpD+Q8lmsXSHW2WpA+eBlZ9HPf3erD1IoPF00/3JKHZ3BknUVA2QGDNu69ZNmyFmCWBSO45XjYKC5w==", - "dev": true - }, - "domhandler": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-3.3.0.tgz", - "integrity": "sha512-J1C5rIANUbuYK+FuFL98650rihynUOEzRLxW+90bKZRWB6A1X1Tf82GxR1qAWLyfNPRvjqfip3Q5tdYlmAa9lA==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1" - } - }, - "domutils": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.4.4.tgz", - "integrity": "sha512-jBC0vOsECI4OMdD0GC9mGn7NXPLb+Qt6KW1YDQzeQYRUFKmNG8lh7mO5HiELfr+lLQE7loDVI4QcAxV80HS+RA==", - "dev": true, - "requires": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0" - }, - "dependencies": { - "domhandler": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.0.0.tgz", - "integrity": "sha512-KPTbnGQ1JeEMQyO1iYXoagsI6so/C96HZiFyByU3T6iAzpXn8EGEvct6unm1ZGoed8ByO2oirxgwxBmqKF9haA==", - "dev": true, - "requires": { - "domelementtype": "^2.1.0" - } - } - } - }, - "htmlparser2": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-4.1.0.tgz", - "integrity": "sha512-4zDq1a1zhE4gQso/c5LP1OtrhYTncXNSpvJYtWJBtXAETPlMfi3IFNjGuQbYLuVY4ZR0QMqRVvo4Pdy9KLyP8Q==", - "dev": true, - "requires": { - "domelementtype": "^2.0.1", - "domhandler": "^3.0.0", - "domutils": "^2.0.0", - "entities": "^2.0.0" - } - } + "dependencies": { + "debug": "^4.1.0", + "detect-node": "^2.0.4", + "hpack.js": "^2.1.6", + "obuf": "^1.1.2", + "readable-stream": "^3.0.6", + "wbuf": "^1.7.3" } }, - "sass": { - "version": "1.34.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.34.1.tgz", - "integrity": "sha512-scLA7EIZM+MmYlej6sdVr0HRbZX5caX5ofDT9asWnUJj21oqgsC+1LuNfm0eg+vM0fCTZHhwImTiCU0sx9h9CQ==", + "node_modules/spdy-transport/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", "dev": true, - "requires": { - "chokidar": ">=3.0.0 <4.0.0" + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" } }, - "sass-loader": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-10.2.0.tgz", - "integrity": "sha512-kUceLzC1gIHz0zNJPpqRsJyisWatGYNFRmv2CKZK2/ngMJgLqxTbXwe/hJ85luyvZkgqU3VlJ33UVF2T/0g6mw==", + "node_modules/speedline-core": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/speedline-core/-/speedline-core-1.4.3.tgz", + "integrity": "sha512-DI7/OuAUD+GMpR6dmu8lliO2Wg5zfeh+/xsdyJZCzd8o5JgFUjCeLsBDuZjIQJdwXS3J0L/uZYrELKYqx+PXog==", "dev": true, - "requires": { - "klona": "^2.0.4", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0", - "semver": "^7.3.2" + "dependencies": { + "@types/node": "*", + "image-ssim": "^0.2.0", + "jpeg-js": "^0.4.1" }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/split-string": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", + "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "dev": true, "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "extend-shallow": "^3.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "sax": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.2.4.tgz", - "integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw==", + "node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", "dev": true }, - "saxes": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/saxes/-/saxes-5.0.1.tgz", - "integrity": "sha512-5LBh1Tls8c9xgGjw3QrMwETmTMVk0oFgvrFSvWx62llR2hcEInrKNZ2GZCCuuy2lvWrdl5jhbpeqc5hRYKFOcw==", + "node_modules/squeak": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz", + "integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=", "dev": true, - "requires": { - "xmlchars": "^2.2.0" + "optional": true, + "dependencies": { + "chalk": "^1.0.0", + "console-stream": "^0.1.1", + "lpad-align": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "scheduler": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.19.1.tgz", - "integrity": "sha512-n/zwRWRYSUj0/3g/otKDRPMh6qv2SYMWNq85IEa8iZyAv8od9zDYpGSnpBEjNgcMNq6Scbu5KfIPxNF72R/2EA==", - "requires": { - "loose-envify": "^1.1.0", - "object-assign": "^4.1.1" + "node_modules/squeak/node_modules/ansi-regex": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", + "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "schema-utils": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-1.0.0.tgz", - "integrity": "sha512-i27Mic4KovM/lnGsy8whRCHhc7VicJajAjTrYg11K9zfZXnYIt4k5F+kZkwjnrhKzLic/HLU4j11mjsz2G/75g==", + "node_modules/squeak/node_modules/ansi-styles": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", + "integrity": "sha512-kmCevFghRiWM7HB5zTPULl4r9bVFSWjz62MhqizDGUrq2NWuNMQyuv4tHHoKJHs69M/MF64lEcHdYIocrdWQYA==", "dev": true, - "requires": { - "ajv": "^6.1.0", - "ajv-errors": "^1.0.0", - "ajv-keywords": "^3.1.0" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "seamless-scroll-polyfill": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/seamless-scroll-polyfill/-/seamless-scroll-polyfill-1.2.3.tgz", - "integrity": "sha512-emnwZtu6NrlBlvT6HrlbAOs024JX4orWew8H5owBOyUJ7eFXn8lGe4bsXTBD6AAWzP/p7LL86AjVIH8Apqec5w==" - }, - "seek-bzip": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz", - "integrity": "sha512-e1QtP3YL5tWww8uKaOCQ18UxIT2laNBXHjV/S2WYCiK4udiv8lkG89KRIoCjUagnAmCBurjF4zEVX2ByBbnCjQ==", + "node_modules/squeak/node_modules/chalk": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", + "integrity": "sha512-U3lRVLMSlsCfjqYPbLyVv11M9CPW4I728d6TCKMAOJueEeB9/8o+eSsMnxPJD+Q+K909sdESg7C+tIkoH6on1A==", "dev": true, + "license": "MIT", "optional": true, - "requires": { - "commander": "^2.8.1" + "dependencies": { + "ansi-styles": "^2.2.1", + "escape-string-regexp": "^1.0.2", + "has-ansi": "^2.0.0", + "strip-ansi": "^3.0.0", + "supports-color": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=" + "node_modules/squeak/node_modules/strip-ansi": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", + "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "ansi-regex": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } }, - "select-hose": { + "node_modules/squeak/node_modules/supports-color": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha1-Yl2GWPhlr0Psliv8N2o3NZpJlMo=", - "dev": true - }, - "selfsigned": { - "version": "1.10.8", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-1.10.8.tgz", - "integrity": "sha512-2P4PtieJeEwVgTU9QEcwIRDQ/mXJLX8/+I3ur+Pg16nS8oNbrGxEso9NyYWy8NAmXiNl4dlAp5MwoNeCWzON4w==", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", + "integrity": "sha512-KKNVtd6pCYgPIKU4cp2733HWYCpplQhddZLBUryaAHou723x+FRzQ5Df824Fj+IyyuiQTRoub4SnIFfIcrp70g==", "dev": true, - "requires": { - "node-forge": "^0.10.0" + "license": "MIT", + "optional": true, + "engines": { + "node": ">=0.8.0" } }, - "semver": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.4.tgz", - "integrity": "sha512-tCfb2WLjqFAtXn4KEdxIhalnRtoKFN7nAwj0B3ZXCbQloV2tq5eDbcTmT68JJD3nRJq24/XgxtQKFIpQdtvmVw==", - "requires": { - "lru-cache": "^6.0.0" + "node_modules/sshpk": { + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", + "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "dev": true, + "dependencies": { + "asn1": "~0.2.3", + "assert-plus": "^1.0.0", + "bcrypt-pbkdf": "^1.0.0", + "dashdash": "^1.12.0", + "ecc-jsbn": "~0.1.1", + "getpass": "^0.1.1", + "jsbn": "~0.1.0", + "safer-buffer": "^2.0.2", + "tweetnacl": "~0.14.0" + }, + "bin": { + "sshpk-conv": "bin/sshpk-conv", + "sshpk-sign": "bin/sshpk-sign", + "sshpk-verify": "bin/sshpk-verify" + }, + "engines": { + "node": ">=0.10.0" } }, - "semver-regex": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/semver-regex/-/semver-regex-2.0.0.tgz", - "integrity": "sha512-mUdIBBvdn0PLOeP3TEkMH7HHeUP3GjsXCwKarjv/kGmUFOYg1VqEemKhoQpWMu6X2I8kHeuVdGibLGkVK+/5Qw==", + "node_modules/stable": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", + "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", + "deprecated": "Modern JS already guarantees Array#sort() is a stable sort, so this library is deprecated. See the compatibility table on MDN: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/sort#browser_compatibility", "dev": true, "optional": true }, - "semver-truncate": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/semver-truncate/-/semver-truncate-1.1.2.tgz", - "integrity": "sha1-V/Qd5pcHpicJp+AQS6IRcQnqR+g=", + "node_modules/stable-hash-x": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/stable-hash-x/-/stable-hash-x-0.2.0.tgz", + "integrity": "sha512-o3yWv49B/o4QZk5ZcsALc6t0+eCelPc44zZsLtCQnZPDwFpDYSWcDnrv2TtMmMbQ7uKo3J0HTURCqckw23czNQ==", "dev": true, - "optional": true, - "requires": { - "semver": "^5.3.0" - }, - "dependencies": { - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true, - "optional": true - } + "license": "MIT", + "engines": { + "node": ">=12.0.0" } }, - "send": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/send/-/send-0.17.1.tgz", - "integrity": "sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==", + "node_modules/stack-utils": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.6.tgz", + "integrity": "sha512-XlkWvfIm6RmsWtNJx+uqtKLS8eqFbxUg0ZzLXqY0caEy9l7hruX8IpiDnjsLavoBgqCCR71TqWO8MaXYheJ3RQ==", "dev": true, - "requires": { - "debug": "2.6.9", - "depd": "~1.1.2", - "destroy": "~1.0.4", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "~1.7.2", - "mime": "1.6.0", - "ms": "2.1.1", - "on-finished": "~2.3.0", - "range-parser": "~1.2.1", - "statuses": "~1.5.0" - }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - }, - "dependencies": { - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } - } - }, - "mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true - }, - "ms": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.1.tgz", - "integrity": "sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==", - "dev": true - } + "escape-string-regexp": "^2.0.0" + }, + "engines": { + "node": ">=10" } }, - "serialize-javascript": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-4.0.0.tgz", - "integrity": "sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==", + "node_modules/stack-utils/node_modules/escape-string-regexp": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", + "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", "dev": true, - "requires": { - "randombytes": "^2.1.0" + "engines": { + "node": ">=8" } }, - "serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha1-03aNabHn2C5c4FD/9bRTvqEqkjk=", + "node_modules/stackframe": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/stackframe/-/stackframe-1.2.1.tgz", + "integrity": "sha512-h88QkzREN/hy8eRdyNhhsO7RSJ5oyTqxxmmn0dzBIMUclZsjpfmrsg81vp8mjjAs2vAZ72nyWxRUwSwmh0e4xg==", + "dev": true + }, + "node_modules/static-extend": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", + "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", "dev": true, - "requires": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha1-i1VoC7S+KDoLW/TqLjhYC+HZMg0=", - "dev": true, - "requires": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - } - }, - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "dev": true - } + "define-property": "^0.2.5", + "object-copy": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "serve-static": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.14.1.tgz", - "integrity": "sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==", + "node_modules/static-extend/node_modules/define-property": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", + "integrity": "sha512-Rr7ADjQZenceVOAKop6ALkkRAmH1A4Gx9hV/7ZujPUN2rkATqFO0JZLZInbAjpZYoJ1gUx8MRMQVkYemcbMSTA==", "dev": true, - "requires": { - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.17.1" + "license": "MIT", + "dependencies": { + "is-descriptor": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "set-blocking": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", - "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" - }, - "set-value": { + "node_modules/statuses": { "version": "2.0.1", - "resolved": "https://registry.npmjs.org/set-value/-/set-value-2.0.1.tgz", - "integrity": "sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", "dev": true, - "requires": { - "extend-shallow": "^2.0.1", - "is-extendable": "^0.1.1", - "is-plain-object": "^2.0.3", - "split-string": "^3.0.1" - }, - "dependencies": { - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - } + "license": "MIT", + "engines": { + "node": ">= 0.8" } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=", - "dev": true - }, - "setprototypeof": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", - "integrity": "sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==" - }, - "sha.js": { - "version": "2.4.11", - "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", - "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "node_modules/stop-iteration-iterator": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", + "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", "dev": true, - "requires": { - "inherits": "^2.0.1", - "safe-buffer": "^5.0.1" + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "internal-slot": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" } }, - "shallow-clone": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-0.1.2.tgz", - "integrity": "sha1-WQnodLp3EG1zrEFM/sH/yofZcGA=", - "dev": true, - "requires": { - "is-extendable": "^0.1.1", - "kind-of": "^2.0.1", - "lazy-cache": "^0.2.3", - "mixin-object": "^2.0.1" - }, - "dependencies": { - "kind-of": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-2.0.1.tgz", - "integrity": "sha1-AY7HpM5+OobLkUG+UZ0kyPqpgbU=", - "dev": true, - "requires": { - "is-buffer": "^1.0.2" - } - }, - "lazy-cache": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/lazy-cache/-/lazy-cache-0.2.7.tgz", - "integrity": "sha1-f+3fLctu23fRHvHRF6tf/fCrG2U=", - "dev": true - } + "node_modules/stream-from-promise": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/stream-from-promise/-/stream-from-promise-1.0.0.tgz", + "integrity": "sha512-j84KLkudt+gr8KJ21RB02btPLx61uGbrLnewsWz6QKmsz8/c4ZFqXw6mJh5+G4oRN7DgDxdbjPxnpySpg1mUig==", + "engines": { + "node": ">=0.10.0" } }, - "shebang-command": { + "node_modules/stream-to-string": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz", - "integrity": "sha1-RKrGW2lbAzmJaMOfNj/uXer98eo=", - "dev": true, - "requires": { - "shebang-regex": "^1.0.0" + "resolved": "https://registry.npmjs.org/stream-to-string/-/stream-to-string-1.2.0.tgz", + "integrity": "sha512-8drZlFIKBHSMdX9GCWv8V9AAWnQcTqw0iAI6/GC7UJ0H0SwKeFKjOoZfGY1tOU00GGU7FYZQoJ/ZCUEoXhD7yQ==", + "dependencies": { + "promise-polyfill": "^1.1.6" } }, - "shebang-regex": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-1.0.0.tgz", - "integrity": "sha1-2kL0l0DAtC2yypcoVxyxkMmO/qM=", - "dev": true - }, - "shelljs": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.3.0.tgz", - "integrity": "sha1-NZbmMHp4FUT1kfN9phg2DzHbV7E=", - "dev": true - }, - "shellwords": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/shellwords/-/shellwords-0.1.1.tgz", - "integrity": "sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==", + "node_modules/streamx": { + "version": "2.23.0", + "resolved": "https://registry.npmjs.org/streamx/-/streamx-2.23.0.tgz", + "integrity": "sha512-kn+e44esVfn2Fa/O0CPFcex27fjIL6MkVae0Mm6q+E6f0hWv578YCERbv+4m02cjxvDsPKLnmxral/rR6lBMAg==", "dev": true, - "optional": true - }, - "showdown": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/showdown/-/showdown-1.9.1.tgz", - "integrity": "sha512-9cGuS382HcvExtf5AHk7Cb4pAeQQ+h0eTr33V1mu+crYWV4KvWAw6el92bDrqGEk5d46Ai/fhbEUwqJ/mTCNEA==", - "requires": { - "yargs": "^14.2" + "license": "MIT", + "dependencies": { + "events-universal": "^1.0.0", + "fast-fifo": "^1.3.2", + "text-decoder": "^1.1.0" } }, - "side-channel": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", - "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", - "requires": { - "call-bind": "^1.0.0", - "get-intrinsic": "^1.0.2", - "object-inspect": "^1.9.0" + "node_modules/strict-uri-encode": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", + "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "dev": true, + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "signal-exit": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.3.tgz", - "integrity": "sha512-VUJ49FC8U1OxwZLxIbTTrDvLnf/6TDgxZcK8wxR8zs13xpx7xbG60ndBlhNrFi2EMuFRoeDoJO7wthSLq42EjA==", - "dev": true + "node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } }, - "simple-html-tokenizer": { - "version": "0.5.11", - "resolved": "https://registry.npmjs.org/simple-html-tokenizer/-/simple-html-tokenizer-0.5.11.tgz", - "integrity": "sha512-C2WEK/Z3HoSFbYq8tI7ni3eOo/NneSPRoPpcM7WdLjFOArFuyXEjAoCdOC3DgMfRyziZQ1hCNR4mrNdWEvD0og==" + "node_modules/string_decoder/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" }, - "simple-swizzle": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", - "integrity": "sha1-pNprY1/8zMoz9w0Xy5JZLeleVXo=", + "node_modules/string-length": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", + "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", "dev": true, - "requires": { - "is-arrayish": "^0.3.1" - }, "dependencies": { - "is-arrayish": { - "version": "0.3.2", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", - "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", - "dev": true - } + "char-regex": "^1.0.2", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" } }, - "sinon": { - "version": "11.1.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-11.1.1.tgz", - "integrity": "sha512-ZSSmlkSyhUWbkF01Z9tEbxZLF/5tRC9eojCdFh33gtQaP7ITQVaMWQHGuFM7Cuf/KEfihuh1tTl3/ABju3AQMg==", + "node_modules/string-length/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^7.1.0", - "@sinonjs/samsam": "^6.0.2", - "diff": "^5.0.0", - "nise": "^5.1.0", - "supports-color": "^7.2.0" - }, "dependencies": { - "@sinonjs/commons": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-1.8.3.tgz", - "integrity": "sha512-xkNcLAn/wZaX14RPlwizcKicDk9G3F8m2nU3L7Ukm5zBgTwiT0wsoFAHx9Jq56fJA1z/7uKGtCRu16sOUCLIHQ==", - "dev": true, - "requires": { - "type-detect": "4.0.8" - } - }, - "@sinonjs/fake-timers": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-7.1.1.tgz", - "integrity": "sha512-am34LJf0N2nON/PT9G7pauA+xjcwX9P6x31m4hBgfUeSXYRZBRv/R6EcdWs8iV4XJjPO++NTsrj7ua/cN2s6ZA==", - "dev": true, - "requires": { - "@sinonjs/commons": "^1.7.0" - } - }, - "diff": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", - "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", - "dev": true - }, - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "sinon-test": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/sinon-test/-/sinon-test-3.1.0.tgz", - "integrity": "sha512-aGQwq6Xl9eJg/8Ugv4Ko4LQWUqjwRYNI8UtxnKa9hmcMEz3HBTR3nnzYrbW4isuRLsJWFuJTUcPGuz7f4XvODg==", + "node_modules/string-template": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", + "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", "dev": true }, - "sirv": { - "version": "1.0.17", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-1.0.17.tgz", - "integrity": "sha512-qx9go5yraB7ekT7bCMqUHJ5jEaOC/GXBxUWv+jeWnb7WzHUFdcQPGWk7YmAwFBaQBrogpuSqd/azbC2lZRqqmw==", + "node_modules/string-width": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", + "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", "dev": true, - "requires": { - "@polka/url": "^1.0.0-next.20", - "mime": "^2.3.1", - "totalist": "^1.0.0" + "license": "MIT", + "dependencies": { + "is-fullwidth-code-point": "^2.0.0", + "strip-ansi": "^4.0.0" + }, + "engines": { + "node": ">=4" } }, - "sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "dev": true - }, - "slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "dev": true + "node_modules/string-width/node_modules/ansi-regex": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.1.tgz", + "integrity": "sha512-+O9Jct8wf++lXxxFc4hc8LsjaSq0HFzzL7cVsw8pRDIPdjKD2mT4ytDZlLuSBZ4cLKZFXIrMGO7DbQCtMJJMKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } }, - "slice-ansi": { + "node_modules/string-width/node_modules/strip-ansi": { "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-4.0.0.tgz", - "integrity": "sha512-qMCMfhY040cVHT43K9BFygqYbUPFZKHOg7K73mtTWJRb8pyP3fzf4Ixd5SzdEJQ6MRUg/WBnOLxghZtKKurENQ==", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", + "integrity": "sha512-4XaJ2zQdCzROZDivEVIDPkcQn8LMFSa8kj8Gxb/Lnwzv9A8VctNZ+lfivC/sV3ivW8ElJTERXZoPBRrZKkNKow==", "dev": true, - "requires": { - "ansi-styles": "^4.0.0", - "astral-regex": "^2.0.0", - "is-fullwidth-code-point": "^3.0.0" - }, + "license": "MIT", "dependencies": { - "ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, - "requires": { - "color-convert": "^2.0.1" - } - }, - "color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, - "requires": { - "color-name": "~1.1.4" - } - }, - "color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - } + "ansi-regex": "^3.0.0" + }, + "engines": { + "node": ">=4" } }, - "snapdragon": { - "version": "0.8.2", - "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", - "integrity": "sha512-FtyOnWN/wCHTVXOMwvSv26d+ko5vWlIDD6zoUJ7LW8vh+ZBC8QdljveRP+crNrtBwioEUWy/4dMtbBjA4ioNlg==", + "node_modules/string.prototype.includes": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", + "integrity": "sha512-o7+c9bW6zpAdJHTtujeePODAhkuicdAryFsfVKwA+wGw89wJ4GTY484WTucM9hLtDEOpOvI+aHnzqnC5lHp4Rg==", "dev": true, - "requires": { - "base": "^0.11.1", - "debug": "^2.2.0", - "define-property": "^0.2.5", - "extend-shallow": "^2.0.1", - "map-cache": "^0.2.2", - "source-map": "^0.5.6", - "source-map-resolve": "^0.5.0", - "use": "^3.1.0" - }, + "license": "MIT", "dependencies": { - "debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, - "requires": { - "ms": "2.0.0" - } - }, - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } - }, - "extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha1-Ua99YUrZqfYQ6huvu5idaxxWiQ8=", - "dev": true, - "requires": { - "is-extendable": "^0.1.0" - } - }, - "ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", - "dev": true - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.3" + }, + "engines": { + "node": ">= 0.4" } }, - "snapdragon-node": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/snapdragon-node/-/snapdragon-node-2.1.1.tgz", - "integrity": "sha512-O27l4xaMYt/RSQ5TR3vpWCAB5Kb/czIcqUFOM/C4fYcLnbZUc1PkjTAMjof2pBWaSTwOUd6qUHcFGVGj7aIwnw==", + "node_modules/string.prototype.matchall": { + "version": "4.0.12", + "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.12.tgz", + "integrity": "sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==", "dev": true, - "requires": { - "define-property": "^1.0.0", - "isobject": "^3.0.0", - "snapdragon-util": "^3.0.1" - }, + "license": "MIT", "dependencies": { - "define-property": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-1.0.0.tgz", - "integrity": "sha1-dp66rz9KY6rTr56NMEybvnm/sOY=", - "dev": true, - "requires": { - "is-descriptor": "^1.0.0" - } - }, - "is-accessor-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-accessor-descriptor/-/is-accessor-descriptor-1.0.0.tgz", - "integrity": "sha512-m5hnHTkcVsPfqx3AKlyttIPb7J+XykHvJP2B9bZDjlhLIoEq4XoK64Vg7boZlVWYK6LUY94dYPEE7Lh0ZkZKcQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-data-descriptor": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-data-descriptor/-/is-data-descriptor-1.0.0.tgz", - "integrity": "sha512-jbRXy1FmtAoCjQkVmIVYwuuqDFUbaOeDjmed1tOGPrsMhtJA4rD9tkgA0F1qJ3gRFRXcHYVkdeaP50Q5rE/jLQ==", - "dev": true, - "requires": { - "kind-of": "^6.0.0" - } - }, - "is-descriptor": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/is-descriptor/-/is-descriptor-1.0.2.tgz", - "integrity": "sha512-2eis5WqQGV7peooDyLmNEPUrps9+SXX5c9pL3xEB+4e9HnGuDa7mB7kHxHw4CbqS9k1T2hOH3miL8n8WtiYVtg==", - "dev": true, - "requires": { - "is-accessor-descriptor": "^1.0.0", - "is-data-descriptor": "^1.0.0", - "kind-of": "^6.0.2" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - } + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.6", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.0.0", + "get-intrinsic": "^1.2.6", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "internal-slot": "^1.1.0", + "regexp.prototype.flags": "^1.5.3", + "set-function-name": "^2.0.2", + "side-channel": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "snapdragon-util": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/snapdragon-util/-/snapdragon-util-3.0.1.tgz", - "integrity": "sha512-mbKkMdQKsjX4BAL4bRYTj21edOf8cN7XHdYUJEe+Zn99hVEYcMvKPct1IqNe7+AZPirn8BCDOQBHQZknqmKlZQ==", + "node_modules/string.prototype.repeat": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/string.prototype.repeat/-/string.prototype.repeat-1.0.0.tgz", + "integrity": "sha512-0u/TldDbKD8bFCQ/4f5+mNRrXwZ8hg2w7ZR8wa16e8z9XpePWl3eGEcUD0OXpEH/VJH/2G3gjUtR3ZOiBe2S/w==", "dev": true, - "requires": { - "kind-of": "^3.2.0" + "license": "MIT", + "dependencies": { + "define-properties": "^1.1.3", + "es-abstract": "^1.17.5" } }, - "socket.io": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-3.1.2.tgz", - "integrity": "sha512-JubKZnTQ4Z8G4IZWtaAZSiRP3I/inpy8c/Bsx2jrwGrTbKeVU5xd6qkKMHpChYeM3dWZSO0QACiGK+obhBNwYw==", - "requires": { - "@types/cookie": "^0.4.0", - "@types/cors": "^2.8.8", - "@types/node": ">=10.0.0", - "accepts": "~1.3.4", - "base64id": "~2.0.0", - "debug": "~4.3.1", - "engine.io": "~4.1.0", - "socket.io-adapter": "~2.1.0", - "socket.io-parser": "~4.0.3" - }, + "node_modules/string.prototype.trim": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.10.tgz", + "integrity": "sha512-Rs66F0P/1kedk5lyYyH9uBzuiI/kNRmwJAR9quK6VOtIpZ2G+hMZd+HQbbv25MgCA6gEffoMZYxlTod4WcdrKA==", + "dev": true, "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-data-property": "^1.1.4", + "define-properties": "^1.2.1", + "es-abstract": "^1.23.5", + "es-object-atoms": "^1.0.0", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "socket.io-adapter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.1.0.tgz", - "integrity": "sha512-+vDov/aTsLjViYTwS9fPy5pEtTkrbEKsw2M+oVSoFGw6OD1IpvlV1VPhUzNbofCQ8oyMbdYJqDtGdmHQK6TdPg==" - }, - "socket.io-parser": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.0.4.tgz", - "integrity": "sha512-t+b0SS+IxG7Rxzda2EVvyBZbvFPBCjJoyHuE0P//7OAsN23GItzDRdWa6ALxZI/8R5ygK7jAR6t028/z+7295g==", - "requires": { - "@types/component-emitter": "^1.2.10", - "component-emitter": "~1.3.0", - "debug": "~4.3.1" - }, + "node_modules/string.prototype.trimend": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.9.tgz", + "integrity": "sha512-G7Ok5C6E/j4SGfyLCloXTrngQIQU3PWtXGst3yM7Bea9FRURf1S42ZHlZZtsNque2FN2PoUhfZXYLNWwEr4dLQ==", + "dev": true, "dependencies": { - "debug": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", - "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", - "requires": { - "ms": "2.1.2" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" - } + "call-bind": "^1.0.8", + "call-bound": "^1.0.2", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "sockjs": { - "version": "0.3.21", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.21.tgz", - "integrity": "sha512-DhbPFGpxjc6Z3I+uX07Id5ZO2XwYsWOrYjaSeieES78cq+JaJvVe5q/m1uvjIQhXinhIeCFRH6JgXe+mvVMyXw==", + "node_modules/string.prototype.trimstart": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.8.tgz", + "integrity": "sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==", "dev": true, - "requires": { - "faye-websocket": "^0.11.3", - "uuid": "^3.4.0", - "websocket-driver": "^0.7.4" - }, "dependencies": { - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } + "call-bind": "^1.0.7", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "sockjs-client": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/sockjs-client/-/sockjs-client-1.5.0.tgz", - "integrity": "sha512-8Dt3BDi4FYNrCFGTL/HtwVzkARrENdwOUf1ZoW/9p3M8lZdFT35jVdrHza+qgxuG9H3/shR4cuX/X9umUrjP8Q==", - "dev": true, - "requires": { - "debug": "^3.2.6", - "eventsource": "^1.0.7", - "faye-websocket": "^0.11.3", - "inherits": "^2.0.4", - "json3": "^3.3.3", - "url-parse": "^1.4.7" + "node_modules/strip-ansi": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-0.1.1.tgz", + "integrity": "sha512-behete+3uqxecWlDAm5lmskaSaISA+ThQ4oNNBDTBJt0x2ppR6IPqfZNuj6BLaLJ/Sji4TPZlcRyOis8wXQTLg==", + "license": "MIT", + "bin": { + "strip-ansi": "cli.js" }, - "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - }, - "faye-websocket": { - "version": "0.11.3", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.3.tgz", - "integrity": "sha512-D2y4bovYpzziGgbHYtGCMjlJM36vAl/y+xUyn1C+FVx8szd1E+86KwVw6XvYSzOP8iMpm1X0I4xJD+QtUb36OA==", - "dev": true, - "requires": { - "websocket-driver": ">=0.5.1" - } - } + "engines": { + "node": ">=0.8.0" } }, - "sort-keys": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sort-keys/-/sort-keys-1.1.2.tgz", - "integrity": "sha1-RBttTTRnmPG05J6JIK37oOVD+a0=", + "node_modules/strip-bom": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-4.0.0.tgz", + "integrity": "sha512-3xurFv5tEgii33Zi8Jtp55wEIILR9eh34FAW00PZf+JnSsTmV/ioewSgQl97JHvgjoRGwPShsWm+IdrxB35d0w==", "dev": true, - "optional": true, - "requires": { - "is-plain-obj": "^1.0.0" + "engines": { + "node": ">=8" } }, - "sort-keys-length": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/sort-keys-length/-/sort-keys-length-1.0.1.tgz", - "integrity": "sha1-nLb09OnkgVWmqgZx7dM2/xR5oYg=", + "node_modules/strip-dirs": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", + "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", "dev": true, "optional": true, - "requires": { - "sort-keys": "^1.0.0" + "dependencies": { + "is-natural-number": "^4.0.1" } }, - "source-list-map": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/source-list-map/-/source-list-map-2.0.1.tgz", - "integrity": "sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==", - "dev": true - }, - "source-map": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", - "integrity": "sha1-igOdLRAh0i0eoUyA2OpGi6LvP8w=" - }, - "source-map-js": { - "version": "0.6.2", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-0.6.2.tgz", - "integrity": "sha512-/3GptzWzu0+0MBQFrDKzw/DvvMTUORvgY6k6jd/VS6iCR4RDTKWH6v6WPwQoUO8667uQEf9Oe38DxAYWY5F/Ug==", - "dev": true - }, - "source-map-loader": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-1.1.3.tgz", - "integrity": "sha512-6YHeF+XzDOrT/ycFJNI53cgEsp/tHTMl37hi7uVyqFAlTXW109JazaQCkbc+jjoL2637qkH1amLi+JzrIpt5lA==", + "node_modules/strip-eof": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", + "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", "dev": true, - "requires": { - "abab": "^2.0.5", - "iconv-lite": "^0.6.2", - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0", - "source-map": "^0.6.1", - "whatwg-mimetype": "^2.3.0" - }, - "dependencies": { - "json5": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.1.3.tgz", - "integrity": "sha512-KXPvOm8K9IJKFM0bmdn8QXh7udDh1g/giieX0NLCaMnb4hEiVFqnop2ImTXCc5e0/oHz3LTqmHGtExn5hfMkOA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.0.0.tgz", - "integrity": "sha512-6D82/xSzO094ajanoOSbe4YvXWMfn2A//8Y1+MUqFAJul5Bs+yn36xbK9OtNDcRVSBJ9jjeoXftM6CfztsjOAA==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.6", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "source-map-resolve": { - "version": "0.5.3", - "resolved": "https://registry.npmjs.org/source-map-resolve/-/source-map-resolve-0.5.3.tgz", - "integrity": "sha512-Htz+RnsXWk5+P2slx5Jh3Q66vhQj1Cllm0zvnaY98+NFx+Dv2CF/f5O/t8x+KaNdrdIAsruNzoh/KpialbqAnw==", + "node_modules/strip-final-newline": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", + "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", "dev": true, - "requires": { - "atob": "^2.1.2", - "decode-uri-component": "^0.2.0", - "resolve-url": "^0.2.1", - "source-map-url": "^0.4.0", - "urix": "^0.1.0" + "engines": { + "node": ">=6" } }, - "source-map-support": { - "version": "0.5.19", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.19.tgz", - "integrity": "sha512-Wonm7zOCIJzBGQdB+thsPar0kYuCIzYvxZwlBa87yi/Mdjv7Tip2cyVbLj5o0cFPN4EVkuTwb3GDDyUx2DGnGw==", + "node_modules/strip-indent": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", + "integrity": "sha512-I5iQq6aFMM62fBEAIB/hXzwJD6EEZ0xEGCX2t7oXqaKPIRgt4WruAQ285BISgdkP+HLGWyeGmNJcpIwFeRYRUA==", "dev": true, - "requires": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - }, + "license": "MIT", + "optional": true, "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "get-stdin": "^4.0.1" + }, + "bin": { + "strip-indent": "cli.js" + }, + "engines": { + "node": ">=0.10.0" } }, - "source-map-url": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/source-map-url/-/source-map-url-0.4.0.tgz", - "integrity": "sha1-PpNdfd1zYxuXZZlW1VEo6HtQhKM=", - "dev": true - }, - "spawnd": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/spawnd/-/spawnd-4.4.0.tgz", - "integrity": "sha512-jLPOfB6QOEgMOQY15Z6+lwZEhH3F5ncXxIaZ7WHPIapwNNLyjrs61okj3VJ3K6tmP5TZ6cO0VAu9rEY4MD4YQg==", - "dev": true, - "requires": { - "exit": "^0.1.2", - "signal-exit": "^3.0.2", - "tree-kill": "^1.2.2", - "wait-port": "^0.2.7" + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "spdx-correct": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.1.1.tgz", - "integrity": "sha512-cOYcUWwhCuHCXi49RhFRCyJEK3iPj1Ziz9DpViV3tbZOwXD49QzIN3MpOLJNxh2qwq2lJJZaKMVw9qNi4jTC0w==", + "node_modules/strip-outer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", + "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", "dev": true, - "requires": { - "spdx-expression-parse": "^3.0.0", - "spdx-license-ids": "^3.0.0" + "dependencies": { + "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "spdx-exceptions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.3.0.tgz", - "integrity": "sha512-/tTrYOC7PPI1nUAgx34hUpqXuyJG+DTHJTnIULG4rDygi4xu/tfgmq1e1cIRwRzwZgo4NLySi+ricLkZkw4i5A==", - "dev": true - }, - "spdx-expression-parse": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", - "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", + "node_modules/strnum": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-1.0.5.tgz", + "integrity": "sha512-J8bbNyKKXl5qYcR36TIO8W3mVGVHrmmxsd5PAItGkmyzwJvybiw2IVq5nqd0i4LSNSkB/sx9VHllbfFdr9k1JA==", "dev": true, - "requires": { - "spdx-exceptions": "^2.1.0", - "spdx-license-ids": "^3.0.0" - } + "optional": true }, - "spdx-license-ids": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.7.tgz", - "integrity": "sha512-U+MTEOO0AiDzxwFvoa4JVnMV6mZlJKk2sBLt90s7G0Gd0Mlknc7kxEn3nuDPNZRta7O2uy8oLcZLVT+4sqNZHQ==", - "dev": true + "node_modules/style-search": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", + "integrity": "sha512-Dj1Okke1C3uKKwQcetra4jSuk0DqbzbYtXipzFlFMZtowbF1x7BKJwB9AayVMyFARvU8EDrZdcax4At/452cAg==", + "dev": true, + "license": "ISC" }, - "spdx-licenses": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/spdx-licenses/-/spdx-licenses-1.0.0.tgz", - "integrity": "sha512-BmeFZRYH9XXf56omx0LuiG+gBXRqwmrKsOtcsGTJh8tw9U0cgRKTrOnyDpP1uvI1AVEkoRKYaAvR902ByotFOw==", - "requires": { - "debug": "4.1.1", - "is2": "2.0.1" + "node_modules/stylehacks": { + "version": "7.0.8", + "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-7.0.8.tgz", + "integrity": "sha512-I3f053GBLIiS5Fg6OMFhq/c+yW+5Hc2+1fgq7gElDMMSqwlRb3tBf2ef6ucLStYRpId4q//bQO1FjcyNyy4yDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "browserslist": "^4.28.1", + "postcss-selector-parser": "^7.1.1" + }, + "engines": { + "node": "^18.12.0 || ^20.9.0 || >=22.0" + }, + "peerDependencies": { + "postcss": "^8.4.32" } }, - "spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", + "node_modules/stylehacks/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "requires": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", + "node_modules/stylelint": { + "version": "16.25.0", + "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-16.25.0.tgz", + "integrity": "sha512-Li0avYWV4nfv1zPbdnxLYBGq4z8DVZxbRgx4Kn6V+Uftz1rMoF1qiEI3oL4kgWqyYgCgs7gT5maHNZ82Gk03vQ==", "dev": true, - "requires": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" } + ], + "license": "MIT", + "dependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4", + "@csstools/media-query-list-parser": "^4.0.3", + "@csstools/selector-specificity": "^5.0.0", + "@dual-bundle/import-meta-resolve": "^4.2.1", + "balanced-match": "^2.0.0", + "colord": "^2.9.3", + "cosmiconfig": "^9.0.0", + "css-functions-list": "^3.2.3", + "css-tree": "^3.1.0", + "debug": "^4.4.3", + "fast-glob": "^3.3.3", + "fastest-levenshtein": "^1.0.16", + "file-entry-cache": "^10.1.4", + "global-modules": "^2.0.0", + "globby": "^11.1.0", + "globjoin": "^0.1.4", + "html-tags": "^3.3.1", + "ignore": "^7.0.5", + "imurmurhash": "^0.1.4", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mathml-tag-names": "^2.1.3", + "meow": "^13.2.0", + "micromatch": "^4.0.8", + "normalize-path": "^3.0.0", + "picocolors": "^1.1.1", + "postcss": "^8.5.6", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-safe-parser": "^7.0.1", + "postcss-selector-parser": "^7.1.0", + "postcss-value-parser": "^4.2.0", + "resolve-from": "^5.0.0", + "string-width": "^4.2.3", + "supports-hyperlinks": "^3.2.0", + "svg-tags": "^1.0.0", + "table": "^6.9.0", + "write-file-atomic": "^5.0.1" + }, + "bin": { + "stylelint": "bin/stylelint.mjs" + }, + "engines": { + "node": ">=18.12.0" } }, - "specificity": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/specificity/-/specificity-0.4.1.tgz", - "integrity": "sha512-1klA3Gi5PD1Wv9Q0wUoOQN1IWAuPu0D1U03ThXTr0cJ20+/iq2tHSDnK7Kk/0LXJ1ztUB2/1Os0wKmfyNgUQfg==", - "dev": true - }, - "split-string": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/split-string/-/split-string-3.1.0.tgz", - "integrity": "sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==", + "node_modules/stylelint-config-recommended": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-14.0.1.tgz", + "integrity": "sha512-bLvc1WOz/14aPImu/cufKAZYfXs/A/owZfSMZ4N+16WGXLoX5lOir53M6odBxvhgmgdxCVnNySJmZKx73T93cg==", "dev": true, - "requires": { - "extend-shallow": "^3.0.0" + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/stylelint" + }, + { + "type": "github", + "url": "https://github.com/sponsors/stylelint" + } + ], + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.1.0" } }, - "sprintf-js": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.2.tgz", - "integrity": "sha512-VE0SOVEHCk7Qc8ulkWw3ntAzXuqf7S2lvwQaDLRnUeIEaKNQJzV6BwmLKhOqT61aGhfUMrXeaBk+oDGCzvhcug==" - }, - "squeak": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/squeak/-/squeak-1.3.0.tgz", - "integrity": "sha1-MwRQN7ZDiLVnZ0uEMiplIQc5FsM=", + "node_modules/stylelint-config-recommended-scss": { + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-14.1.0.tgz", + "integrity": "sha512-bhaMhh1u5dQqSsf6ri2GVWWQW5iUjBYgcHkh7SgDDn92ijoItC/cfO/W+fpXshgTQWhwFkP1rVcewcv4jaftRg==", "dev": true, - "optional": true, - "requires": { - "chalk": "^1.0.0", - "console-stream": "^0.1.1", - "lpad-align": "^1.0.1" - }, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true, - "optional": true - }, - "ansi-styles": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", - "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", - "dev": true, - "optional": true - }, - "chalk": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", - "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", - "dev": true, - "optional": true, - "requires": { - "ansi-styles": "^2.2.1", - "escape-string-regexp": "^1.0.2", - "has-ansi": "^2.0.0", - "strip-ansi": "^3.0.0", - "supports-color": "^2.0.0" - } - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "optional": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", - "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", - "dev": true, + "postcss-scss": "^4.0.9", + "stylelint-config-recommended": "^14.0.1", + "stylelint-scss": "^6.4.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "postcss": "^8.3.3", + "stylelint": "^16.6.1" + }, + "peerDependenciesMeta": { + "postcss": { "optional": true } } }, - "sshpk": { - "version": "1.16.1", - "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.16.1.tgz", - "integrity": "sha512-HXXqVUq7+pcKeLqqZj6mHFUMvXtOJt1uoUx09pFW6011inTMxqI8BA8PM95myrIyyKwdnzjdFjLiE6KBPVtJIg==", + "node_modules/stylelint-scss": { + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-6.14.0.tgz", + "integrity": "sha512-ZKmHMZolxeuYsnB+PCYrTpFce0/QWX9i9gh0hPXzp73WjuIMqUpzdQaBCrKoLWh6XtCFSaNDErkMPqdjy1/8aA==", "dev": true, - "requires": { - "asn1": "~0.2.3", - "assert-plus": "^1.0.0", - "bcrypt-pbkdf": "^1.0.0", - "dashdash": "^1.12.0", - "ecc-jsbn": "~0.1.1", - "getpass": "^0.1.1", - "jsbn": "~0.1.0", - "safer-buffer": "^2.0.2", - "tweetnacl": "~0.14.0" + "license": "MIT", + "dependencies": { + "css-tree": "^3.0.1", + "is-plain-object": "^5.0.0", + "known-css-properties": "^0.37.0", + "mdn-data": "^2.25.0", + "postcss-media-query-parser": "^0.2.3", + "postcss-resolve-nested-selector": "^0.1.6", + "postcss-selector-parser": "^7.1.1", + "postcss-value-parser": "^4.2.0" + }, + "engines": { + "node": ">=18.12.0" + }, + "peerDependencies": { + "stylelint": "^16.8.2" } }, - "ssri": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/ssri/-/ssri-6.0.2.tgz", - "integrity": "sha512-cepbSq/neFK7xB6A50KHN0xHDotYzq58wWCa5LeWqnPrHG8GzfEjO/4O8kpmcGW+oaxkvhEJCWgbgNk4/ZV93Q==", + "node_modules/stylelint-scss/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, - "requires": { - "figgy-pudding": "^3.5.1" + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "stable": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/stable/-/stable-0.1.8.tgz", - "integrity": "sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==", - "dev": true - }, - "stack-trace": { - "version": "0.0.10", - "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", - "integrity": "sha1-VHxws0fo0ytOEI6hoqFZ5f3eGcA=", - "dev": true + "node_modules/stylelint-scss/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" }, - "stack-utils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/stack-utils/-/stack-utils-2.0.3.tgz", - "integrity": "sha512-gL//fkxfWUsIlFL2Tl42Cl6+HFALEaB1FU76I/Fy+oZjRreP7OPMXFlGbxM7NQsI0ZpUfw76sHnv0WNYuTb7Iw==", + "node_modules/stylelint-scss/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "requires": { - "escape-string-regexp": "^2.0.0" - }, + "license": "MIT", "dependencies": { - "escape-string-regexp": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-2.0.0.tgz", - "integrity": "sha512-UpzcLCXolUWcNu5HtVMHYdXJjArjsF9C0aNnquZYY4uW/Vu0miy5YoWvbV345HauVvcAUnpRuhMMcqTcGOY2+w==", - "dev": true - } + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "state-toggle": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/state-toggle/-/state-toggle-1.0.3.tgz", - "integrity": "sha512-d/5Z4/2iiCnHw6Xzghyhb+GcmF89bxwgXG60wjIiZaxnymbyOmI8Hk4VqHXiVVp6u2ysaskFfXg3ekCj4WNftQ==", - "dev": true - }, - "static-extend": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/static-extend/-/static-extend-0.1.2.tgz", - "integrity": "sha1-YICcOcv/VTNyJv1eC1IPNB8ftcY=", + "node_modules/stylelint/node_modules/@csstools/media-query-list-parser": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", + "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", "dev": true, - "requires": { - "define-property": "^0.2.5", - "object-copy": "^0.1.0" - }, - "dependencies": { - "define-property": { - "version": "0.2.5", - "resolved": "https://registry.npmjs.org/define-property/-/define-property-0.2.5.tgz", - "integrity": "sha1-w1se+RjsPJkPmlvFe+BKrOxcgRY=", - "dev": true, - "requires": { - "is-descriptor": "^0.1.0" - } + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/csstools" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/csstools" } + ], + "license": "MIT", + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@csstools/css-parser-algorithms": "^3.0.5", + "@csstools/css-tokenizer": "^3.0.4" } }, - "statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=" - }, - "stream-browserify": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", - "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "node_modules/stylelint/node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", "dev": true, - "requires": { - "inherits": "~2.0.1", - "readable-stream": "^2.0.2" - } + "license": "Python-2.0" }, - "stream-cache": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/stream-cache/-/stream-cache-0.0.2.tgz", - "integrity": "sha1-GsWtaDJCjKVWZ9ve45Xa1ObbEY8=" + "node_modules/stylelint/node_modules/balanced-match": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", + "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", + "dev": true, + "license": "MIT" }, - "stream-each": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/stream-each/-/stream-each-1.2.3.tgz", - "integrity": "sha512-vlMC2f8I2u/bZGqkdfLQW/13Zihpej/7PmSiMQsbYddxuTsJp8vRe2x2FvVExZg7FaOds43ROAuFJwPR4MTZLw==", + "node_modules/stylelint/node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dev": true, - "requires": { - "end-of-stream": "^1.1.0", - "stream-shift": "^1.0.0" + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" } }, - "stream-from-promise": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/stream-from-promise/-/stream-from-promise-1.0.0.tgz", - "integrity": "sha1-djaH9913fkyJT2QIMz/Gs/yKYbs=" + "node_modules/stylelint/node_modules/cosmiconfig": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", + "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "env-paths": "^2.2.1", + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } }, - "stream-http": { - "version": "2.8.3", - "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", - "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "node_modules/stylelint/node_modules/css-tree": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-3.2.1.tgz", + "integrity": "sha512-X7sjQzceUhu1u7Y/ylrRZFU2FS6LRiFVp6rKLPg23y3x3c3DOKAwuXGDp+PAGjh6CSnCjYeAul8pcT8bAl+lSA==", "dev": true, - "requires": { - "builtin-status-codes": "^3.0.0", - "inherits": "^2.0.1", - "readable-stream": "^2.3.6", - "to-arraybuffer": "^1.0.0", - "xtend": "^4.0.0" + "license": "MIT", + "dependencies": { + "mdn-data": "2.27.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" } }, - "stream-shift": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/stream-shift/-/stream-shift-1.0.1.tgz", - "integrity": "sha512-AiisoFqQ0vbGcZgQPY1cdP2I76glaVA/RauYR4G4thNFgkTqr90yXTo4LYX60Jl+sIlPNHHdGSwo01AvbKUSVQ==", - "dev": true - }, - "stream-to-string": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/stream-to-string/-/stream-to-string-1.2.0.tgz", - "integrity": "sha512-8drZlFIKBHSMdX9GCWv8V9AAWnQcTqw0iAI6/GC7UJ0H0SwKeFKjOoZfGY1tOU00GGU7FYZQoJ/ZCUEoXhD7yQ==", - "requires": { - "promise-polyfill": "^1.1.6" - } + "node_modules/stylelint/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "streamroller": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-2.2.4.tgz", - "integrity": "sha512-OG79qm3AujAM9ImoqgWEY1xG4HX+Lw+yY6qZj9R1K2mhF5bEmQ849wvrb+4vt4jLMLzwXttJlQbOdPOQVRv7DQ==", - "requires": { - "date-format": "^2.1.0", - "debug": "^4.1.1", - "fs-extra": "^8.1.0" - }, + "node_modules/stylelint/node_modules/file-entry-cache": { + "version": "10.1.4", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-10.1.4.tgz", + "integrity": "sha512-5XRUFc0WTtUbjfGzEwXc42tiGxQHBmtbUG1h9L2apu4SulCGN3Hqm//9D6FAolf8MYNL7f/YlJl9vy08pj5JuA==", + "dev": true, + "license": "MIT", "dependencies": { - "date-format": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/date-format/-/date-format-2.1.0.tgz", - "integrity": "sha512-bYQuGLeFxhkxNOF3rcMtiZxvCBAquGzZm6oWA1oZ0g2THUzivaRhv8uOhdr19LmoobSOLoIAxeUK2RdbM8IFTA==" - } + "flat-cache": "^6.1.13" } }, - "strict-uri-encode": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz", - "integrity": "sha1-J5siXfHVgrH1TmWt3UNS4Y+qBxM=", + "node_modules/stylelint/node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dev": true, - "optional": true + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } }, - "string-length": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/string-length/-/string-length-4.0.2.tgz", - "integrity": "sha512-+l6rNN5fYHNhZZy41RXsYptCjA2Igmq4EG7kZAYFQI1E1VTXarr6ZPXBg6eq7Y6eK4FEhY6AJlyuFIb/v/S0VQ==", + "node_modules/stylelint/node_modules/flat-cache": { + "version": "6.1.21", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.21.tgz", + "integrity": "sha512-2u7cJfSf7Th7NxEk/VzQjnPoglok2YCsevS7TSbJjcDQWJPbqUUnSYtriHSvtnq+fRZHy1s0ugk4ApnQyhPGoQ==", "dev": true, - "requires": { - "char-regex": "^1.0.2", - "strip-ansi": "^6.0.0" - }, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "cacheable": "^2.3.3", + "flatted": "^3.4.1", + "hookified": "^1.15.0" } }, - "string-template": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/string-template/-/string-template-0.2.1.tgz", - "integrity": "sha1-QpMuWYo1LQH8IuwzZ9nYTuxsmt0=", - "dev": true - }, - "string-width": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-3.1.0.tgz", - "integrity": "sha512-vafcv6KjVZKSgz06oM/H6GDBrAtz8vdhQakGjFIvNrHA6y3HCF1CInLy+QLq8dTJPQ1b+KDUqDFctkdRW44e1w==", - "requires": { - "emoji-regex": "^7.0.1", - "is-fullwidth-code-point": "^2.0.0", - "strip-ansi": "^5.1.0" + "node_modules/stylelint/node_modules/global-modules": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", + "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "global-prefix": "^3.0.0" + }, + "engines": { + "node": ">=6" } }, - "string.prototype.matchall": { - "version": "4.0.5", - "resolved": "https://registry.npmjs.org/string.prototype.matchall/-/string.prototype.matchall-4.0.5.tgz", - "integrity": "sha512-Z5ZaXO0svs0M2xd/6By3qpeKpLKd9mO4v4q3oMEQrk8Ck4xOD5d5XeBOOjGrmVZZ/AHB1S0CgG4N5r1G9N3E2Q==", + "node_modules/stylelint/node_modules/global-prefix": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", + "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.2", - "get-intrinsic": "^1.1.1", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "regexp.prototype.flags": "^1.3.1", - "side-channel": "^1.0.4" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - } - }, - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "regexp.prototype.flags": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.3.1.tgz", - "integrity": "sha512-JiBdRBq91WlY7uRJ0ds7R+dU02i6LKi8r3BuQhNXn+kmeLN+EfHhfjqMRis1zJxnlu88hq/4dx0P2OP3APRTOA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "license": "MIT", + "dependencies": { + "ini": "^1.3.5", + "kind-of": "^6.0.2", + "which": "^1.3.1" + }, + "engines": { + "node": ">=6" } }, - "string.prototype.trim": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/string.prototype.trim/-/string.prototype.trim-1.2.4.tgz", - "integrity": "sha512-hWCk/iqf7lp0/AgTF7/ddO1IWtSNPASjlzCicV5irAVdE1grjsneK26YG6xACMBEdCvO8fUST0UzDMh/2Qy+9Q==", + "node_modules/stylelint/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3", - "es-abstract": "^1.18.0-next.2" - }, - "dependencies": { - "call-bind": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", - "integrity": "sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "get-intrinsic": "^1.0.2" - } - }, - "es-abstract": { - "version": "1.18.5", - "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.18.5.tgz", - "integrity": "sha512-DDggyJLoS91CkJjgauM5c0yZMjiD1uK3KcaCeAmffGwZ+ODWzOkPN4QwRbsK5DOFf06fywmyLci3ZD8jLGhVYA==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "es-to-primitive": "^1.2.1", - "function-bind": "^1.1.1", - "get-intrinsic": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.2", - "internal-slot": "^1.0.3", - "is-callable": "^1.2.3", - "is-negative-zero": "^2.0.1", - "is-regex": "^1.1.3", - "is-string": "^1.0.6", - "object-inspect": "^1.11.0", - "object-keys": "^1.1.1", - "object.assign": "^4.1.2", - "string.prototype.trimend": "^1.0.4", - "string.prototype.trimstart": "^1.0.4", - "unbox-primitive": "^1.0.1" - }, - "dependencies": { - "get-intrinsic": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.1.1.tgz", - "integrity": "sha512-kWZrnVM42QCiEA2Ig1bG8zjoIMOgxWwYCEeNdwY6Tv/cOSeGpcoX4pXHfKUxNKVoArnrEr2e9srnAxxGIraS9Q==", - "dev": true, - "requires": { - "function-bind": "^1.1.1", - "has": "^1.0.3", - "has-symbols": "^1.0.1" - } - } - } - }, - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==", - "dev": true - }, - "is-callable": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.2.4.tgz", - "integrity": "sha512-nsuwtxZfMX67Oryl9LCQ+upnC0Z0BgpwntpS89m1H/TLF0zNfzfLMV/9Wa/6MZsj0acpEjAO0KF1xT6ZdLl95w==", - "dev": true - }, - "is-regex": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", - "integrity": "sha512-kvRdxDsxZjhzUX07ZnLydzS1TU/TJlTUHHY4YLL87e37oUA49DfkLqgy+VjFocowy29cKvcSiu+kIv728jTTVg==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "has-tostringtag": "^1.0.0" - } - }, - "object-inspect": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.11.0.tgz", - "integrity": "sha512-jp7ikS6Sd3GxQfZJPyH3cjcbJF6GZPClgdV+EFygjFLQ5FmW/dRUnTd9PQ9k0JhoNDabWFbpF1yCdSWCC6gexg==", - "dev": true - }, - "string.prototype.trimend": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.4.tgz", - "integrity": "sha512-y9xCjw1P23Awk8EvTpcyL2NIr1j7wJ39f+k6lvRnSMz+mz9CGz9NYPelDk42kOz6+ql8xjfK8oYzy3jAP5QU5A==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - }, - "string.prototype.trimstart": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.4.tgz", - "integrity": "sha512-jh6e984OBfvxS50tdY2nRZnoC5/mLFKOREQfw8t5yytkoUsJRNxvI/E39qu1sD0OtWI3OC0XgKSmcWwziwYuZw==", - "dev": true, - "requires": { - "call-bind": "^1.0.2", - "define-properties": "^1.1.3" - } - } + "license": "MIT", + "engines": { + "node": ">= 4" } }, - "string.prototype.trimend": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimend/-/string.prototype.trimend-1.0.3.tgz", - "integrity": "sha512-ayH0pB+uf0U28CtjlLvL7NaohvR1amUvVZk+y3DYb0Ey2PUV5zPkkKy9+U1ndVEIXO8hNg18eIv9Jntbii+dKw==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "node_modules/stylelint/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "string.prototype.trimstart": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/string.prototype.trimstart/-/string.prototype.trimstart-1.0.3.tgz", - "integrity": "sha512-oBIBUy5lea5tt0ovtOFiEQaBkoBBkyJhZXzJYrSmDo5IUUqbOPvVezuRs/agBIdZ2p2Eo1FD6bD9USyBLfl3xg==", - "requires": { - "call-bind": "^1.0.0", - "define-properties": "^1.1.3" + "node_modules/stylelint/node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" } }, - "string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "requires": { - "safe-buffer": "~5.1.0" - }, + "node_modules/stylelint/node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "dev": true, + "license": "MIT", "dependencies": { - "safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" - } + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" } }, - "strip-ansi": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-5.2.0.tgz", - "integrity": "sha512-DuRs1gKbBqsMKIZlrffwlug8MHkcnpjs5VPmL1PAh+mA30U0DTotfDZ0d2UUsXpPmPmMMJ6W773MaA3J+lbiWA==", - "requires": { - "ansi-regex": "^4.1.0" + "node_modules/stylelint/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" } }, - "strip-bom": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", - "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", - "dev": true + "node_modules/stylelint/node_modules/mdn-data": { + "version": "2.27.1", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.27.1.tgz", + "integrity": "sha512-9Yubnt3e8A0OKwxYSXyhLymGW4sCufcLG6VdiDdUGVkPhpqLxlvP5vl1983gQjJl3tqbrM731mjaZaP68AgosQ==", + "dev": true, + "license": "CC0-1.0" }, - "strip-dirs": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/strip-dirs/-/strip-dirs-2.1.0.tgz", - "integrity": "sha512-JOCxOeKLm2CAS73y/U4ZeZPTkE+gNVCzKt7Eox84Iej1LT/2pTWYpZKJuxwQpvX1LiZb1xokNR7RLfuBAa7T3g==", + "node_modules/stylelint/node_modules/meow": { + "version": "13.2.0", + "resolved": "https://registry.npmjs.org/meow/-/meow-13.2.0.tgz", + "integrity": "sha512-pxQJQzB6djGPXh08dacEloMFopsOqGVRKFPYvPOt9XDZ1HasbgDZA74CJGreSU4G3Ak7EFJGoiH2auq+yXISgA==", "dev": true, - "optional": true, - "requires": { - "is-natural-number": "^4.0.1" + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "strip-eof": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-eof/-/strip-eof-1.0.0.tgz", - "integrity": "sha1-u0P/VZim6wXYm1n80SnJgzE2Br8=", - "dev": true - }, - "strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "dev": true - }, - "strip-indent": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-1.0.1.tgz", - "integrity": "sha1-DHlipq3vp7vUrDZkYKY4VSrhoKI=", + "node_modules/stylelint/node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", "dev": true, - "optional": true, - "requires": { - "get-stdin": "^4.0.1" - }, + "license": "MIT", "dependencies": { - "get-stdin": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-4.0.1.tgz", - "integrity": "sha1-uWjGsKBDhDJJAui/Gl3zJXmkUP4=", - "dev": true, - "optional": true - } + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" } }, - "strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "dev": true - }, - "strip-outer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/strip-outer/-/strip-outer-1.0.1.tgz", - "integrity": "sha512-k55yxKHwaXnpYGsOzg4Vl8+tDrWylxDEpknGjhTiZB8dFRU5rTo9CAzeycivxV3s+zlTKwrs6WxMxR95n26kwg==", + "node_modules/stylelint/node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", "dev": true, - "requires": { - "escape-string-regexp": "^1.0.2" + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" } }, - "style-search": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/style-search/-/style-search-0.1.0.tgz", - "integrity": "sha1-eVjHk+R+MuB9K1yv5cC/jhLneQI=", - "dev": true + "node_modules/stylelint/node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } }, - "style-value-types": { - "version": "4.1.4", - "resolved": "https://registry.npmjs.org/style-value-types/-/style-value-types-4.1.4.tgz", - "integrity": "sha512-LCJL6tB+vPSUoxgUBt9juXIlNJHtBMy8jkXzUJSBzeHWdBu6lhzHqCvLVkXFGsFIlNa2ln1sQHya/gzaFmB2Lg==", - "requires": { - "hey-listen": "^1.0.8", - "tslib": "^2.1.0" + "node_modules/stylelint/node_modules/signal-exit": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", + "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" }, - "dependencies": { - "tslib": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.1.tgz", - "integrity": "sha512-77EbyPPpMz+FRFRuAFlWMtmgUWGe9UOG2Z25NqCwiIjRhOf5iKGuzSe5P2w1laq+FkRy4p+PCuVkJSGkzTEKVw==" - } + "funding": { + "url": "https://github.com/sponsors/isaacs" } }, - "stylehacks": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-4.0.3.tgz", - "integrity": "sha512-7GlLk9JwlElY4Y6a/rmbH2MhVlTyVmiJd1PfTCqFaIBEGMYNsrO/v3SeGTdhBThLg4Z+NbOk/qFMwCa+J+3p/g==", + "node_modules/stylelint/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", "dev": true, - "requires": { - "browserslist": "^4.0.0", - "postcss": "^7.0.0", - "postcss-selector-parser": "^3.0.0" - }, - "dependencies": { - "postcss-selector-parser": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-3.1.2.tgz", - "integrity": "sha512-h7fJ/5uWuRVyOtkO45pnt1Ih40CEleeyCHzipqAZO2e5H20g25Y48uYnFUiShvY4rZWNJ/Bib/KVPmanaCtOhA==", - "dev": true, - "requires": { - "dot-prop": "^5.2.0", - "indexes-of": "^1.0.1", - "uniq": "^1.0.1" - } - } + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "stylelint": { - "version": "13.13.1", - "resolved": "https://registry.npmjs.org/stylelint/-/stylelint-13.13.1.tgz", - "integrity": "sha512-Mv+BQr5XTUrKqAXmpqm6Ddli6Ief+AiPZkRsIrAoUKFuq/ElkUh9ZMYxXD0iQNZ5ADghZKLOWz1h7hTClB7zgQ==", + "node_modules/stylelint/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", "dev": true, - "requires": { - "@stylelint/postcss-css-in-js": "^0.37.2", - "@stylelint/postcss-markdown": "^0.36.2", - "autoprefixer": "^9.8.6", - "balanced-match": "^2.0.0", - "chalk": "^4.1.1", - "cosmiconfig": "^7.0.0", - "debug": "^4.3.1", - "execall": "^2.0.0", - "fast-glob": "^3.2.5", - "fastest-levenshtein": "^1.0.12", - "file-entry-cache": "^6.0.1", - "get-stdin": "^8.0.0", - "global-modules": "^2.0.0", - "globby": "^11.0.3", - "globjoin": "^0.1.4", - "html-tags": "^3.1.0", - "ignore": "^5.1.8", - "import-lazy": "^4.0.0", - "imurmurhash": "^0.1.4", - "known-css-properties": "^0.21.0", - "lodash": "^4.17.21", - "log-symbols": "^4.1.0", - "mathml-tag-names": "^2.1.3", - "meow": "^9.0.0", - "micromatch": "^4.0.4", - "normalize-selector": "^0.2.0", - "postcss": "^7.0.35", - "postcss-html": "^0.36.0", - "postcss-less": "^3.1.4", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-safe-parser": "^4.0.2", - "postcss-sass": "^0.4.4", - "postcss-scss": "^2.1.1", - "postcss-selector-parser": "^6.0.5", - "postcss-syntax": "^0.36.2", - "postcss-value-parser": "^4.1.0", - "resolve-from": "^5.0.0", - "slash": "^3.0.0", - "specificity": "^0.4.1", - "string-width": "^4.2.2", - "strip-ansi": "^6.0.0", - "style-search": "^0.1.0", - "sugarss": "^2.0.0", - "svg-tags": "^1.0.0", - "table": "^6.6.0", - "v8-compile-cache": "^2.3.0", - "write-file-atomic": "^3.0.3" - }, + "license": "MIT", "dependencies": { - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "dev": true - }, - "balanced-match": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-2.0.0.tgz", - "integrity": "sha512-1ugUSr8BHXRnK23KfuYS+gVMC3LB8QGH9W1iGtDPsNWoQbgtXSExkBu2aDR4epiGWZOjZsj6lDl/N/AqqTC3UA==", - "dev": true - }, - "braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", - "dev": true, - "requires": { - "fill-range": "^7.0.1" - } - }, - "camelcase-keys": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", - "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, - "requires": { - "camelcase": "^5.3.1", - "map-obj": "^4.0.0", - "quick-lru": "^4.0.1" - } - }, - "cosmiconfig": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-7.0.1.tgz", - "integrity": "sha512-a1YWNUV2HwGimB7dU2s1wUMurNKjpx60HxBB6xUM8Re+2s1g1IIfJvFR0/iCF+XHdE0GMTKTuLR32UQff4TEyQ==", - "dev": true, - "requires": { - "@types/parse-json": "^4.0.0", - "import-fresh": "^3.2.1", - "parse-json": "^5.0.0", - "path-type": "^4.0.0", - "yaml": "^1.10.0" - } - }, - "debug": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.2.tgz", - "integrity": "sha512-mOp8wKcvj7XxC78zLgw/ZA+6TSgkoE2C/ienthhRD298T7UNwAg9diBpLRxC0mOezLl4B0xV7M0cCO6P/O0Xhw==", - "dev": true, - "requires": { - "ms": "2.1.2" - } - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", - "dev": true, - "requires": { - "to-regex-range": "^5.0.1" - } - }, - "find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "requires": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - } - }, - "get-stdin": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/get-stdin/-/get-stdin-8.0.0.tgz", - "integrity": "sha512-sY22aA6xchAzprjyqmSEQv4UbAAzRN0L2dQB0NlN5acTTK9Don6nhoc3eAbUnpZiCANAMfd/+40kVdKfFygohg==", - "dev": true - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "globby": { - "version": "11.0.4", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.0.4.tgz", - "integrity": "sha512-9O4MVG9ioZJ08ffbcyVYyLOJLk5JQ688pJ4eMGLpdWLHq/Wr1D9BlriLQyL0E+jbkuePVZXYFj47QM/v093wHg==", - "dev": true, - "requires": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.1.1", - "ignore": "^5.1.4", - "merge2": "^1.3.0", - "slash": "^3.0.0" - } - }, - "hosted-git-info": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-4.0.2.tgz", - "integrity": "sha512-c9OGXbZ3guC/xOlCg1Ci/VgWlwsqDv1yMQL1CWqXDL0hDjXuNcq0zuR4xqPSuasI3kqFDhqSyTjREz5gzq0fXg==", - "dev": true, - "requires": { - "lru-cache": "^6.0.0" - } - }, - "indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "dev": true - }, - "is-core-module": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.6.0.tgz", - "integrity": "sha512-wShG8vs60jKfPWpF2KZRaAtvt3a20OAn7+IJ6hLPECpSABLcKtFKTTI4ZtH5QcBruBHlq+WsdHWyz0BCZW7svQ==", - "dev": true, - "requires": { - "has": "^1.0.3" - } - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "dev": true - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "requires": { - "p-locate": "^4.1.0" - } - }, - "map-obj": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.2.1.tgz", - "integrity": "sha512-+WA2/1sPmDj1dlvvJmB5G6JKfY9dpn7EVBUL06+y6PoljPkh+6V1QihwxNkbcGxCRjt2b0F9K0taiCuo7MbdFQ==", - "dev": true - }, - "meow": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/meow/-/meow-9.0.0.tgz", - "integrity": "sha512-+obSblOQmRhcyBt62furQqRAQpNyWXo8BuQ5bN7dG8wmwQ+vwHKp/rCFD4CrTP8CsDQD1sjoZ94K417XEUk8IQ==", - "dev": true, - "requires": { - "@types/minimist": "^1.2.0", - "camelcase-keys": "^6.2.2", - "decamelize": "^1.2.0", - "decamelize-keys": "^1.1.0", - "hard-rejection": "^2.1.0", - "minimist-options": "4.1.0", - "normalize-package-data": "^3.0.0", - "read-pkg-up": "^7.0.1", - "redent": "^3.0.0", - "trim-newlines": "^3.0.0", - "type-fest": "^0.18.0", - "yargs-parser": "^20.2.3" - } - }, - "micromatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.4.tgz", - "integrity": "sha512-pRmzw/XUcwXGpD9aI9q/0XOwLNygjETJ8y0ao0wdqprrzDa4YnxLcz7fQRZr8voh8V10kGhABbNcHVk5wHgWwg==", - "dev": true, - "requires": { - "braces": "^3.0.1", - "picomatch": "^2.2.3" - } - }, - "ms": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true - }, - "normalize-package-data": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", - "integrity": "sha512-p2W1sgqij3zMMyRC067Dg16bfzVH+w7hyegmpIvZ4JNjqtGOVAIvLmjBx3yP7YTe9vKJgkoNOPjwQGogDoMXFA==", - "dev": true, - "requires": { - "hosted-git-info": "^4.0.1", - "is-core-module": "^2.5.0", - "semver": "^7.3.4", - "validate-npm-package-license": "^3.0.1" - } - }, - "p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "requires": { - "p-limit": "^2.2.0" - } - }, - "path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true - }, - "picomatch": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.0.tgz", - "integrity": "sha512-lY1Q/PiJGC2zOv/z391WOTD+Z02bCgsFfvxoXXf6h7kv9o+WmsmzYqrAwY63sNgOxE4xEdq0WyUnXfKeBrSvYw==", - "dev": true - }, - "postcss-selector-parser": { - "version": "6.0.6", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.6.tgz", - "integrity": "sha512-9LXrvaaX3+mcv5xkg5kFwqSzSH1JIObIx51PrndZwlmznwXRfxMddDvo9gve3gVR8ZTKgoFDdWkbRFmEhT4PMg==", - "dev": true, - "requires": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - } - }, - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - }, - "read-pkg": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz", - "integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==", - "dev": true, - "requires": { - "@types/normalize-package-data": "^2.4.0", - "normalize-package-data": "^2.5.0", - "parse-json": "^5.0.0", - "type-fest": "^0.6.0" - }, - "dependencies": { - "hosted-git-info": { - "version": "2.8.9", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", - "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", - "dev": true - }, - "normalize-package-data": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", - "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", - "dev": true, - "requires": { - "hosted-git-info": "^2.1.4", - "resolve": "^1.10.0", - "semver": "2 || 3 || 4 || 5", - "validate-npm-package-license": "^3.0.1" - } - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "type-fest": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz", - "integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==", - "dev": true - } - } - }, - "read-pkg-up": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz", - "integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==", - "dev": true, - "requires": { - "find-up": "^4.1.0", - "read-pkg": "^5.2.0", - "type-fest": "^0.8.1" - }, - "dependencies": { - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true - } - } - }, - "redent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz", - "integrity": "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg==", - "dev": true, - "requires": { - "indent-string": "^4.0.0", - "strip-indent": "^3.0.0" - } - }, - "resolve-from": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", - "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - }, - "strip-indent": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz", - "integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==", - "dev": true, - "requires": { - "min-indent": "^1.0.0" - } - }, - "to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "dev": true, - "requires": { - "is-number": "^7.0.0" - } - }, - "trim-newlines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-3.0.1.tgz", - "integrity": "sha512-c1PTsA3tYrIsLGkJkzHF+w9F2EyxfXGo4UyJc4pFL++FMjnq0HJS69T3M7d//gKrFKwy429bouPescbjecU+Zw==", - "dev": true - }, - "type-fest": { - "version": "0.18.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.18.1.tgz", - "integrity": "sha512-OIAYXk8+ISY+qTOwkHtKqzAuxchoMiD9Udx+FSGQDuiRR+PJKJHc2NJAXlbhkGwTt/4/nKZxELY1w3ReWOL8mw==", - "dev": true - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yargs-parser": { - "version": "20.2.9", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", - "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", - "dev": true - } + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "stylelint-config-recommended": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-3.0.0.tgz", - "integrity": "sha512-F6yTRuc06xr1h5Qw/ykb2LuFynJ2IxkKfCMf+1xqPffkxh0S09Zc902XCffcsw/XMFq/OzQ1w54fLIDtmRNHnQ==", - "dev": true - }, - "stylelint-config-recommended-scss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended-scss/-/stylelint-config-recommended-scss-4.3.0.tgz", - "integrity": "sha512-/noGjXlO8pJTr/Z3qGMoaRFK8n1BFfOqmAbX1RjTIcl4Yalr+LUb1zb9iQ7pRx1GsEBXOAm4g2z5/jou/pfMPg==", + "node_modules/stylelint/node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", "dev": true, - "requires": { - "stylelint-config-recommended": "^5.0.0" - }, + "license": "MIT", "dependencies": { - "stylelint-config-recommended": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/stylelint-config-recommended/-/stylelint-config-recommended-5.0.0.tgz", - "integrity": "sha512-c8aubuARSu5A3vEHLBeOSJt1udOdS+1iue7BmJDTSXoCBmfEQmmWX+59vYIj3NQdJBY6a/QRv1ozVFpaB9jaqA==", - "dev": true - } + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" } }, - "stylelint-scss": { - "version": "3.20.1", - "resolved": "https://registry.npmjs.org/stylelint-scss/-/stylelint-scss-3.20.1.tgz", - "integrity": "sha512-OTd55O1TTAC5nGKkVmUDLpz53LlK39R3MImv1CfuvsK7/qugktqiZAeQLuuC4UBhzxCnsc7fp9u/gfRZwFAIkA==", + "node_modules/stylelint/node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", "dev": true, - "requires": { - "lodash": "^4.17.15", - "postcss-media-query-parser": "^0.2.3", - "postcss-resolve-nested-selector": "^0.1.1", - "postcss-selector-parser": "^6.0.2", - "postcss-value-parser": "^4.1.0" - }, + "license": "ISC", "dependencies": { - "postcss-value-parser": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz", - "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==", - "dev": true - } + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" } }, - "sugarss": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/sugarss/-/sugarss-2.0.0.tgz", - "integrity": "sha512-WfxjozUk0UVA4jm+U1d736AUpzSrNsQcIbyOkoE364GrtWmIrFdk5lksEupgWMD4VaT/0kVx1dobpiDumSgmJQ==", + "node_modules/stylelint/node_modules/write-file-atomic": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-5.0.1.tgz", + "integrity": "sha512-+QU2zd6OTD8XWIJCbffaiQeH9U73qIqafo1x6V1snCWYGJf6cVE0cDR4D8xRzcEnfI21IFrUPzPGtcPf8AC+Rw==", "dev": true, - "requires": { - "postcss": "^7.0.2" + "license": "ISC", + "dependencies": { + "imurmurhash": "^0.1.4", + "signal-exit": "^4.0.1" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, - "supports-color": { + "node_modules/supports-color": { "version": "5.5.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "requires": { + "dev": true, + "dependencies": { "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/supports-hyperlinks": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-3.2.0.tgz", + "integrity": "sha512-zFObLMyZeEwzAoKCyu1B91U79K2t7ApXuQfo8OuxwXLDgcKxuwM+YvcbIhm6QWqz7mHUH1TVytR1PwVVjEuMig==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0", + "supports-color": "^7.0.0" + }, + "engines": { + "node": ">=14.18" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, + "node_modules/supports-hyperlinks/node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-hyperlinks/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "supports-hyperlinks": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-2.2.0.tgz", - "integrity": "sha512-6sXEzV5+I5j8Bmq9/vUphGRM/RJNT9SCURJLjwfOg51heRtguGWDzcaBlgAzKhQa0EVNpPEKzQuBwZ8S8WaCeQ==", + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", "dev": true, - "requires": { - "has-flag": "^4.0.0", - "supports-color": "^7.0.0" + "engines": { + "node": ">= 0.4" }, - "dependencies": { - "has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "dev": true - }, - "supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "dev": true, - "requires": { - "has-flag": "^4.0.0" - } - } + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "svg-parser": { + "node_modules/svg-parser": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", "dev": true }, - "svg-tags": { + "node_modules/svg-tags": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/svg-tags/-/svg-tags-1.0.0.tgz", - "integrity": "sha1-WPcc7jvVGbWdSyqEO2x95krAR2Q=", + "integrity": "sha512-ovssysQTa+luh7A5Weu3Rta6FJlFBBbInjOh722LIt6klpU2/HtdUbszju/G4devcvk8PGt7FCLv5wftu3THUA==", "dev": true }, - "svgo": { + "node_modules/svgo": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/svgo/-/svgo-1.3.2.tgz", "integrity": "sha512-yhy/sQYxR5BkC98CY7o31VGsg014AKLEPxdfhora76l36hD9Rdy5NZA/Ocn6yayNPgSamYdtX2rFJdcv07AYVw==", + "deprecated": "This SVGO version is no longer supported. Upgrade to v2.x.x.", "dev": true, - "requires": { + "optional": true, + "dependencies": { "chalk": "^2.4.1", "coa": "^2.0.2", "css-select": "^2.0.0", @@ -24681,2345 +31223,2628 @@ "unquote": "~1.1.1", "util.promisify": "~1.0.0" }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/svgo/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "optional": true, "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - } + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" } }, - "symbol-tree": { + "node_modules/symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", - "dev": true + "dev": true, + "license": "MIT" + }, + "node_modules/synckit": { + "version": "0.11.12", + "resolved": "https://registry.npmjs.org/synckit/-/synckit-0.11.12.tgz", + "integrity": "sha512-Bh7QjT8/SuKUIfObSXNHNSK6WHo6J1tHCqJsuaFDP7gP0fkzSfTxI8y85JrppZ0h8l0maIgc2tfuZQ6/t3GtnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@pkgr/core": "^0.2.9" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/synckit" + } }, - "table": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/table/-/table-6.7.1.tgz", - "integrity": "sha512-ZGum47Yi6KOOFDE8m223td53ath2enHcYLgOCjGr5ngu8bdIARQk6mN/wRMv4yMRcHnCSnHbCEha4sobQx5yWg==", + "node_modules/table": { + "version": "6.9.0", + "resolved": "https://registry.npmjs.org/table/-/table-6.9.0.tgz", + "integrity": "sha512-9kY+CygyYM6j02t5YFHbNz2FN5QmYGv9zAjVp4lCDjlCw7amdckXlEt/bjMhUIfj4ThGRE4gCUH5+yGnNuPo5A==", "dev": true, - "requires": { + "license": "BSD-3-Clause", + "dependencies": { "ajv": "^8.0.1", - "lodash.clonedeep": "^4.5.0", "lodash.truncate": "^4.4.2", "slice-ansi": "^4.0.0", - "string-width": "^4.2.0", - "strip-ansi": "^6.0.0" + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1" }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/table/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", "dependencies": { - "ajv": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.6.2.tgz", - "integrity": "sha512-9807RlWAgT564wT+DjeyU5OFMPjmzxVobvDFmNAhY+5zD6A2ly3jDp6sgnfyDtlIQ+7H97oc/DGCzzfu9rjw9w==", - "dev": true, - "requires": { - "fast-deep-equal": "^3.1.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2", - "uri-js": "^4.2.2" - } - }, - "ansi-regex": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.0.tgz", - "integrity": "sha512-bY6fj56OUQ0hU1KjFNDQuJFezqKdrAyFdIevADiqrWHwSlbmBNMHp5ak2f40Pm8JTFyM2mqxkG6ngkHO11f/lg==", - "dev": true - }, - "emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true - }, - "is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "dev": true - }, - "json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "dev": true - }, - "string-width": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.2.tgz", - "integrity": "sha512-XBJbT3N4JhVumXE0eoLU9DCjcaF92KLNqTmFCnG1pf8duUxFGwtP6AD6nkjw9a3IdiRtL3E2w3JDiE/xi3vOeA==", - "dev": true, - "requires": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.0" - } - }, - "strip-ansi": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.0.tgz", - "integrity": "sha512-AuvKTrTfQNYNIctbR1K/YGTR1756GycPsg7b9bdV9Duqur4gv6aKqHXah67Z8ImS7WEz5QVcOtlfW2rZEugt6w==", - "dev": true, - "requires": { - "ansi-regex": "^5.0.0" - } - } + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "taffydb": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", - "integrity": "sha1-fLy2S1oUG2ou/CxdLGe04VCyomg=", - "dev": true + "node_modules/table/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" }, - "tannin": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/tannin/-/tannin-1.2.0.tgz", - "integrity": "sha512-U7GgX/RcSeUETbV7gYgoz8PD7Ni4y95pgIP/Z6ayI3CfhSujwKEBlGFTCRN+Aqnuyf4AN2yHL+L8x+TCGjb9uA==", - "requires": { - "@tannin/plural-forms": "^1.1.0" + "node_modules/table/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "tapable": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-1.1.3.tgz", - "integrity": "sha512-4WK/bYZmj8xLr+HUCODHGF1ZFzsYffasLUgEiMBY4fgtltdO6B4WJtlSbPaDTLpYTcGVwM2qLnFTICEcNxs3kA==", - "dev": true + "node_modules/table/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" }, - "tar": { - "version": "6.1.11", - "resolved": "https://registry.npmjs.org/tar/-/tar-6.1.11.tgz", - "integrity": "sha512-an/KZQzQUkZCkuoAA64hM92X0Urb6VpRhAFllDzz44U2mcD5scmT3zBc4VgVpkugF580+DQn8eAFSyoQt0tznA==", - "dev": true, - "requires": { - "chownr": "^2.0.0", - "fs-minipass": "^2.0.0", - "minipass": "^3.0.0", - "minizlib": "^2.1.1", - "mkdirp": "^1.0.3", - "yallist": "^4.0.0" + "node_modules/table/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" }, + "engines": { + "node": ">=8" + } + }, + "node_modules/table/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", "dependencies": { - "chownr": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", - "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true - }, - "mkdirp": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", - "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true - } + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "tar-fs": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", - "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "node_modules/taffydb": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/taffydb/-/taffydb-2.6.2.tgz", + "integrity": "sha512-y3JaeRSplks6NYQuCOj3ZFMO3j60rTwbuKCvZxsAraGYH2epusatvZ0baZYA01WsGqJBq/Dl6vOrMUJqyMj8kA==", + "dev": true + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/tar-fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.1.tgz", + "integrity": "sha512-LZA0oaPOc2fVo82Txf3gw+AkEd38szODlptMYejQUhndHMLQ9M059uXR+AfS7DNo0NpINvSqDsvyaCrBVkptWg==", "dev": true, - "requires": { - "chownr": "^1.1.1", - "mkdirp-classic": "^0.5.2", + "license": "MIT", + "dependencies": { "pump": "^3.0.0", - "tar-stream": "^2.1.4" + "tar-stream": "^3.1.5" + }, + "optionalDependencies": { + "bare-fs": "^4.0.1", + "bare-path": "^3.0.0" } }, - "tar-stream": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", - "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "node_modules/tar-stream": { + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-3.1.8.tgz", + "integrity": "sha512-U6QpVRyCGHva435KoNWy9PRoi2IFYCgtEhq9nmrPPpbRacPs9IH4aJ3gbrFC8dPcXvdSZ4XXfXT5Fshbp2MtlQ==", "dev": true, - "requires": { - "bl": "^4.0.3", - "end-of-stream": "^1.4.1", - "fs-constants": "^1.0.0", - "inherits": "^2.0.3", - "readable-stream": "^3.1.1" - }, - "dependencies": { - "readable-stream": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz", - "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==", - "dev": true, - "requires": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - } - } + "license": "MIT", + "dependencies": { + "b4a": "^1.6.4", + "bare-fs": "^4.5.5", + "fast-fifo": "^1.2.0", + "streamx": "^2.15.0" + } + }, + "node_modules/teex": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/teex/-/teex-1.0.1.tgz", + "integrity": "sha512-eYE6iEI62Ni1H8oIa7KlDU6uQBtqr4Eajni3wX7rpfXD8ysFx8z0+dri+KWEPWpBsxXfxu58x/0jvTVT1ekOSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "streamx": "^2.12.5" } }, - "temp-dir": { + "node_modules/temp-dir": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-1.0.0.tgz", "integrity": "sha1-CnwOom06Oa+n4OvqnB/AvE2qAR0=", "dev": true, - "optional": true + "optional": true, + "engines": { + "node": ">=4" + } }, - "tempfile": { + "node_modules/tempfile": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tempfile/-/tempfile-2.0.0.tgz", "integrity": "sha1-awRGhWqbERTRhW/8vlCczLCXcmU=", "dev": true, "optional": true, - "requires": { + "dependencies": { "temp-dir": "^1.0.0", "uuid": "^3.0.1" }, - "dependencies": { - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true, - "optional": true - } + "engines": { + "node": ">=4" } }, - "terminal-link": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-2.1.1.tgz", - "integrity": "sha512-un0FmiRUQNr5PJqy9kP7c40F5BOfpGlYTrxonDChEZB7pzZxRNp/bt+ymiy9/npwXya9KH99nJ/GXFIiUkYGFQ==", + "node_modules/tempfile/node_modules/uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", + "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", "dev": true, - "requires": { - "ansi-escapes": "^4.2.1", - "supports-hyperlinks": "^2.0.0" + "optional": true, + "bin": { + "uuid": "bin/uuid" } }, - "terser": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-4.8.0.tgz", - "integrity": "sha512-EAPipTNeWsb/3wLPeup1tVPaXfIaU68xMnVdPafIL1TV05OhASArYyIfFvnvJCNrR2NIOvDVNNTFRa+Re2MWyw==", + "node_modules/terser": { + "version": "5.37.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", + "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", "dev": true, - "requires": { + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.8.2", "commander": "^2.20.0", - "source-map": "~0.6.1", - "source-map-support": "~0.5.12" + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true } } }, - "terser-webpack-plugin": { - "version": "1.4.5", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-1.4.5.tgz", - "integrity": "sha512-04Rfe496lN8EYruwi6oPQkG0vo8C+HT49X687FZnpPF0qMAIHONI6HEXYPKDOE8e5HjXTyKfqRd/agHtH0kOtw==", + "node_modules/terser-webpack-plugin/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^4.0.0", - "source-map": "^0.6.1", - "terser": "^4.1.2", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/terser-webpack-plugin/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/terser-webpack-plugin/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/terser/node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" } }, - "test-exclude": { + "node_modules/test-exclude": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/test-exclude/-/test-exclude-6.0.0.tgz", "integrity": "sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==", "dev": true, - "requires": { + "dependencies": { "@istanbuljs/schema": "^0.1.2", "glob": "^7.1.4", "minimatch": "^3.0.4" + }, + "engines": { + "node": ">=8" } }, - "text-table": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", - "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", - "dev": true - }, - "thread-loader": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/thread-loader/-/thread-loader-3.0.4.tgz", - "integrity": "sha512-ByaL2TPb+m6yArpqQUZvP+5S1mZtXsEP7nWKKlAUTm7fCml8kB5s1uI3+eHRP2bk5mVYfRSBI7FFf+tWEyLZwA==", + "node_modules/text-decoder": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/text-decoder/-/text-decoder-1.1.0.tgz", + "integrity": "sha512-TmLJNj6UgX8xcUZo4UDStGQtDiTzF7BzWlzn9g7UWrjkpHr5uJTK1ld16wZ3LXb2vb6jH8qU89dW5whuMdXYdw==", "dev": true, - "requires": { - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^4.1.0", - "loader-utils": "^2.0.0", - "neo-async": "^2.6.2", - "schema-utils": "^3.0.0" - }, "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-runner": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.2.0.tgz", - "integrity": "sha512-92+huvxMvYlMzMt0iIOukcwYBFpkYJdpl2xsZ7LrlayO7E8SOv+JJUEK17B/dJIHAOLMfh2dZZ/Y18WgmGtYNw==", - "dev": true - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } - } + "b4a": "^1.6.4" } }, - "throat": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/throat/-/throat-5.0.0.tgz", - "integrity": "sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA==", + "node_modules/text-table": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", + "integrity": "sha512-N+8UisAXDGk8PFXP4HAzVR9nbfmVJ3zYLAWiTIoqC5v5isinhr+r5uaO8+7r3BMfuNIufIsA7RdpVgacC2cSpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/third-party-web": { + "version": "0.26.2", + "resolved": "https://registry.npmjs.org/third-party-web/-/third-party-web-0.26.2.tgz", + "integrity": "sha512-taJ0Us0lKoYBqcbccMuDElSUPOxmBfwlHe1OkHQ3KFf+RwovvBHdXhbFk9XJVQE2vHzxbTwvwg5GFsT9hbDokQ==", "dev": true }, - "through": { + "node_modules/through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", "dev": true }, - "through2": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", - "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", - "dev": true, - "requires": { - "readable-stream": "~2.3.6", - "xtend": "~4.0.1" - } - }, - "thunky": { + "node_modules/thunky": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", "dev": true }, - "timed-out": { + "node_modules/timed-out": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/timed-out/-/timed-out-4.0.1.tgz", "integrity": "sha1-8y6srFoXW+ol1/q1Zas+2HQe9W8=", "dev": true, - "optional": true - }, - "timers-browserify": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", - "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", - "dev": true, - "requires": { - "setimmediate": "^1.0.4" + "optional": true, + "engines": { + "node": ">=0.10.0" } }, - "timsort": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/timsort/-/timsort-0.3.0.tgz", - "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", - "dev": true - }, - "tiny-emitter": { + "node_modules/tiny-emitter": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==" }, - "tiny-glob": { - "version": "0.2.8", - "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.8.tgz", - "integrity": "sha512-vkQP7qOslq63XRX9kMswlby99kyO5OvKptw7AMwBVMjXEI7Tb61eoI5DydyEMOseyGS5anDN1VPoVxEvH01q8w==", + "node_modules/tiny-glob": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz", + "integrity": "sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==", "dev": true, - "requires": { + "dependencies": { "globalyzer": "0.1.0", "globrex": "^0.1.2" } }, - "tiny-lr": { + "node_modules/tiny-lr": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/tiny-lr/-/tiny-lr-1.1.1.tgz", "integrity": "sha512-44yhA3tsaRoMOjQQ+5v5mVdqef+kH6Qze9jTpqtVufgYjYt08zyZAwNwwVBj3i1rJMnR52IxOW0LK0vBzgAkuA==", "dev": true, - "requires": { + "dependencies": { "body": "^5.1.0", "debug": "^3.1.0", "faye-websocket": "~0.10.0", "livereload-js": "^2.3.0", "object-assign": "^4.1.0", "qs": "^6.4.0" + } + }, + "node_modules/tiny-lr/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyglobby/node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/tldts": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts/-/tldts-6.1.86.tgz", + "integrity": "sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==", + "dev": true, + "license": "MIT", "dependencies": { - "debug": { - "version": "3.2.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", - "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", - "dev": true, - "requires": { - "ms": "^2.1.1" - } - } + "tldts-core": "^6.1.86" + }, + "bin": { + "tldts": "bin/cli.js" } }, - "tinycolor2": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/tinycolor2/-/tinycolor2-1.4.2.tgz", - "integrity": "sha512-vJhccZPs965sV/L2sU4oRQVAos0pQXwsvTLkWYdqJ+a8Q5kPFzJTuOFwy7UniPli44NKQGAglksjvOcpo95aZA==" + "node_modules/tldts-core": { + "version": "6.1.86", + "resolved": "https://registry.npmjs.org/tldts-core/-/tldts-core-6.1.86.tgz", + "integrity": "sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==", + "dev": true, + "license": "MIT" + }, + "node_modules/tldts-icann": { + "version": "6.1.75", + "resolved": "https://registry.npmjs.org/tldts-icann/-/tldts-icann-6.1.75.tgz", + "integrity": "sha512-rCkEQd21uiOl+yKcX5DZ0mMd/DKl4f3NDTGH9zbSVG2tiRcJOx9MeBwPcbPr1u0kanopJnpzPEQJ+S2ky9iqFA==", + "dev": true, + "dependencies": { + "tldts-core": "^6.1.75" + } }, - "tmp": { + "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", "integrity": "sha512-jRCJlojKnZ3addtTOjdIqoRuPEKBvNXcGYqzO6zWZX8KfKEpnGY5jfggJQ3EjKuu8D4bJRr0y+cYJFmYbImXGw==", "dev": true, - "requires": { + "dependencies": { "os-tmpdir": "~1.0.2" + }, + "engines": { + "node": ">=0.6.0" } }, - "tmpl": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.4.tgz", - "integrity": "sha1-I2QN17QtAEM5ERQIIOXPRA5SHdE=", - "dev": true - }, - "to-arraybuffer": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", - "integrity": "sha1-fSKbH8xjfkZsoIEYCDanqr/4P0M=", + "node_modules/tmpl": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/tmpl/-/tmpl-1.0.5.tgz", + "integrity": "sha512-3f0uOEAQwIqGuWW2MVzYg8fV/QNnc/IpuJNG837rLuczAaLVHslWHZQj4IGiEl5Hs3kkbhwL9Ab7Hrsmuj+Smw==", "dev": true }, - "to-buffer": { + "node_modules/to-buffer": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/to-buffer/-/to-buffer-1.1.1.tgz", "integrity": "sha512-lx9B5iv7msuFYE3dytT+KE5tap+rNYw+K4jVkb9R/asAb+pbBSM17jtunHplhBe6RRJdZx3Pn2Jph24O32mOVg==", "dev": true, "optional": true }, - "to-fast-properties": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", - "integrity": "sha1-3F5pjL0HkmW8c+A3doGk5Og/YW4=" - }, - "to-object-path": { + "node_modules/to-object-path": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/to-object-path/-/to-object-path-0.3.0.tgz", "integrity": "sha1-KXWIt7Dn4KwI4E5nL4XB9JmeF68=", "dev": true, - "requires": { + "dependencies": { "kind-of": "^3.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "to-regex": { + "node_modules/to-regex": { "version": "3.0.2", "resolved": "https://registry.npmjs.org/to-regex/-/to-regex-3.0.2.tgz", "integrity": "sha512-FWtleNAtZ/Ki2qtqej2CXTOayOH9bHDQF+Q48VpWyDXjbYxA4Yz8iDB31zXOBUlOHHKidDbqGVrTUvQMPmBGBw==", "dev": true, - "requires": { + "dependencies": { "define-property": "^2.0.2", "extend-shallow": "^3.0.2", "regex-not": "^1.0.2", "safe-regex": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" } }, - "to-regex-range": { + "node_modules/to-regex-range": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-2.1.1.tgz", - "integrity": "sha1-fIDBe53+vlmeJzZ+DU3VWQFB2zg=", + "integrity": "sha512-ZZWNfCjUokXXDGXFpZehJIkZqq91BcULFq/Pi7M5i4JnxXdhMKAK682z8bCW3o8Hj1wuuzoKcW3DfVzaP6VuNg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "is-number": "^3.0.0", "repeat-string": "^1.6.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "toidentifier": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", - "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.6" + } }, - "toposort": { + "node_modules/toposort": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", - "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + "integrity": "sha512-0a5EOkAUp8D4moMi2W8ZF8jcga7BgZd91O/yabJCFY8az+XSzeGyTKs0Aoo897iV1Nj6guFq8orWDS96z91oGg==" }, - "totalist": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-1.1.0.tgz", - "integrity": "sha512-gduQwd1rOdDMGxFG1gEvhV88Oirdo2p+KjoYFU7k2g+i7n6AFFbDQ5kMPUsW0pNbfQsB/cwXvT1i4Bue0s9g5g==", - "dev": true - }, - "tough-cookie": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-4.0.0.tgz", - "integrity": "sha512-tHdtEpQCMrc1YLrMaqXXcj6AxhYi/xgit6mZu1+EDWUn+qhUf8wMQoFIy9NXuq23zAwtcB0t/MjACGR18pcRbg==", + "node_modules/totalist": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", + "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", "dev": true, - "requires": { - "psl": "^1.1.33", - "punycode": "^2.1.1", - "universalify": "^0.1.2" + "engines": { + "node": ">=6" } }, - "tr46": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tr46/-/tr46-2.1.0.tgz", - "integrity": "sha512-15Ih7phfcdP5YxqiB+iDtLoaTz4Nd35+IiAv0kQ5FNKHzXgdWqPoTIqEDDJmXceQt4JZk6lVPT8lnDlPpGDppw==", + "node_modules/tough-cookie": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-5.1.2.tgz", + "integrity": "sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==", "dev": true, - "requires": { - "punycode": "^2.1.1" + "license": "BSD-3-Clause", + "dependencies": { + "tldts": "^6.1.32" + }, + "engines": { + "node": ">=16" } }, - "traverse": { - "version": "0.6.6", - "resolved": "https://registry.npmjs.org/traverse/-/traverse-0.6.6.tgz", - "integrity": "sha1-y99WD9e5r2MlAv7UD5GMFX6pcTc=" + "node_modules/tr46": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz", + "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==", + "dev": true, + "license": "MIT", + "dependencies": { + "punycode": "^2.3.1" + }, + "engines": { + "node": ">=18" + } }, - "tree-kill": { + "node_modules/tree-kill": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", - "dev": true - }, - "trim": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/trim/-/trim-0.0.1.tgz", - "integrity": "sha1-WFhUf2spB1fulczMZm+1AITEYN0=", - "dev": true + "dev": true, + "bin": { + "tree-kill": "cli.js" + } }, - "trim-newlines": { + "node_modules/trim-newlines": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-newlines/-/trim-newlines-1.0.0.tgz", - "integrity": "sha1-WIeWa7WCpFA6QetST301ARgVphM=", + "integrity": "sha512-Nm4cF79FhSTzrLKGDMi3I4utBtFv8qKy4sq1enftf2gMdpqI8oVQTAfySkTz5r49giVzDj88SVZXP4CeYQwjaw==", "dev": true, - "optional": true + "optional": true, + "engines": { + "node": ">=0.10.0" + } }, - "trim-repeated": { + "node_modules/trim-repeated": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/trim-repeated/-/trim-repeated-1.0.0.tgz", "integrity": "sha1-42RqLqTokTEr9+rObPsFOAvAHCE=", "dev": true, - "requires": { + "dependencies": { "escape-string-regexp": "^1.0.2" + }, + "engines": { + "node": ">=0.10.0" } }, - "trim-trailing-lines": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/trim-trailing-lines/-/trim-trailing-lines-1.1.4.tgz", - "integrity": "sha512-rjUWSqnfTNrjbB9NQWfPMH/xRK1deHeGsHoVfpxJ++XeYXE0d6B1En37AHfw3jtfTU7dzMzZL2jjpe8Qb5gLIQ==", - "dev": true - }, - "trough": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/trough/-/trough-1.0.5.tgz", - "integrity": "sha512-rvuRbTarPXmMb79SmzEp8aqXNKcK+y0XaB298IXueQ8I2PsrATcPBCSPyK/dDNa2iWOhKlfNnOjdAOTBU/nkFA==", - "dev": true + "node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } }, - "tsconfig-paths": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.11.0.tgz", - "integrity": "sha512-7ecdYDnIdmv639mmDwslG6KQg1Z9STTz1j7Gcz0xa+nshh/gKDAHcPxRbWOsA3SPp0tXP2leTcY9Kw+NAkfZzA==", + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "@types/json5": "^0.0.29", - "json5": "^1.0.1", - "minimist": "^1.2.0", + "json5": "^1.0.2", + "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, - "tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "license": "MIT", + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "dev": true }, - "tsutils": { + "node_modules/tsutils": { "version": "3.21.0", "resolved": "https://registry.npmjs.org/tsutils/-/tsutils-3.21.0.tgz", "integrity": "sha512-mHKK3iUXL+3UF6xL5k0PEhKRUBKPBCv/+RkEOpjRWxxx27KKRBmmA60A9pgOUvMi8GKhRMPEmjBRPzs2W7O1OA==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "tslib": "^1.8.1" + }, + "engines": { + "node": ">= 6" + }, + "peerDependencies": { + "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" } }, - "tty-browserify": { - "version": "0.0.0", - "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", - "integrity": "sha1-oVe6QC2iTpv5V/mqadUk7tQpAaY=", - "dev": true + "node_modules/tsutils/node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "dev": true, + "license": "0BSD" }, - "tunnel-agent": { + "node_modules/tunnel-agent": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha1-J6XeoGs2sEoKmWZ3SykIaPD8QP0=", "dev": true, - "requires": { + "dependencies": { "safe-buffer": "^5.0.1" + }, + "engines": { + "node": "*" } }, - "turbo-combine-reducers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/turbo-combine-reducers/-/turbo-combine-reducers-1.0.2.tgz", - "integrity": "sha512-gHbdMZlA6Ym6Ur5pSH/UWrNQMIM9IqTH6SoL1DbHpqEdQ8i+cFunSmSlFykPt0eGQwZ4d/XTHOl74H0/kFBVWw==" - }, - "tweetnacl": { + "node_modules/tweetnacl": { "version": "0.14.5", "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", "integrity": "sha1-WuaBd/GS1EViadEIr6k/+HQ/T2Q=", "dev": true }, - "twemoji": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/twemoji/-/twemoji-13.1.0.tgz", - "integrity": "sha512-e3fZRl2S9UQQdBFLYXtTBT6o4vidJMnpWUAhJA+yLGR+kaUTZAt3PixC0cGvvxWSuq2MSz/o0rJraOXrWw/4Ew==", - "requires": { - "fs-extra": "^8.0.1", - "jsonfile": "^5.0.0", - "twemoji-parser": "13.1.0", - "universalify": "^0.1.2" - } - }, - "twemoji-parser": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/twemoji-parser/-/twemoji-parser-13.1.0.tgz", - "integrity": "sha512-AQOzLJpYlpWMy8n+0ATyKKZzWlZBJN+G0C+5lhX7Ftc2PeEVdUU/7ns2Pn2vVje26AIZ/OHwFoUbdv6YYD/wGg==" - }, - "type-check": { + "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", "dev": true, - "requires": { + "license": "MIT", + "dependencies": { "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" } }, - "type-detect": { + "node_modules/type-detect": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/type-detect/-/type-detect-4.0.8.tgz", "integrity": "sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==", - "dev": true + "dev": true, + "engines": { + "node": ">=4" + } }, - "type-fest": { - "version": "0.8.1", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz", - "integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==", - "dev": true + "node_modules/type-fest": { + "version": "0.20.2", + "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.20.2.tgz", + "integrity": "sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==", + "dev": true, + "license": "(MIT OR CC0-1.0)", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "type-is": { + "node_modules/type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "requires": { + "dev": true, + "license": "MIT", + "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/typed-array-buffer": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-buffer/-/typed-array-buffer-1.0.3.tgz", + "integrity": "sha512-nAYYwfY3qnzX30IkA6AQZjVbtK6duGontcQm1WSG1MD94YLqK0515GNApXkoxKOWMusVssAHWLh9SeaoefYFGw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" } }, - "typedarray": { - "version": "0.0.6", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", - "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", + "node_modules/typed-array-byte-length": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/typed-array-byte-length/-/typed-array-byte-length-1.0.3.tgz", + "integrity": "sha512-BaXgOuIxz8n8pIq3e7Atg/7s+DpiYrxn4vdot3w9KbnBhcRQq6o3xemQdIfynqSeXeDrF32x+WvfzmOjPiY9lg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.14" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-byte-offset": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/typed-array-byte-offset/-/typed-array-byte-offset-1.0.4.tgz", + "integrity": "sha512-bTlAFB/FBYMcuX81gbL4OcpH5PmlFHqlCCpAl8AlEzMz5k53oNDvN8p1PNOWLEmI2x4orp3raOFB51tv9X+MFQ==", + "dev": true, + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "for-each": "^0.3.3", + "gopd": "^1.2.0", + "has-proto": "^1.2.0", + "is-typed-array": "^1.1.15", + "reflect.getprototypeof": "^1.0.9" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-array-length": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/typed-array-length/-/typed-array-length-1.0.7.tgz", + "integrity": "sha512-3KS2b+kL7fsuk/eJZ7EQdnEmQoaho/r6KUef7hxvltNA5DR8NAUM+8wJMbJyZ4G9/7i3v5zPBIMN5aybAh2/Jg==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.7", + "for-each": "^0.3.3", + "gopd": "^1.0.1", + "is-typed-array": "^1.1.13", + "possible-typed-array-names": "^1.0.0", + "reflect.getprototypeof": "^1.0.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/typed-query-selector": { + "version": "2.12.0", + "resolved": "https://registry.npmjs.org/typed-query-selector/-/typed-query-selector-2.12.0.tgz", + "integrity": "sha512-SbklCd1F0EiZOyPiW192rrHZzZ5sBijB6xM+cpmrwDqObvdtunOHHIk9fCGsoK5JVIYXoyEp4iEdE3upFH3PAg==", "dev": true }, - "typedarray-to-buffer": { + "node_modules/typedarray-to-buffer": { "version": "3.1.5", "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", "dev": true, - "requires": { + "dependencies": { "is-typedarray": "^1.0.0" } }, - "ua-parser-js": { - "version": "0.7.28", - "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.28.tgz", - "integrity": "sha512-6Gurc1n//gjp9eQNXjD9O3M/sMwVtN5S8Lv9bvOYBfKfDNiIIhqiyi01vMBO45u4zkDE420w/e0se7Vs+sIg+g==" + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } }, - "uc.micro": { + "node_modules/uc.micro": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==", "dev": true }, - "uglify-js": { - "version": "3.13.9", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.9.tgz", - "integrity": "sha512-wZbyTQ1w6Y7fHdt8sJnHfSIuWeDgk6B5rCb4E/AM6QNNPbOMIZph21PW5dRB3h7Df0GszN+t7RuUH6sWK5bF0g==", - "dev": true - }, - "uglify-to-browserify": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/uglify-to-browserify/-/uglify-to-browserify-1.0.2.tgz", - "integrity": "sha1-bgkk1r2mta/jSeOabWMoUKD4grc=", - "optional": true - }, - "uglifyjs-webpack-plugin": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/uglifyjs-webpack-plugin/-/uglifyjs-webpack-plugin-2.2.0.tgz", - "integrity": "sha512-mHSkufBmBuJ+KHQhv5H0MXijtsoA1lynJt1lXOaotja8/I0pR4L9oGaPIZw+bQBOFittXZg9OC1sXSGO9D9ZYg==", - "dev": true, - "requires": { - "cacache": "^12.0.2", - "find-cache-dir": "^2.1.0", - "is-wsl": "^1.1.0", - "schema-utils": "^1.0.0", - "serialize-javascript": "^1.7.0", - "source-map": "^0.6.1", - "uglify-js": "^3.6.0", - "webpack-sources": "^1.4.0", - "worker-farm": "^1.7.0" + "node_modules/uglify-js": { + "version": "3.19.3", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz", + "integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "uglifyjs": "bin/uglifyjs" }, - "dependencies": { - "serialize-javascript": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-1.9.1.tgz", - "integrity": "sha512-0Vb/54WJ6k5v8sSWN09S0ora+Hnr+cX40r9F170nT+mSkaxltoE/7R3OrIdBSUv1OoiobH1QoWQbCnAO+e8J1A==", - "dev": true - }, - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "engines": { + "node": ">=0.8.0" } }, - "unbox-primitive": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.0.1.tgz", - "integrity": "sha512-tZU/3NqK3dA5gpE1KtyiJUrEB0lxnGkMFHptJ7q6ewdZ8s12QrODwNbhIJStmJkd1QDXa1NRA8aF2A1zk/Ypyw==", - "requires": { - "function-bind": "^1.1.1", - "has-bigints": "^1.0.1", - "has-symbols": "^1.0.2", - "which-boxed-primitive": "^1.0.2" - }, - "dependencies": { - "has-symbols": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.2.tgz", - "integrity": "sha512-chXa79rL/UC2KlX17jo3vRGz0azaWEx5tGqZg5pO3NUyEJVB17dMruQlzCCOfUvElghKcm5194+BCRvi2Rv/Gw==" - } + "node_modules/unbox-primitive": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz", + "integrity": "sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "has-bigints": "^1.0.2", + "has-symbols": "^1.1.0", + "which-boxed-primitive": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "unbzip2-stream": { + "node_modules/unbzip2-stream": { "version": "1.4.3", "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", "dev": true, - "requires": { + "dependencies": { "buffer": "^5.2.1", "through": "^2.3.8" } }, - "unc-path-regex": { + "node_modules/unc-path-regex": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/unc-path-regex/-/unc-path-regex-0.1.2.tgz", "integrity": "sha1-5z3T17DXxe2G+6xrCufYxqadUPo=", - "dev": true + "dev": true, + "engines": { + "node": ">=0.10.0" + } }, - "underscore": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", - "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==" + "node_modules/underscore": { + "version": "1.13.8", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.8.tgz", + "integrity": "sha512-DXtD3ZtEQzc7M8m4cXotyHR+FAS18C64asBYY5vqZexfYryNNnDc02W4hKg3rdQuqOYas1jkseX0+nZXjTXnvQ==", + "license": "MIT" }, - "underscore.string": { + "node_modules/underscore.string": { "version": "3.3.5", "resolved": "https://registry.npmjs.org/underscore.string/-/underscore.string-3.3.5.tgz", "integrity": "sha512-g+dpmgn+XBneLmXXo+sGlW5xQEt4ErkS3mgeN2GFbremYeMBSJKr9Wf2KJplQVaiPY/f7FN6atosWYNm9ovrYg==", "dev": true, - "requires": { + "dependencies": { "sprintf-js": "^1.0.3", "util-deprecate": "^1.0.2" + }, + "engines": { + "node": "*" } }, - "unherit": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/unherit/-/unherit-1.1.3.tgz", - "integrity": "sha512-Ft16BJcnapDKp0+J/rqFC3Rrk6Y/Ng4nzsC028k2jdDII/rdZ7Wd3pPT/6+vIIxRagwRc9K0IUX0Ra4fKvw+WQ==", + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", + "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", "dev": true, - "requires": { - "inherits": "^2.0.0", - "xtend": "^4.0.0" + "engines": { + "node": ">=4" } }, - "unicode-canonical-property-names-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-1.0.4.tgz", - "integrity": "sha512-jDrNnXWHd4oHiTZnx/ZG7gtUTVp+gCcTTKr8L0HjlwphROEW3+Him+IpvC+xcJEFegapiMZyZe02CyuOnRmbnQ==", - "dev": true - }, - "unicode-match-property-ecmascript": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-1.0.4.tgz", - "integrity": "sha512-L4Qoh15vTfntsn4P1zqnHulG0LdXgjSO035fEpdtp6YxXhMT51Q6vgM5lYdG/5X3MjS+k/Y9Xw4SFCY9IkR0rg==", + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", "dev": true, - "requires": { - "unicode-canonical-property-names-ecmascript": "^1.0.4", - "unicode-property-aliases-ecmascript": "^1.0.4" + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" } }, - "unicode-match-property-value-ecmascript": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-1.2.0.tgz", - "integrity": "sha512-wjuQHGQVofmSJv1uVISKLE5zO2rNGzM/KCYZch/QQvez7C1hUhBIuZ701fYXExuufJFMPhv2SyL8CyoIfMLbIQ==", - "dev": true - }, - "unicode-property-aliases-ecmascript": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-1.1.0.tgz", - "integrity": "sha512-PqSoPh/pWetQ2phoj5RLiaqIk4kCNwoV3CI+LfGmWLKI3rE3kl1h59XpX2BjgDrmbxD9ARtQobPGU1SguCYuQg==", - "dev": true + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.0.tgz", + "integrity": "sha512-4IehN3V/+kkr5YeSSDDQG8QLqO26XpL2XP3GQtqwlT/QYSECAwFztxVHjlbh0+gjJ3XmNLS0zDsbgs9jWKExLg==", + "dev": true, + "engines": { + "node": ">=4" + } }, - "unified": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/unified/-/unified-6.2.0.tgz", - "integrity": "sha512-1k+KPhlVtqmG99RaTbAv/usu85fcSRu3wY8X+vnsEhIxNP5VbVIDiXnLqyKIG+UMdyTg0ZX9EI6k2AfjJkHPtA==", + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", "dev": true, - "requires": { - "bail": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^1.1.0", - "trough": "^1.0.0", - "vfile": "^2.0.0", - "x-is-string": "^0.1.0" + "engines": { + "node": ">=4" } }, - "union-value": { + "node_modules/union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", "integrity": "sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==", "dev": true, - "requires": { + "dependencies": { "arr-union": "^3.1.0", "get-value": "^2.0.6", "is-extendable": "^0.1.1", "set-value": "^2.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "uniq": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz", - "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=", - "dev": true - }, - "uniqs": { + "node_modules/unique-string": { "version": "2.0.0", - "resolved": "https://registry.npmjs.org/uniqs/-/uniqs-2.0.0.tgz", - "integrity": "sha1-/+3ks2slKQaW5uFl1KWe25mOawI=", - "dev": true - }, - "unique-filename": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-1.1.1.tgz", - "integrity": "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ==", - "dev": true, - "requires": { - "unique-slug": "^2.0.0" - } - }, - "unique-slug": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-2.0.2.tgz", - "integrity": "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w==", - "dev": true, - "requires": { - "imurmurhash": "^0.1.4" - } - }, - "unist-util-find-all-after": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/unist-util-find-all-after/-/unist-util-find-all-after-3.0.2.tgz", - "integrity": "sha512-xaTC/AGZ0rIM2gM28YVRAFPIZpzbpDtU3dRmp7EXlNVA8ziQc4hY3H7BHXM1J49nEmiqc3svnqMReW+PGqbZKQ==", + "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-2.0.0.tgz", + "integrity": "sha512-uNaeirEPvpZWSgzwsPGtU2zVSTrn/8L5q/IexZmH0eH6SA73CmAA5U4GwORTxQAZs95TAXLNqeLoPPNO5gZfWg==", "dev": true, - "requires": { - "unist-util-is": "^4.0.0" - }, "dependencies": { - "unist-util-is": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-4.1.0.tgz", - "integrity": "sha512-ZOQSsnce92GrxSqlnEEseX0gi7GH9zTJZ0p9dtu87WRb/37mMPO2Ilx1s/t9vBHrFhbgweUwb+t7cIn5dxPhZg==", - "dev": true - } - } - }, - "unist-util-is": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-3.0.0.tgz", - "integrity": "sha512-sVZZX3+kspVNmLWBPAB6r+7D9ZgAFPNWm66f7YNb420RlQSbn+n8rG8dGZSkrER7ZIXGQYNm5pqC3v3HopH24A==", - "dev": true - }, - "unist-util-remove-position": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/unist-util-remove-position/-/unist-util-remove-position-1.1.4.tgz", - "integrity": "sha512-tLqd653ArxJIPnKII6LMZwH+mb5q+n/GtXQZo6S6csPRs5zB0u79Yw8ouR3wTw8wxvdJFhpP6Y7jorWdCgLO0A==", - "dev": true, - "requires": { - "unist-util-visit": "^1.1.0" - } - }, - "unist-util-stringify-position": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-1.1.2.tgz", - "integrity": "sha512-pNCVrk64LZv1kElr0N1wPiHEUoXNVFERp+mlTg/s9R5Lwg87f9bM/3sQB99w+N9D/qnM9ar3+AKDBwo/gm/iQQ==", - "dev": true - }, - "unist-util-visit": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-1.4.1.tgz", - "integrity": "sha512-AvGNk7Bb//EmJZyhtRUnNMEpId/AZ5Ph/KUpTI09WHQuDZHKovQ1oEv3mfmKpWKtoMzyMC4GLBm1Zy5k12fjIw==", - "dev": true, - "requires": { - "unist-util-visit-parents": "^2.0.0" - } - }, - "unist-util-visit-parents": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-2.1.2.tgz", - "integrity": "sha512-DyN5vD4NE3aSeB+PXYNKxzGsfocxp6asDc2XXE3b0ekO2BaRUpBicbbUygfSvYfUz1IkmjFR1YF7dPklraMZ2g==", - "dev": true, - "requires": { - "unist-util-is": "^3.0.0" + "crypto-random-string": "^2.0.0" + }, + "engines": { + "node": ">=8" } }, - "universalify": { + "node_modules/universalify": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", - "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" - }, - "unorm": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/unorm/-/unorm-1.6.0.tgz", - "integrity": "sha512-b2/KCUlYZUeA7JFUuRJZPUtr4gZvBh7tavtv4fvk4+KV9pfGiR6CQAQAWl49ZpR3ts2dk4FYkP7EIgDJoiOLDA==" + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.0.0" + } }, - "unpipe": { + "node_modules/unpipe": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=" + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8" + } }, - "unquote": { + "node_modules/unquote": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/unquote/-/unquote-1.1.1.tgz", "integrity": "sha1-j97XMk7G6IoP+LkF58CYzcCG1UQ=", - "dev": true + "dev": true, + "optional": true }, - "unset-value": { + "node_modules/unrs-resolver": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/unrs-resolver/-/unrs-resolver-1.11.1.tgz", + "integrity": "sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "dependencies": { + "napi-postinstall": "^0.3.0" + }, + "funding": { + "url": "https://opencollective.com/unrs-resolver" + }, + "optionalDependencies": { + "@unrs/resolver-binding-android-arm-eabi": "1.11.1", + "@unrs/resolver-binding-android-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-arm64": "1.11.1", + "@unrs/resolver-binding-darwin-x64": "1.11.1", + "@unrs/resolver-binding-freebsd-x64": "1.11.1", + "@unrs/resolver-binding-linux-arm-gnueabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm-musleabihf": "1.11.1", + "@unrs/resolver-binding-linux-arm64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-arm64-musl": "1.11.1", + "@unrs/resolver-binding-linux-ppc64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-riscv64-musl": "1.11.1", + "@unrs/resolver-binding-linux-s390x-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-gnu": "1.11.1", + "@unrs/resolver-binding-linux-x64-musl": "1.11.1", + "@unrs/resolver-binding-wasm32-wasi": "1.11.1", + "@unrs/resolver-binding-win32-arm64-msvc": "1.11.1", + "@unrs/resolver-binding-win32-ia32-msvc": "1.11.1", + "@unrs/resolver-binding-win32-x64-msvc": "1.11.1" + } + }, + "node_modules/unset-value": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/unset-value/-/unset-value-1.0.0.tgz", "integrity": "sha1-g3aHP30jNRef+x5vw6jtDfyKtVk=", "dev": true, - "requires": { + "dependencies": { "has-value": "^0.3.1", "isobject": "^3.0.0" }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value": { + "version": "0.3.1", + "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", + "integrity": "sha512-gpG936j8/MzaeID5Yif+577c17TxaDmhuyVgSwtnL/q8UUTySg8Mecb+8Cf1otgLoD7DDH75axp86ER7LFsf3Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "get-value": "^2.0.3", + "has-values": "^0.1.4", + "isobject": "^2.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-value/node_modules/isobject": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", + "integrity": "sha512-+OUdGJlgjOBZDfxnDjYYG6zp487z0JGNQq3cYQYg5f5hKR+syHMsaztzGeml/4kGG55CSpKSpWTY+jYGgsHLgA==", + "dev": true, + "license": "MIT", "dependencies": { - "has-value": { - "version": "0.3.1", - "resolved": "https://registry.npmjs.org/has-value/-/has-value-0.3.1.tgz", - "integrity": "sha1-ex9YutpiyoJ+wKIHgCVlSEWZXh8=", - "dev": true, - "requires": { - "get-value": "^2.0.3", - "has-values": "^0.1.4", - "isobject": "^2.0.0" - }, - "dependencies": { - "isobject": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-2.1.0.tgz", - "integrity": "sha1-8GVWEJaj8dou9GJy+BXIQNh+DIk=", - "dev": true, - "requires": { - "isarray": "1.0.0" - } - } - } + "isarray": "1.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/unset-value/node_modules/has-values": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", + "integrity": "sha512-J8S0cEdWuQbqD9//tlZxiMuMNmxB8PlEwvYwuxsTmR1G5RXUePEX/SJn7aD0GMLieuZYSwNH0cQuJGwnYunXRQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" }, - "has-values": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/has-values/-/has-values-0.1.4.tgz", - "integrity": "sha1-bWHeldkd/Km5oCCJrThL/49it3E=", - "dev": true + { + "type": "github", + "url": "https://github.com/sponsors/ai" } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" } }, - "upath": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/upath/-/upath-1.2.0.tgz", - "integrity": "sha512-aZwGpamFO61g3OlfT7OQCHqhGnW43ieH9WZeP7QxN/G/jS4jfqUkZxoryvJgVPEcrl5NL/ggHsSmLMHuH64Lhg==", - "dev": true + "node_modules/upper-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case/-/upper-case-2.0.2.tgz", + "integrity": "sha512-KgdgDGJt2TpuwBUIjgG6lzw2GWFRCW9Qkfkiv0DxqHHLYJHmtmdUIKcZd8rHgFSjopVTlw6ggzCm1b8MFQwikg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } }, - "uri-js": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.0.tgz", - "integrity": "sha512-B0yRTzYdUCCn9n+F4+Gh4yIDtMQcaJsmYBDsTSG8g/OejKBodLQ2IHfN3bM7jUsRXndopT7OIXWdYqc1fjmV6g==", + "node_modules/upper-case-first": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/upper-case-first/-/upper-case-first-2.0.2.tgz", + "integrity": "sha512-514ppYHBaKwfJRK/pNC6c/OxfGa0obSnAl106u97Ed0I625Nin96KAjttZF6ZL3e1XLtphxnqrOi9iWgm+u+bg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", "dev": true, - "requires": { + "dependencies": { "punycode": "^2.1.0" } }, - "uri-path": { + "node_modules/uri-path": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/uri-path/-/uri-path-1.0.0.tgz", "integrity": "sha1-l0fwGDWJM8Md4PzP2C0TjmcmLjI=", - "dev": true + "dev": true, + "engines": { + "node": ">= 0.10" + } }, - "urix": { + "node_modules/urix": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/urix/-/urix-0.1.0.tgz", "integrity": "sha1-2pN/emLiH+wf0Y1Js1wpNQZ6bHI=", + "deprecated": "Please see https://github.com/lydell/urix#deprecated", "dev": true }, - "url": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/url/-/url-0.11.0.tgz", - "integrity": "sha1-ODjpfPxgUh63PFJajlW/3Z4uKPE=", - "dev": true, - "requires": { - "punycode": "1.3.2", - "querystring": "0.2.0" - }, - "dependencies": { - "punycode": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.3.2.tgz", - "integrity": "sha1-llOgNvt8HuQjQvIyXM7v6jkmxI0=", - "dev": true - } - } - }, - "url-loader": { + "node_modules/url-loader": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", "dev": true, - "requires": { + "dependencies": { "loader-utils": "^2.0.0", "mime-types": "^2.1.27", "schema-utils": "^3.0.0" }, - "dependencies": { - "@types/json-schema": { - "version": "7.0.9", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.9.tgz", - "integrity": "sha512-qcUXuemtEu+E5wZSJHNxUXeCZhAfXKQ41D+duX+VYPde7xyEVZci+/oXKJL13tnRs9lR2pr4fod59GT6/X1/yQ==", - "dev": true - }, - "json5": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.0.tgz", - "integrity": "sha512-f+8cldu7X/y7RAJurMEJmdoKXGB/X550w2Nr3tTbezL6RwEE/iMcm+tZnXeoZtKuOq6ft8+CqzEkrIgx1fPoQA==", - "dev": true, - "requires": { - "minimist": "^1.2.5" - } - }, - "loader-utils": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz", - "integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==", - "dev": true, - "requires": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - } - }, - "schema-utils": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.1.1.tgz", - "integrity": "sha512-Y5PQxS4ITlC+EahLuXaY86TXfR7Dc5lw294alXOq86JAHCihAIZfqv8nNCWvaEJvaC51uN9hbLGeV0cFBdH+Fw==", - "dev": true, - "requires": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - } + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "file-loader": "*", + "webpack": "^4.0.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "file-loader": { + "optional": true } } }, - "url-parse": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/url-parse/-/url-parse-1.5.1.tgz", - "integrity": "sha512-HOfCOUJt7iSYzEx/UqgtwKRMC6EU91NFhsCHMv9oM03VJcVo2Qrp8T8kI9D7amFf1cu+/3CEhgb3rF9zL7k85Q==", - "dev": true, - "requires": { - "querystringify": "^2.1.1", - "requires-port": "^1.0.0" - } - }, - "url-parse-lax": { + "node_modules/url-parse-lax": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/url-parse-lax/-/url-parse-lax-1.0.0.tgz", - "integrity": "sha1-evjzA2Rem9eaJy56FKxovAYJ2nM=", + "integrity": "sha512-BVA4lR5PIviy2PMseNd2jbFQ+jwSwQGdJejf5ctd1rEXt0Ypd7yanUK9+lYechVlN5VaTJGsu2U/3MDDu6KgBA==", "dev": true, + "license": "MIT", "optional": true, - "requires": { + "dependencies": { "prepend-http": "^1.0.1" + }, + "engines": { + "node": ">=0.10.0" } }, - "url-to-options": { + "node_modules/url-to-options": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/url-to-options/-/url-to-options-1.0.1.tgz", "integrity": "sha1-FQWgOiiaSMvXpDTvuu7FBV9WM6k=", "dev": true, - "optional": true - }, - "use": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", - "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", - "dev": true - }, - "use-memo-one": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/use-memo-one/-/use-memo-one-1.1.2.tgz", - "integrity": "sha512-u2qFKtxLsia/r8qG0ZKkbytbztzRb317XCkT7yP8wxL0tZ/CzK2G+WWie5vWvpyeP7+YoPIwbJoIHJ4Ba4k0oQ==" + "optional": true, + "engines": { + "node": ">= 4" + } }, - "usertiming": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/usertiming/-/usertiming-0.1.8.tgz", - "integrity": "sha1-NTeOf0GiSNQOZY0F+AQjRpp7BlA=" + "node_modules/urlpattern-polyfill": { + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/urlpattern-polyfill/-/urlpattern-polyfill-10.0.0.tgz", + "integrity": "sha512-H/A06tKD7sS1O1X2SshBVeA5FLycRpjqiBeqGKmBwBDBy28EnRjORxTNe269KSSr5un5qyWi1iL61wLxpd+ZOg==", + "dev": true, + "license": "MIT" }, - "util": { - "version": "0.11.1", - "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", - "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "node_modules/use": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/use/-/use-3.1.1.tgz", + "integrity": "sha512-cwESVXlO3url9YWlFW/TA9cshCEhtu7IKJ/p5soJ/gGpj7vbvFrAY/eIioQ6Dw23KjZhYgiIo8HOs1nQ2vr/oQ==", "dev": true, - "requires": { - "inherits": "2.0.3" - }, - "dependencies": { - "inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", - "dev": true - } + "engines": { + "node": ">=0.10.0" } }, - "util-deprecate": { + "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=" }, - "util.promisify": { + "node_modules/util.promisify": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/util.promisify/-/util.promisify-1.0.1.tgz", "integrity": "sha512-g9JpC/3He3bm38zsLupWryXHoEcS22YHthuPQSJdMy6KNrzIRzWqcsHzD/WUnqe45whVou4VIsPew37DoXWNrA==", "dev": true, - "requires": { + "optional": true, + "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.2", "has-symbols": "^1.0.1", "object.getownpropertydescriptors": "^2.1.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "utils-merge": { + "node_modules/utils-merge": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=" - }, - "uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } }, - "v8-compile-cache": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/v8-compile-cache/-/v8-compile-cache-2.3.0.tgz", - "integrity": "sha512-l8lCEmLcLYZh4nbunNZvQCJc5pv7+RCwa8q/LdUx8u7lsWvPDKmpodJAJNwkAhJC//dFY48KuIEmjtd4RViDrA==", - "dev": true + "node_modules/uuid": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", + "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist-node/bin/uuid" + } }, - "v8-to-istanbul": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-7.1.2.tgz", - "integrity": "sha512-TxNb7YEUwkLXCQYeudi6lgQ/SZrzNO4kMdlqVxaZPUIUjCv6iSSypUQX70kNBSERpQ8fk48+d61FXk+tgqcWow==", + "node_modules/v8-to-istanbul": { + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/v8-to-istanbul/-/v8-to-istanbul-9.1.0.tgz", + "integrity": "sha512-6z3GW9x8G1gd+JIIgQQQxXuiJtCXeAjp6RaPEPLv62mH3iPHPxV6W3robxtCzNErRo6ZwTmzWhsbNvjyEBKzKA==", "dev": true, - "requires": { + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.12", "@types/istanbul-lib-coverage": "^2.0.1", - "convert-source-map": "^1.6.0", - "source-map": "^0.7.3" + "convert-source-map": "^1.6.0" }, - "dependencies": { - "source-map": { - "version": "0.7.3", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.3.tgz", - "integrity": "sha512-CkCj6giN3S+n9qrYiBTX5gystlENnRW5jZeNLHpe6aue+SrHcG5VYwujhW9s4dY31mEGsxBDrHR6oI69fTXsaQ==", - "dev": true - } + "engines": { + "node": ">=10.12.0" } }, - "v8flags": { + "node_modules/v8flags": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/v8flags/-/v8flags-3.2.0.tgz", "integrity": "sha512-mH8etigqMfiGWdeXpaaqGfs6BndypxusHHcv2qSHyZkGEznCd/qAXCWWRzeowtL54147cktFOC4P5y+kl8d8Jg==", "dev": true, - "requires": { + "dependencies": { "homedir-polyfill": "^1.0.1" + }, + "engines": { + "node": ">= 0.10" } }, - "validate-npm-package-license": { + "node_modules/validate-npm-package-license": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", "dev": true, - "requires": { + "dependencies": { "spdx-correct": "^3.0.0", "spdx-expression-parse": "^3.0.0" } }, - "vary": { + "node_modules/validate-npm-package-name": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-5.0.0.tgz", + "integrity": "sha512-YuKoXDAhBYxY7SfOKxHBDoSyENFeW5VvIIQp2TGQuit8gpK6MnWaQelBKxso72DoxTZfZdcP3W90LqpSkgPzLQ==", + "dev": true, + "dependencies": { + "builtins": "^5.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/vary": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=" - }, - "vendors": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/vendors/-/vendors-1.0.4.tgz", - "integrity": "sha512-/juG65kTL4Cy2su4P8HjtkTxk6VmJDiOPBufWniqQ6wknac6jNiXS9vU+hO3wgusiyqWlzTbVHi0dyJqRONg3w==", - "dev": true + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } }, - "verror": { + "node_modules/verror": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", "integrity": "sha1-OhBcoXBTr1XW4nDB+CiGguGNpAA=", "dev": true, - "requires": { + "engines": [ + "node >=0.6.0" + ], + "dependencies": { "assert-plus": "^1.0.0", "core-util-is": "1.0.2", "extsprintf": "^1.2.0" } }, - "vfile": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-2.3.0.tgz", - "integrity": "sha512-ASt4mBUHcTpMKD/l5Q+WJXNtshlWxOogYyGYYrg4lt/vuRjC1EFQtlAofL5VmtVNIZJzWYFJjzGWZ0Gw8pzW1w==", + "node_modules/w3c-xmlserializer": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-5.0.0.tgz", + "integrity": "sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==", "dev": true, - "requires": { - "is-buffer": "^1.1.4", - "replace-ext": "1.0.0", - "unist-util-stringify-position": "^1.0.0", - "vfile-message": "^1.0.0" + "license": "MIT", + "dependencies": { + "xml-name-validator": "^5.0.0" + }, + "engines": { + "node": ">=18" } }, - "vfile-location": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-2.0.6.tgz", - "integrity": "sha512-sSFdyCP3G6Ka0CEmN83A2YCMKIieHx0EDaj5IDP4g1pa5ZJ4FJDvpO0WODLxo4LUX4oe52gmSCK7Jw4SBghqxA==", - "dev": true + "node_modules/wait-on": { + "version": "9.0.4", + "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-9.0.4.tgz", + "integrity": "sha512-k8qrgfwrPVJXTeFY8tl6BxVHiclK11u72DVKhpybHfUL/K6KM4bdyK9EhIVYGytB5MJe/3lq4Tf0hrjM+pvJZQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "axios": "^1.13.5", + "joi": "^18.0.2", + "lodash": "^4.17.23", + "minimist": "^1.2.8", + "rxjs": "^7.8.2" + }, + "bin": { + "wait-on": "bin/wait-on" + }, + "engines": { + "node": ">=20.0.0" + } }, - "vfile-message": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-1.1.1.tgz", - "integrity": "sha512-1WmsopSGhWt5laNir+633LszXvZ+Z/lxveBf6yhGsqnQIhlhzooZae7zV6YVM1Sdkw68dtAW3ow0pOdPANugvA==", + "node_modules/wait-on/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", "dev": true, - "requires": { - "unist-util-stringify-position": "^1.1.1" + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.1.0" } }, - "vm-browserify": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", - "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==", - "dev": true + "node_modules/walker": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.8.tgz", + "integrity": "sha512-ts/8E8l5b7kY0vlWLewOkDXMmPdLcVV4GmOQLyxuSswIJsweeFZtAsMF7k1Nszz+TYBQrlYRmzOnr398y1JemQ==", + "dev": true, + "dependencies": { + "makeerror": "1.0.12" + } }, - "void-elements": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", - "integrity": "sha1-wGavtYK7HLQSjWDqkjkulNXp2+w=" + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } }, - "w3c-hr-time": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/w3c-hr-time/-/w3c-hr-time-1.0.2.tgz", - "integrity": "sha512-z8P5DvDNjKDoFIHK7q8r8lackT6l+jo/Ye3HOle7l9nICP9lf1Ci25fy9vHd0JOWewkIFzXIEig3TdKT7JQ5fQ==", + "node_modules/watchpack/node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/wbuf": { + "version": "1.7.3", + "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", + "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", "dev": true, - "requires": { - "browser-process-hrtime": "^1.0.0" + "dependencies": { + "minimalistic-assert": "^1.0.0" } }, - "w3c-xmlserializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/w3c-xmlserializer/-/w3c-xmlserializer-2.0.0.tgz", - "integrity": "sha512-4tzD0mF8iSiMiNs30BiLO3EpfGLZUT2MSX/G+o7ZywDzliWQ3OPtTZ0PTC3B3ca1UAf4cJMHB+2Bf56EriJuRA==", + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/web-vitals": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.3.tgz", + "integrity": "sha512-/CFAm1mNxSmOj6i0Co+iGFJ58OS4NRGVP+AWS/l509uIK5a1bSoIVaHz/ZumpHTfHSZBpgrJ+wjfpAOrTHok5Q==", + "dev": true + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", "dev": true, - "requires": { - "xml-name-validator": "^3.0.0" + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" } }, - "wait-on": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/wait-on/-/wait-on-5.3.0.tgz", - "integrity": "sha512-DwrHrnTK+/0QFaB9a8Ol5Lna3k7WvUR4jzSKmz0YaPBpuN2sACyiPVKVfj6ejnjcajAcvn3wlbTyMIn9AZouOg==", + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", "dev": true, - "requires": { - "axios": "^0.21.1", - "joi": "^17.3.0", - "lodash": "^4.17.21", - "minimist": "^1.2.5", - "rxjs": "^6.6.3" - }, - "dependencies": { - "rxjs": { - "version": "6.6.6", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.6.tgz", - "integrity": "sha512-/oTwee4N4iWzAMAL9xdGKjkEHmIwupR3oXbQjCKywF1BeFohswF3vZdogbmEF6pZkOsXTzWkrZszrWpQTByYVg==", - "dev": true, - "requires": { - "tslib": "^1.9.0" - } + "license": "MIT", + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true } } }, - "wait-port": { - "version": "0.2.9", - "resolved": "https://registry.npmjs.org/wait-port/-/wait-port-0.2.9.tgz", - "integrity": "sha512-hQ/cVKsNqGZ/UbZB/oakOGFqic00YAMM5/PEj3Bt4vKarv2jWIWzDbqlwT94qMs/exAQAsvMOq99sZblV92zxQ==", + "node_modules/webpack-bundle-analyzer": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.1.tgz", + "integrity": "sha512-s3P7pgexgT/HTUSYgxJyn28A+99mmLq4HsJepMPzu0R8ImJc52QNqaFYW1Z2z2uIb1/J3eYgaAWVpaC+v/1aAQ==", "dev": true, - "requires": { - "chalk": "^2.4.2", - "commander": "^3.0.2", - "debug": "^4.1.1" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - } - }, - "commander": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", - "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==", - "dev": true - } + "dependencies": { + "@discoveryjs/json-ext": "0.5.7", + "acorn": "^8.0.4", + "acorn-walk": "^8.0.0", + "commander": "^7.2.0", + "debounce": "^1.2.1", + "escape-string-regexp": "^4.0.0", + "gzip-size": "^6.0.0", + "html-escaper": "^2.0.2", + "is-plain-object": "^5.0.0", + "opener": "^1.5.2", + "picocolors": "^1.0.0", + "sirv": "^2.0.3", + "ws": "^7.3.1" + }, + "bin": { + "webpack-bundle-analyzer": "lib/bin/analyzer.js" + }, + "engines": { + "node": ">= 10.13.0" } }, - "walker": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/walker/-/walker-1.0.7.tgz", - "integrity": "sha1-L3+bj9ENZ3JisYqITijRlhjgKPs=", + "node_modules/webpack-bundle-analyzer/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", "dev": true, - "requires": { - "makeerror": "1.0.x" + "engines": { + "node": ">= 10" } }, - "watchpack": { - "version": "1.7.5", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-1.7.5.tgz", - "integrity": "sha512-9P3MWk6SrKjHsGkLT2KHXdQ/9SNkyoJbabxnKOoJepsvJjJG8uYTR3yTPxPQvNDI3w4Nz1xnE0TLHK4RIVe/MQ==", + "node_modules/webpack-bundle-analyzer/node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", "dev": true, - "requires": { - "chokidar": "^3.4.1", - "graceful-fs": "^4.1.2", - "neo-async": "^2.5.0", - "watchpack-chokidar2": "^2.0.1" + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "watchpack-chokidar2": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/watchpack-chokidar2/-/watchpack-chokidar2-2.0.1.tgz", - "integrity": "sha512-nCFfBIPKr5Sh61s4LPpy1Wtfi0HE8isJ3d2Yb5/Ppw2P2B/3eVSEBjKfN0fmHJSK14+31KwMKmcrzs2GM4P0Ww==", + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", "dev": true, - "optional": true, - "requires": { - "chokidar": "^2.1.8" - }, - "dependencies": { - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "optional": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "optional": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { "optional": true }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "optional": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "optional": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "optional": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "optional": true, - "requires": { - "binary-extensions": "^1.0.0" - } + "webpack-bundle-analyzer": { + "optional": true }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "optional": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } + "webpack-dev-server": { + "optional": true } } }, - "wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", "dev": true, - "requires": { - "minimalistic-assert": "^1.0.0" + "engines": { + "node": ">=14" } }, - "web-animations-js": { - "version": "2.3.2", - "resolved": "https://registry.npmjs.org/web-animations-js/-/web-animations-js-2.3.2.tgz", - "integrity": "sha512-TOMFWtQdxzjWp8qx4DAraTWTsdhxVSiWa6NkPFSaPtZ1diKUxTn4yTix73A1euG1WbSOMMPcY51cnjTIHrGtDA==" + "node_modules/webpack-cli/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } }, - "webidl-conversions": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-5.0.0.tgz", - "integrity": "sha512-VlZwKPCkYKxQgeSbH5EyngOmRp7Ww7I9rQLERETtf5ofd9pGeswWiOtogpEO850jziPRarreGxn5QIiTqpb2wA==" - }, - "webpack": { - "version": "4.43.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-4.43.0.tgz", - "integrity": "sha512-GW1LjnPipFW2Y78OOab8NJlCflB7EFskMih2AHdvjbpKMeDJqEgSx24cXXXiPS65+WSwVyxtDsJH6jGX2czy+g==", - "dev": true, - "requires": { - "@webassemblyjs/ast": "1.9.0", - "@webassemblyjs/helper-module-context": "1.9.0", - "@webassemblyjs/wasm-edit": "1.9.0", - "@webassemblyjs/wasm-parser": "1.9.0", - "acorn": "^6.4.1", - "ajv": "^6.10.2", - "ajv-keywords": "^3.4.1", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^4.1.0", - "eslint-scope": "^4.0.3", - "json-parse-better-errors": "^1.0.2", - "loader-runner": "^2.4.0", - "loader-utils": "^1.2.3", - "memory-fs": "^0.4.1", - "micromatch": "^3.1.10", - "mkdirp": "^0.5.3", - "neo-async": "^2.6.1", - "node-libs-browser": "^2.2.1", - "schema-utils": "^1.0.0", - "tapable": "^1.1.3", - "terser-webpack-plugin": "^1.4.3", - "watchpack": "^1.6.1", - "webpack-sources": "^1.4.1" - } - }, - "webpack-bundle-analyzer": { - "version": "4.4.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.4.2.tgz", - "integrity": "sha512-PIagMYhlEzFfhMYOzs5gFT55DkUdkyrJi/SxJp8EF3YMWhS+T9vvs2EoTetpk5qb6VsCq02eXTlRDOydRhDFAQ==", + "node_modules/webpack-cli/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", "dev": true, - "requires": { - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "chalk": "^4.1.0", - "commander": "^6.2.0", - "gzip-size": "^6.0.0", - "lodash": "^4.17.20", - "opener": "^1.5.2", - "sirv": "^1.0.7", - "ws": "^7.3.1" + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-cli/node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/webpack-cli/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-cli/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, "dependencies": { - "acorn": { - "version": "8.4.1", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.4.1.tgz", - "integrity": "sha512-asabaBSkEKosYKMITunzX177CXxQ4Q8BSSzMTKD+FefUhipQC70gfW5SiUDhYQ3vk8G+81HqQk7Fv9OXwwn9KA==", - "dev": true + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-middleware/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true }, - "acorn-walk": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.1.1.tgz", - "integrity": "sha512-FbJdceMlPHEAWJOILDk1fXD8lnTlEIWFkqtfk+MvmL5q/qlHfN7GEHcsFZWt/Tea9jRNPWUZG4G976nqAAmU9w==", - "dev": true + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/webpack-dev-server/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true + }, + "node_modules/webpack-dev-server/node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true }, - "commander": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-6.2.1.tgz", - "integrity": "sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==", - "dev": true + "utf-8-validate": { + "optional": true } } }, - "webpack-cli": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-3.3.12.tgz", - "integrity": "sha512-NVWBaz9k839ZH/sinurM+HcDvJOTXwSjYp1ku+5XKeOC03z8v5QitnK/x+lAxGXFyhdayoIf/GOpv85z3/xPag==", + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-merge/node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/webpack-merge/node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/webpack-merge/node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", "dev": true, - "requires": { - "chalk": "^2.4.2", - "cross-spawn": "^6.0.5", - "enhanced-resolve": "^4.1.1", - "findup-sync": "^3.0.0", - "global-modules": "^2.0.0", - "import-local": "^2.0.0", - "interpret": "^1.4.0", - "loader-utils": "^1.4.0", - "supports-color": "^6.1.0", - "v8-compile-cache": "^2.1.1", - "yargs": "^13.3.2" - }, - "dependencies": { - "chalk": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", - "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", - "dev": true, - "requires": { - "ansi-styles": "^3.2.1", - "escape-string-regexp": "^1.0.5", - "supports-color": "^5.3.0" - }, - "dependencies": { - "supports-color": { - "version": "5.5.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", - "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - } - } - }, - "cross-spawn": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-6.0.5.tgz", - "integrity": "sha512-eTVLrBSt7fjbDygz805pMnstIs2VTBNkRm0qxZd+M7A5XDdxVRWO5MxGBXZhjY4cqLYLdtrGqRf8mBPmzwSpWQ==", - "dev": true, - "requires": { - "nice-try": "^1.0.4", - "path-key": "^2.0.1", - "semver": "^5.5.0", - "shebang-command": "^1.2.0", - "which": "^1.2.9" - } - }, - "global-modules": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/global-modules/-/global-modules-2.0.0.tgz", - "integrity": "sha512-NGbfmJBp9x8IxyJSd1P+otYK8vonoJactOogrVfFRIAEY1ukil8RSKDz2Yo7wh1oihl51l/r6W4epkeKJHqL8A==", - "dev": true, - "requires": { - "global-prefix": "^3.0.0" - } - }, - "global-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/global-prefix/-/global-prefix-3.0.0.tgz", - "integrity": "sha512-awConJSVCHVGND6x3tmMaKcQvwXLhjdkmomy2W+Goaui8YPgYgXJZewhg3fWC+DlfqqQuWg8AwqjGTD2nAPVWg==", - "dev": true, - "requires": { - "ini": "^1.3.5", - "kind-of": "^6.0.2", - "which": "^1.3.1" - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "dev": true - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "semver": { - "version": "5.7.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.1.tgz", - "integrity": "sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==", - "dev": true - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "which": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", - "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", - "dev": true, - "requires": { - "isexe": "^2.0.0" - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "engines": { + "node": ">=0.10.0" } }, - "webpack-dev-middleware": { - "version": "3.7.3", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-3.7.3.tgz", - "integrity": "sha512-djelc/zGiz9nZj/U7PTBi2ViorGJXEWo/3ltkPbDyxCXhhEXkW0ce99falaok4TPj+AsxLiXJR0EBOb0zh9fKQ==", + "node_modules/webpack-merge/node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", "dev": true, - "requires": { - "memory-fs": "^0.4.1", - "mime": "^2.4.4", - "mkdirp": "^0.5.1", - "range-parser": "^1.2.1", - "webpack-log": "^2.0.0" + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" } }, - "webpack-dev-server": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-3.11.2.tgz", - "integrity": "sha512-A80BkuHRQfCiNtGBS1EMf2ChTUs0x+B3wGDFmOeT4rmJOHhHTCH2naNxIHhmkr0/UillP4U3yeIyv1pNp+QDLQ==", + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", "dev": true, - "requires": { - "ansi-html": "0.0.7", - "bonjour": "^3.5.0", - "chokidar": "^2.1.8", - "compression": "^1.7.4", - "connect-history-api-fallback": "^1.6.0", - "debug": "^4.1.1", - "del": "^4.1.1", - "express": "^4.17.1", - "html-entities": "^1.3.1", - "http-proxy-middleware": "0.19.1", - "import-local": "^2.0.0", - "internal-ip": "^4.3.0", - "ip": "^1.1.5", - "is-absolute-url": "^3.0.3", - "killable": "^1.0.1", - "loglevel": "^1.6.8", - "opn": "^5.5.0", - "p-retry": "^3.0.1", - "portfinder": "^1.0.26", - "schema-utils": "^1.0.0", - "selfsigned": "^1.10.8", - "semver": "^6.3.0", - "serve-index": "^1.9.1", - "sockjs": "^0.3.21", - "sockjs-client": "^1.5.0", - "spdy": "^4.0.2", - "strip-ansi": "^3.0.1", - "supports-color": "^6.1.0", - "url": "^0.11.0", - "webpack-dev-middleware": "^3.7.2", - "webpack-log": "^2.0.0", - "ws": "^6.2.1", - "yargs": "^13.3.2" - }, - "dependencies": { - "ansi-regex": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", - "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", - "dev": true - }, - "anymatch": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-2.0.0.tgz", - "integrity": "sha512-5teOsQWABXHHBFP9y3skS5P3d/WfWXpv3FUpy+LorMrNYaT9pI4oLMQX7jzQ2KklNpGpWHzdCXTDT2Y3XGlZBw==", - "dev": true, - "requires": { - "micromatch": "^3.1.4", - "normalize-path": "^2.1.1" - }, - "dependencies": { - "normalize-path": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-2.1.1.tgz", - "integrity": "sha1-GrKLVW4Zg2Oowab35vogE3/mrtk=", - "dev": true, - "requires": { - "remove-trailing-separator": "^1.0.1" - } - } - } - }, - "binary-extensions": { - "version": "1.13.1", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-1.13.1.tgz", - "integrity": "sha512-Un7MIEDdUC5gNpcGDV97op1Ywk748MpHcFTHoYs6qnj1Z3j7I53VG3nwZhKzoBZmbdRNnb6WRdFlwl7tSDuZGw==", - "dev": true - }, - "chokidar": { - "version": "2.1.8", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-2.1.8.tgz", - "integrity": "sha512-ZmZUazfOzf0Nve7duiCKD23PFSCs4JPoYyccjUFF3aQkQadqBhfzhjkwBH2mNOG9cTBwhamM37EIsIkZw3nRgg==", - "dev": true, - "requires": { - "anymatch": "^2.0.0", - "async-each": "^1.0.1", - "braces": "^2.3.2", - "fsevents": "^1.2.7", - "glob-parent": "^3.1.0", - "inherits": "^2.0.3", - "is-binary-path": "^1.0.0", - "is-glob": "^4.0.0", - "normalize-path": "^3.0.0", - "path-is-absolute": "^1.0.0", - "readdirp": "^2.2.1", - "upath": "^1.1.1" - } - }, - "fsevents": { - "version": "1.2.13", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-1.2.13.tgz", - "integrity": "sha512-oWb1Z6mkHIskLzEJ/XWX0srkpkTQ7vaopMQkyaEIoq0fmtFVxOthb8cCxeT+p3ynTdkk/RZwbgG4brR5BeWECw==", - "dev": true, - "optional": true, - "requires": { - "bindings": "^1.5.0", - "nan": "^2.12.1" - } - }, - "glob-parent": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-3.1.0.tgz", - "integrity": "sha1-nmr2KZ2NO9K9QEMIMr0RPfkGxa4=", - "dev": true, - "requires": { - "is-glob": "^3.1.0", - "path-dirname": "^1.0.0" - }, - "dependencies": { - "is-glob": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-3.1.0.tgz", - "integrity": "sha1-e6WuJCF4BKxwcHuWkiVnSGzD6Eo=", - "dev": true, - "requires": { - "is-extglob": "^2.1.0" - } - } - } - }, - "import-local": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/import-local/-/import-local-2.0.0.tgz", - "integrity": "sha512-b6s04m3O+s3CGSbqDIyP4R6aAwAeYlVq9+WUWep6iHa8ETRf9yei1U48C5MmfJmV9AiLYYBKPMq/W+/WRpQmCQ==", - "dev": true, - "requires": { - "pkg-dir": "^3.0.0", - "resolve-cwd": "^2.0.0" - } - }, - "is-absolute-url": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-3.0.3.tgz", - "integrity": "sha512-opmNIX7uFnS96NtPmhWQgQx6/NYFgsUXYMllcfzwWKUMwfo8kku1TvE6hkNcH+Q1ts5cMVrsY7j0bxXQDciu9Q==", - "dev": true - }, - "is-binary-path": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-1.0.1.tgz", - "integrity": "sha1-dfFmQrSA8YenEcgUFh/TpKdlWJg=", - "dev": true, - "requires": { - "binary-extensions": "^1.0.0" - } - }, - "readdirp": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-2.2.1.tgz", - "integrity": "sha512-1JU/8q+VgFZyxwrJ+SVIOsh+KywWGpds3NTqikiKpDMZWScmAYyKIgqkO+ARvNWJfXeXR1zxz7aHF4u4CyH6vQ==", - "dev": true, - "requires": { - "graceful-fs": "^4.1.11", - "micromatch": "^3.1.10", - "readable-stream": "^2.0.2" - } - }, - "resolve-cwd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-2.0.0.tgz", - "integrity": "sha1-AKn3OHVW4nA46uIyyqNypqWbZlo=", - "dev": true, - "requires": { - "resolve-from": "^3.0.0" - } - }, - "resolve-from": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-3.0.0.tgz", - "integrity": "sha1-six699nWiBvItuZTM17rywoYh0g=", - "dev": true - }, - "semver": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", - "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==", - "dev": true - }, - "strip-ansi": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", - "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", - "dev": true, - "requires": { - "ansi-regex": "^2.0.0" - } - }, - "supports-color": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz", - "integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==", - "dev": true, - "requires": { - "has-flag": "^3.0.0" - } - }, - "ws": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ws/-/ws-6.2.2.tgz", - "integrity": "sha512-zmhltoSR8u1cnDsD43TX59mzoMZsLKqUweyYBAIvTngR3shc0W6aOZylZmq/7hqyVxPdi+5Ud2QInblgyE72fw==", - "dev": true, - "requires": { - "async-limiter": "~1.0.0" - }, - "dependencies": { - "async-limiter": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/async-limiter/-/async-limiter-1.0.1.tgz", - "integrity": "sha512-csOlWGAcRFJaI6m+F2WKdnMKr4HhdhFVBk0H/QbJFMCr+uO2kwohwXQPxw/9OCxp05r5ghVBFSyioixx3gfkNQ==", - "dev": true - } - } - }, - "yargs": { - "version": "13.3.2", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-13.3.2.tgz", - "integrity": "sha512-AX3Zw5iPruN5ie6xGRIDgqkT+ZhnRlZMLMHAs8tg7nRruy2Nb+i5o9bwghAogtM08q1dpr2LVoS8KSTMYpWXUw==", - "dev": true, - "requires": { - "cliui": "^5.0.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", - "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^13.1.2" - } - }, - "yargs-parser": { - "version": "13.1.2", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-13.1.2.tgz", - "integrity": "sha512-3lbsNRf/j+A4QuSZfDRA7HRSfWrzO0YjqTJd5kjAq37Zep1CEgaYmrH9Q3GwPiB9cHyd1Y1UwggGhJGoxipbzg==", - "dev": true, - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" - } - } + "license": "MIT", + "engines": { + "node": ">=10.13.0" } }, - "webpack-livereload-plugin": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/webpack-livereload-plugin/-/webpack-livereload-plugin-2.3.0.tgz", - "integrity": "sha512-vVBLQLlNpElt2sfsBG+XLDeVbQFS4RrniVU8Hi1/hX5ycSfx6mtW8MEEITr2g0Cvo36kuPWShFFDuy+DS7KFMA==", + "node_modules/webpack/node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", "dev": true, - "requires": { - "anymatch": "^3.1.1", - "portfinder": "^1.0.17", - "tiny-lr": "^1.1.1" + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" } }, - "webpack-log": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/webpack-log/-/webpack-log-2.0.0.tgz", - "integrity": "sha512-cX8G2vR/85UYG59FgkoMamwHUIkSSlV3bBMRsbxVXVUk2j6NleCKjQ/WE9eYg9WY4w25O9w8wKP4rzNZFmUcUg==", + "node_modules/webpack/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", "dev": true, - "requires": { - "ansi-colors": "^3.0.0", - "uuid": "^3.3.2" - }, + "license": "MIT", "dependencies": { - "ansi-colors": { - "version": "3.2.4", - "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-3.2.4.tgz", - "integrity": "sha512-hHUXGagefjN2iRrID63xckIvotOXOojhQKWIPUZ4mNUZ9nLZW+7FMNoE1lOkEhNWYsx/7ysGIuJYCiMAA9FnrA==", - "dev": true - }, - "uuid": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", - "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", - "dev": true - } + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" } }, - "webpack-sources": { - "version": "1.4.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-1.4.3.tgz", - "integrity": "sha512-lgTS3Xhv1lCOKo7SA5TjKXMjpSM4sBjNV5+q2bqesbSPs5FjGmU6jjtBSkX9b4qW87vDIsCIlUPOEhbZrMdjeQ==", + "node_modules/webpack/node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true + }, + "node_modules/webpack/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", "dev": true, - "requires": { - "source-list-map": "^2.0.0", - "source-map": "~0.6.1" - }, + "license": "MIT" + }, + "node_modules/webpack/node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true - } + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "websocket-driver": { + "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", "dev": true, - "requires": { + "dependencies": { "http-parser-js": ">=0.5.1", "safe-buffer": ">=5.1.0", "websocket-extensions": ">=0.1.1" + }, + "engines": { + "node": ">=0.8.0" } }, - "websocket-extensions": { + "node_modules/websocket-extensions": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "dev": true + "dev": true, + "engines": { + "node": ">=0.8.0" + } }, - "whatwg-encoding": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", - "integrity": "sha512-b5lim54JOPN9HtzvK9HFXvBma/rnfFeqsic0hSpjtDbVxR3dJKLc+KB4V6GgiGOvl7CY/KNh8rxSo9DKQrnUEw==", + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", "dev": true, - "requires": { - "iconv-lite": "0.4.24" - }, + "license": "MIT", "dependencies": { - "iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, - "requires": { - "safer-buffer": ">= 2.1.2 < 3" - } - } + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" } }, - "whatwg-fetch": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.2.tgz", - "integrity": "sha512-bJlen0FcuU/0EMLrdbJ7zOnW6ITZLrZMIarMUVmdKtsGvZna8vxKYaexICWPfZ8qwf9fzNq+UEIZrnSaApt6RA==" - }, - "whatwg-mimetype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz", - "integrity": "sha512-M4yMwr6mAnQz76TbJm914+gPpB/nCwvZbJU28cUD6dR004SAxDLOOSUaB1JDRqLtaOV/vi0IC5lEAGFgrjGv/g==", - "dev": true + "node_modules/whatwg-fetch": { + "version": "3.6.20", + "resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.6.20.tgz", + "integrity": "sha512-EqhiFU6daOA8kpjOWTL0olhVOF3i7OrFzSYiGsEMB8GcXS+RrzauAERX65xMeNWVqxA6HXH2m69Z9LaKKdisfg==" }, - "whatwg-url": { - "version": "8.7.0", - "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-8.7.0.tgz", - "integrity": "sha512-gAojqb/m9Q8a5IV96E3fHJM70AzCkgt4uXYX2O7EmuyOnLrViCQlsEBmF9UQIu3/aeAIp2U17rtbpZWNntQqdg==", + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", "dev": true, - "requires": { - "lodash": "^4.7.0", - "tr46": "^2.1.0", - "webidl-conversions": "^6.1.0" - }, - "dependencies": { - "webidl-conversions": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-6.1.0.tgz", - "integrity": "sha512-qBIvFLGiBpLjfwmYAaHPXsn+ho5xZnGvyGvsarywGNc8VyQJUMHJ8OBKGGrPER0okBeMDaan4mNBlgBROxuI8w==", - "dev": true - } + "license": "MIT", + "engines": { + "node": ">=18" } }, - "whatwg-url-without-unicode": { - "version": "8.0.0-3", - "resolved": "https://registry.npmjs.org/whatwg-url-without-unicode/-/whatwg-url-without-unicode-8.0.0-3.tgz", - "integrity": "sha512-HoKuzZrUlgpz35YO27XgD28uh/WJH4B0+3ttFqRo//lmq+9T/mIOJ6kqmINI9HpUpz1imRC/nR/lxKpJiv0uig==", - "requires": { - "buffer": "^5.4.3", - "punycode": "^2.1.1", - "webidl-conversions": "^5.0.0" + "node_modules/whatwg-url": { + "version": "14.2.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz", + "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tr46": "^5.1.0", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=18" } }, - "which": { + "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", "dev": true, - "requires": { + "dependencies": { "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" } }, - "which-boxed-primitive": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz", - "integrity": "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg==", - "requires": { - "is-bigint": "^1.0.1", - "is-boolean-object": "^1.1.0", - "is-number-object": "^1.0.4", - "is-string": "^1.0.5", - "is-symbol": "^1.0.3" + "node_modules/which-boxed-primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.1.1.tgz", + "integrity": "sha512-TbX3mj8n0odCBFVlY8AxkqcHASw3L60jIuF8jFP78az3C2YhmGvqbHBpAjTRH2/xqYunrJ9g1jSyjCjpoWzIAA==", + "dev": true, + "dependencies": { + "is-bigint": "^1.1.0", + "is-boolean-object": "^1.2.1", + "is-number-object": "^1.1.1", + "is-string": "^1.1.1", + "is-symbol": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" } }, - "which-module": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.0.tgz", - "integrity": "sha1-2e8H3Od7mQK4o6j6SzHD4/fm6Ho=" + "node_modules/which-builtin-type": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/which-builtin-type/-/which-builtin-type-1.2.1.tgz", + "integrity": "sha512-6iBczoX+kDQ7a3+YJBnh3T+KZRxM/iYNPXicqk66/Qfm1b93iu+yOImkg0zHbj5LNOcNv1TEADiZ0xa34B4q6Q==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "function.prototype.name": "^1.1.6", + "has-tostringtag": "^1.0.2", + "is-async-function": "^2.0.0", + "is-date-object": "^1.1.0", + "is-finalizationregistry": "^1.1.0", + "is-generator-function": "^1.0.10", + "is-regex": "^1.2.1", + "is-weakref": "^1.0.2", + "isarray": "^2.0.5", + "which-boxed-primitive": "^1.1.0", + "which-collection": "^1.0.2", + "which-typed-array": "^1.1.16" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "wicg-inert": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.1.tgz", - "integrity": "sha512-PhBaNh8ur9Xm4Ggy4umelwNIP6pPP1bv3EaWaKqfb/QNme2rdLjm7wIInvV4WhxVHhzA4Spgw9qNSqWtB/ca2A==" + "node_modules/which-builtin-type/node_modules/isarray": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-2.0.5.tgz", + "integrity": "sha512-xHjhDr3cNBK0BzdUJSPXZntQUx/mwMS5Rw4A7lPJ90XGAO6ISP/ePDNuo0vhqOZU+UD5JoodwCAAoZQd3FeAKw==", + "dev": true }, - "window-size": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/window-size/-/window-size-0.1.0.tgz", - "integrity": "sha1-VDjNLqk7IC76Ohn+iIeu58lPnJ0=" + "node_modules/which-collection": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/which-collection/-/which-collection-1.0.2.tgz", + "integrity": "sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==", + "dev": true, + "dependencies": { + "is-map": "^2.0.3", + "is-set": "^2.0.3", + "is-weakmap": "^2.0.2", + "is-weakset": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } }, - "word-wrap": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", - "integrity": "sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==", + "node_modules/which-typed-array": { + "version": "1.1.19", + "resolved": "https://registry.npmjs.org/which-typed-array/-/which-typed-array-1.1.19.tgz", + "integrity": "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw==", + "dev": true, + "license": "MIT", + "dependencies": { + "available-typed-arrays": "^1.0.7", + "call-bind": "^1.0.8", + "call-bound": "^1.0.4", + "for-each": "^0.3.5", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/wicg-inert": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/wicg-inert/-/wicg-inert-3.1.3.tgz", + "integrity": "sha512-5L0PKK7iP+0Q/jv2ccgmkz/pfXbumZtlEyWS/xnX+L+Og3f7WjL4+iEs18k4IuldOX3PgGpza3qGndL9xUBjCQ==", + "license": "W3C-20150513" + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", "dev": true }, - "wordwrap": { - "version": "0.0.2", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.2.tgz", - "integrity": "sha1-t5Zpu0LstAn4PVg8rVLKF+qhZD8=" + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } }, - "worker-farm": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/worker-farm/-/worker-farm-1.7.0.tgz", - "integrity": "sha512-rvw3QTZc8lAxyVrqcSGVm5yP/IJ2UcB3U0graE3LCFoZ0Yn2x4EoVSqJKdB/T5M+FLcRPjz4TDacRf3OCfNUzw==", + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", "dev": true, - "requires": { - "errno": "~0.1.7" + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "wrap-ansi": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-5.1.0.tgz", - "integrity": "sha512-QC1/iN/2/RPVJ5jYK8BGttj5z83LmSKmvbvrXPNCLZSEb32KKVDJDl/MOt2N01qU2H/FkzEa9PKto1BqDjtd7Q==", - "requires": { - "ansi-styles": "^3.2.0", - "string-width": "^3.0.0", - "strip-ansi": "^5.0.0" + "node_modules/wrap-ansi/node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/wrap-ansi/node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/wrap-ansi/node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/wrap-ansi/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" } }, - "wrappy": { + "node_modules/wrap-ansi/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrap-ansi/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/wrappy": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" + "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", + "dev": true }, - "write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", + "node_modules/write-file-atomic": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz", + "integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==", "dev": true, - "requires": { + "dependencies": { "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" + "signal-exit": "^3.0.7" + }, + "engines": { + "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, - "ws": { - "version": "7.4.6", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.6.tgz", - "integrity": "sha512-YmhHDO4MzaDLB+M9ym/mDA5z0naX8j7SIlT8f8z+I0VtzsRbekxEutHSme7NPS2qE8StCYQNUnfWdXta/Yu85A==" + "node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "dev": true, + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } }, - "x-is-string": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/x-is-string/-/x-is-string-0.1.0.tgz", - "integrity": "sha1-R0tQhlrzpJqcRlfwWs0UVFj3fYI=", - "dev": true + "node_modules/xdg-basedir": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-4.0.0.tgz", + "integrity": "sha512-PSNhEJDejZYV7h50BohL09Er9VaIefr2LMAf3OEmpCkjOi34eYyQYAXUTjEQtZJTKcF0E2UKTh+osDLsgNim9Q==", + "dev": true, + "engines": { + "node": ">=8" + } }, - "xml-name-validator": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-3.0.0.tgz", - "integrity": "sha512-A5CUptxDsvxKJEU3yO6DuWBSJz/qizqzJKOMIfUJHETbBw/sFaDxgd6fxm1ewUaM0jZ444Fc5vC5ROYurg/4Pw==", - "dev": true + "node_modules/xml": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz", + "integrity": "sha512-huCv9IH9Tcf95zuYCsQraZtWnJvBtLVE0QHMOs8bWyZAFZNDcYjsPq1nEx8jKA9y+Beo9v+7OBPRisQTjinQMw==", + "license": "MIT" + }, + "node_modules/xml-name-validator": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-5.0.0.tgz", + "integrity": "sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } }, - "xmlbuilder": { + "node_modules/xmlbuilder": { "version": "8.2.2", "resolved": "https://registry.npmjs.org/xmlbuilder/-/xmlbuilder-8.2.2.tgz", "integrity": "sha1-aSSGc0ELS6QuGmE2VR0pIjNap3M=", - "dev": true + "dev": true, + "engines": { + "node": ">=4.0" + } }, - "xmlchars": { + "node_modules/xmlchars": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/xmlchars/-/xmlchars-2.2.0.tgz", "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", - "dev": true - }, - "xmlcreate": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/xmlcreate/-/xmlcreate-2.0.3.tgz", - "integrity": "sha512-HgS+X6zAztGa9zIK3Y3LXuJes33Lz9x+YyTxgrkIdabu2vqcGOWwdfCpf1hWLRrd553wd4QCDf6BBO6FfdsRiQ==", - "dev": true + "dev": true, + "license": "MIT" }, - "xmlrpc": { + "node_modules/xmlrpc": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/xmlrpc/-/xmlrpc-1.3.2.tgz", "integrity": "sha1-JrLqNHhI0Ciqx+dRS1NRl23j6D0=", "dev": true, - "requires": { + "dependencies": { "sax": "1.2.x", "xmlbuilder": "8.2.x" + }, + "engines": { + "node": ">=0.8", + "npm": ">=1.0.0" } }, - "xtend": { + "node_modules/xtend": { "version": "4.0.2", "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", - "dev": true - }, - "y18n": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.1.tgz", - "integrity": "sha512-wNcy4NvjMYL8gogWWYAO7ZFWFfHcbdbE57tZO8e4cbpj8tfUcwrwqSl3ad8HxpYWCdXcJUCeKKZS62Av1affwQ==" + "dev": true, + "optional": true, + "engines": { + "node": ">=0.4" + } }, - "yaku": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/yaku/-/yaku-1.0.1.tgz", - "integrity": "sha512-uZCrhA5DEytGnFfw9XZKIoWRG43v6oCA20vInFS4anPJb3G5Hy+PKuaKSMTJ+aChvFdNDxY5K8jFx5wmlLbTjQ==" + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } }, - "yallist": { + "node_modules/yallist": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==" + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", + "dev": true }, - "yaml": { + "node_modules/yaml": { "version": "1.10.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-1.10.2.tgz", - "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==" - }, - "yargs": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/yargs/-/yargs-14.2.3.tgz", - "integrity": "sha512-ZbotRWhF+lkjijC/VhmOT9wSgyBQ7+zr13+YLkhfsSiTriYsMzkTUFP18pFhWwBeMa5gUc1MzbhrO6/VB7c9Xg==", - "requires": { - "cliui": "^5.0.0", - "decamelize": "^1.2.0", - "find-up": "^3.0.0", - "get-caller-file": "^2.0.1", + "integrity": "sha512-r3vXyErRCYJ7wg28yvBY5VSoAF8ZvlcW9/BwUzEtUsjvX/DKs24dIkuwjtuprwJJHsbyUbLApepYTR1BN4uHrg==", + "dev": true, + "engines": { + "node": ">= 6" + } + }, + "node_modules/yargs": { + "version": "17.7.2", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", + "integrity": "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^8.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", - "require-main-filename": "^2.0.0", - "set-blocking": "^2.0.0", - "string-width": "^3.0.0", - "which-module": "^2.0.0", - "y18n": "^4.0.0", - "yargs-parser": "^15.0.1" + "string-width": "^4.2.3", + "y18n": "^5.0.5", + "yargs-parser": "^21.1.1" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs-parser": { + "version": "21.1.1", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz", + "integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/yargs/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/yargs/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/yargs/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" } }, - "yargs-parser": { - "version": "15.0.3", - "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-15.0.3.tgz", - "integrity": "sha512-/MVEVjTXy/cGAjdtQf8dW3V9b97bPN7rNn8ETj6BmAQL7ibC7O1Q9SPJbGjgh3SlwoBNXMzj/ZGIj8mBgl12YA==", - "requires": { - "camelcase": "^5.0.0", - "decamelize": "^1.2.0" + "node_modules/yargs/node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" } }, - "yauzl": { + "node_modules/yauzl": { "version": "2.10.0", "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", "integrity": "sha1-x+sXyT4RLLEIb6bY5R+wZnt5pfk=", "dev": true, - "requires": { + "dependencies": { "buffer-crc32": "~0.2.3", "fd-slicer": "~1.1.0" } }, - "yocto-queue": { + "node_modules/yocto-queue": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", - "dev": true + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } }, - "zwitch": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-1.0.5.tgz", - "integrity": "sha512-V50KMwwzqJV0NpZIZFwfOD5/lyny3WlSzRiXgA0G7VUnRlqttta1L6UQIHzd6EuBY/cHGfwTIck7w1yH6Q5zUw==", - "dev": true + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index d3b6998d602c5..396ae5589560b 100644 --- a/package.json +++ b/package.json @@ -1,14 +1,18 @@ { "name": "WordPress", - "version": "5.9.0", + "version": "7.1.0", "description": "WordPress is open source software you can use to create a beautiful website, blog, or app.", "repository": { "type": "svn", "url": "https://develop.svn.wordpress.org/trunk" }, + "gutenberg": { + "sha": "a2a354cf35e5b69c3330d6c1cfd42d8dc2efb9fd", + "ghcrRepo": "WordPress/gutenberg/gutenberg-wp-develop-build" + }, "engines": { - "node": ">=14.15.0", - "npm": ">=6.14.8" + "node": ">=20.10.0", + "npm": ">=10.2.3" }, "author": "The WordPress Contributors", "license": "GPL-2.0-or-later", @@ -24,156 +28,119 @@ "last 2 Opera versions" ], "devDependencies": { - "@wordpress/babel-preset-default": "6.2.1", - "@wordpress/custom-templated-path-webpack-plugin": "2.0.5", - "@wordpress/dependency-extraction-webpack-plugin": "3.1.4", - "@wordpress/e2e-test-utils": "5.4.3", - "@wordpress/library-export-default-webpack-plugin": "2.0.5", - "@wordpress/scripts": "16.1.5", - "autoprefixer": "^9.8.6", - "chalk": "4.1.1", - "check-node-version": "4.1.0", - "copy-webpack-plugin": "^5.1.2", - "cssnano": "4.1.11", - "dotenv": "10.0.0", - "dotenv-expand": "5.1.0", - "grunt": "~1.4.1", + "@lodder/grunt-postcss": "^3.1.1", + "@playwright/test": "1.58.2", + "@pmmmwh/react-refresh-webpack-plugin": "0.6.2", + "@types/codemirror": "5.60.17", + "@types/espree": "10.1.0", + "@types/htmlhint": "1.1.5", + "@types/jquery": "3.5.34", + "@types/underscore": "1.13.0", + "@wordpress/e2e-test-utils-playwright": "1.42.0", + "@wordpress/prettier-config": "4.42.0", + "@wordpress/scripts": "31.7.0", + "autoprefixer": "10.4.27", + "chalk": "5.6.2", + "check-node-version": "4.2.1", + "cssnano": "7.1.3", + "dotenv": "17.3.1", + "dotenv-expand": "12.0.3", + "grunt": "1.6.1", "grunt-banner": "^0.6.0", - "grunt-contrib-clean": "~2.0.0", - "grunt-contrib-concat": "1.0.1", + "grunt-contrib-clean": "~2.0.1", + "grunt-contrib-concat": "2.1.0", "grunt-contrib-copy": "~1.0.0", - "grunt-contrib-cssmin": "~4.0.0", + "grunt-contrib-cssmin": "~5.0.0", "grunt-contrib-imagemin": "~4.0.0", - "grunt-contrib-jshint": "3.0.0", - "grunt-contrib-qunit": "^4.0.0", - "grunt-contrib-uglify": "~5.0.1", + "grunt-contrib-jshint": "3.2.0", + "grunt-contrib-qunit": "~10.1.1", + "grunt-contrib-uglify": "~5.2.2", "grunt-contrib-watch": "~1.1.0", "grunt-file-append": "0.0.7", - "grunt-includes": "~1.1.0", "grunt-jsdoc": "2.4.1", - "grunt-jsvalidate": "~0.2.2", "grunt-legacy-util": "^2.0.1", - "grunt-patch-wordpress": "~3.0.0", - "grunt-postcss": "~0.9.0", + "grunt-patch-wordpress": "~4.0.0", "grunt-replace-lts": "~1.1.0", "grunt-rtlcss": "~2.0.2", - "grunt-sass": "~3.1.0", - "grunt-webpack": "^4.0.3", - "ink-docstrap": "1.3.2", + "grunt-sass": "~4.1.0", + "grunt-webpack": "7.0.1", "install-changed": "1.1.0", - "jest-image-snapshot": "3.0.1", - "matchdep": "~2.0.0", - "prettier": "npm:wp-prettier@2.0.5", - "qunit": "~2.16.0", - "sass": "^1.34.1", - "sinon": "~11.1.1", - "sinon-test": "~3.1.0", - "source-map-loader": "^1.1.3", - "uglify-js": "^3.13.9", - "uglifyjs-webpack-plugin": "2.2.0", - "uuid": "8.3.2", - "wait-on": "5.3.0", - "webpack": "4.43.0", - "webpack-dev-server": "3.11.2", - "webpack-livereload-plugin": "2.3.0" + "json2php": "0.0.12", + "php-array-reader": "2.1.3", + "postcss": "8.5.8", + "prettier": "npm:wp-prettier@3.0.3", + "qunit": "~2.25.0", + "react-refresh": "0.14.0", + "sass": "1.98.0", + "sinon": "16.1.3", + "sinon-test": "~3.1.6", + "source-map-loader": "5.0.0", + "terser-webpack-plugin": "5.4.0", + "typescript": "5.9.3", + "uuid": "13.0.0", + "wait-on": "9.0.4", + "webpack": "5.105.4" }, "dependencies": { - "@wordpress/a11y": "3.1.2", - "@wordpress/annotations": "2.1.6", - "@wordpress/api-fetch": "5.1.2", - "@wordpress/autop": "3.1.2", - "@wordpress/blob": "3.1.2", - "@wordpress/block-directory": "2.1.21", - "@wordpress/block-editor": "6.1.14", - "@wordpress/block-library": "3.2.19", - "@wordpress/block-serialization-default-parser": "4.1.2", - "@wordpress/blocks": "9.1.8", - "@wordpress/components": "14.1.11", - "@wordpress/compose": "4.1.6", - "@wordpress/core-data": "3.1.12", - "@wordpress/customize-widgets": "1.0.20", - "@wordpress/data": "5.1.6", - "@wordpress/data-controls": "2.1.6", - "@wordpress/date": "4.1.2", - "@wordpress/deprecated": "3.1.2", - "@wordpress/dom": "3.1.5", - "@wordpress/dom-ready": "3.1.2", - "@wordpress/edit-post": "4.1.21", - "@wordpress/edit-widgets": "2.1.21", - "@wordpress/editor": "10.1.17", - "@wordpress/element": "3.1.2", - "@wordpress/escape-html": "2.1.2", - "@wordpress/format-library": "2.1.14", - "@wordpress/hooks": "3.1.1", - "@wordpress/html-entities": "3.1.2", - "@wordpress/i18n": "4.1.2", - "@wordpress/icons": "4.0.3", - "@wordpress/interface": "3.1.12", - "@wordpress/is-shallow-equal": "4.1.1", - "@wordpress/keyboard-shortcuts": "2.1.7", - "@wordpress/keycodes": "3.1.2", - "@wordpress/list-reusable-blocks": "2.1.11", - "@wordpress/media-utils": "2.1.2", - "@wordpress/notices": "3.1.6", - "@wordpress/nux": "4.1.11", - "@wordpress/plugins": "3.1.6", - "@wordpress/primitives": "2.1.2", - "@wordpress/priority-queue": "2.1.2", - "@wordpress/redux-routine": "4.1.2", - "@wordpress/reusable-blocks": "2.1.17", - "@wordpress/rich-text": "4.1.6", - "@wordpress/server-side-render": "2.1.12", - "@wordpress/shortcode": "3.1.2", - "@wordpress/token-list": "2.1.1", - "@wordpress/url": "3.1.2", - "@wordpress/viewport": "3.1.6", - "@wordpress/warning": "2.1.2", - "@wordpress/widgets": "1.1.19", - "@wordpress/wordcount": "3.1.2", - "backbone": "1.4.0", - "clipboard": "2.0.8", + "backbone": "1.6.1", + "clipboard": "2.0.11", + "codemirror": "5.65.20", "core-js-url-browser": "3.6.4", - "element-closest": "^2.0.2", - "formdata-polyfill": "4.0.0", + "csslint": "1.0.5", + "element-closest": "3.0.2", + "espree": "9.6.1", + "esprima": "4.0.1", + "formdata-polyfill": "4.0.10", "hoverintent": "2.2.1", - "imagesloaded": "4.1.4", - "jquery": "3.6.0", - "jquery-color": "2.2.0", + "htmlhint": "1.8.0", + "imagesloaded": "5.0.0", + "jquery": "3.7.1", + "jquery-color": "3.0.0", "jquery-form": "4.3.0", - "jquery-hoverintent": "1.10.1", - "lodash": "4.17.21", + "jquery-hoverintent": "1.10.2", + "jsonlint": "1.6.3", + "lodash": "4.18.1", "masonry-layout": "4.2.2", - "moment": "2.29.1", + "moment": "2.30.1", "objectFitPolyfill": "2.3.5", - "polyfill-library": "3.105.0", - "react": "16.13.1", - "react-dom": "16.13.1", - "regenerator-runtime": "0.13.7", - "twemoji": "13.1.0", - "underscore": "1.13.1", - "whatwg-fetch": "3.6.2" + "polyfill-library": "4.8.0", + "react": "18.3.1", + "react-dom": "18.3.1", + "react-is": "18.3.1", + "regenerator-runtime": "0.14.1", + "underscore": "1.13.8", + "whatwg-fetch": "3.6.20", + "wicg-inert": "3.1.3" }, "scripts": { "build": "grunt build", "build:dev": "grunt build --dev", + "build:gutenberg": "grunt build:gutenberg", "dev": "grunt watch --dev", "test": "grunt test", "watch": "grunt watch", "grunt": "grunt", "lint:jsdoc": "wp-scripts lint-js", "lint:jsdoc:fix": "wp-scripts lint-js --fix", - "env:start": "node ./tools/local-env/scripts/start.js", + "typecheck:js": "tsc --build", + "env:start": "node ./tools/local-env/scripts/start.js && node ./tools/local-env/scripts/docker.js run -T --rm php composer update -W", "env:stop": "node ./tools/local-env/scripts/docker.js down", "env:restart": "npm run env:stop && npm run env:start", "env:clean": "node ./tools/local-env/scripts/docker.js down -v --remove-orphans", "env:reset": "node ./tools/local-env/scripts/docker.js down --rmi all -v --remove-orphans", "env:install": "node ./tools/local-env/scripts/install.js", - "env:cli": "node ./tools/local-env/scripts/docker.js run cli", + "env:cli": "node ./tools/local-env/scripts/docker.js exec --user wp_php cli wp", + "env:composer": "node ./tools/local-env/scripts/docker.js run -T --rm php composer", "env:logs": "node ./tools/local-env/scripts/docker.js logs", "env:pull": "node ./tools/local-env/scripts/docker.js pull", - "test:php": "node ./tools/local-env/scripts/docker.js run -T php composer update -W && node ./tools/local-env/scripts/docker.js run php ./vendor/bin/phpunit", - "test:e2e": "node ./tests/e2e/run-tests.js", - "test:visual": "node ./tests/visual-regression/run-tests.js", - "wp-packages-update": "wp-scripts packages-update" + "test:performance": "wp-scripts test-playwright --config tests/performance/playwright.config.js", + "test:php": "node ./tools/local-env/scripts/docker.js run --rm php ./vendor/bin/phpunit", + "test:coverage": "npm run test:php -- --coverage-html ./coverage/html/ --coverage-php ./coverage/php/report.php --coverage-text=./coverage/text/report.txt", + "test:e2e": "wp-scripts test-playwright --config tests/e2e/playwright.config.js", + "test:visual": "wp-scripts test-playwright --config tests/visual-regression/playwright.config.js", + "typecheck:php": "node ./tools/local-env/scripts/docker.js run --rm php composer phpstan", + "gutenberg:copy": "node tools/gutenberg/copy.js", + "gutenberg:verify": "node tools/gutenberg/utils.js", + "gutenberg:download": "node tools/gutenberg/download.js && grunt build:gutenberg --dev" } } diff --git a/phpcompat.xml.dist b/phpcompat.xml.dist index 5af8e2a5039ae..c4ebab64ea19b 100644 --- a/phpcompat.xml.dist +++ b/phpcompat.xml.dist @@ -2,16 +2,18 @@ Apply PHP compatibility checks to all WordPress Core files - - - - + - + + + ./src/ - - /node_modules/* + + /src/wp-content/mu-plugins/* - - /vendor/* + + /src/wp-content/plugins/* + + + /src/wp-content/themes/(?!twenty)* + + + /src/wp-content/languages/* src/wp-includes/sodium_compat/lib/php72compat_const\.php$ + + + + + + + + + + + /ID3/getid3\.php$ + + + /ID3/getid3\.php$ + /PHPMailer/PHPMailer\.php$ + + + /ID3/getid3\.php$ + + + /ID3/getid3\.php$ + + + /ID3/module.audio-video.quicktime\.php$ + + + /PHPMailer/PHPMailer\.php$ + + + /PHPMailer/PHPMailer\.php$ + /sodium_compat/src/Compat\.php$ @@ -55,26 +107,19 @@ /sodium_compat/src/Compat\.php$ + + /sodium_compat/src/PHP52/SplFixedArray\.php$ + + + /sodium_compat/src/PHP52/SplFixedArray\.php$ + - - /random_compat/byte_safe_strings\.php$ - - - /random_compat/random_bytes_mcrypt\.php$ - - - /random_compat/random_bytes_mcrypt\.php$ - - - /random_compat/random_bytes_mcrypt\.php$ + + /src/wp-includes/script-loader\.php$ - - - /src/wp-includes/wp-db\.php - diff --git a/phpcs.xml.dist b/phpcs.xml.dist index dedf053d632aa..f7d820d85559f 100644 --- a/phpcs.xml.dist +++ b/phpcs.xml.dist @@ -2,17 +2,24 @@ Apply WordPress Coding Standards to all Core files + + - + - - + @@ -23,82 +30,21 @@ - . - - - - - - - warning - - - warning - - - warning - - - warning - - - - - - - - - - - - - - - - - - - - - - - - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + . ^build/* + + /gutenberg/* + /node_modules/* /vendor/* @@ -109,6 +55,7 @@ /src/wp-admin/includes/ms-deprecated\.php /src/wp-includes/atomlib\.php + /src/wp-includes/class-avif-info\.php /src/wp-includes/class-IXR\.php /src/wp-includes/class-json\.php /src/wp-includes/class-phpass\.php @@ -116,20 +63,21 @@ /src/wp-includes/class-requests\.php /src/wp-includes/class-simplepie\.php /src/wp-includes/class-snoopy\.php - /src/wp-includes/class-wp-block-parser\.php /src/wp-includes/deprecated\.php /src/wp-includes/ms-deprecated\.php /src/wp-includes/pluggable-deprecated\.php /src/wp-includes/rss\.php /src/wp-includes/assets/* - /src/wp-includes/blocks/* + /src/wp-includes/blocks/*/*.asset.php + /src/wp-includes/blocks/blocks-json.php + /src/wp-includes/build/* /src/wp-includes/ID3/* /src/wp-includes/IXR/* /src/wp-includes/js/* /src/wp-includes/PHPMailer/* - /src/wp-includes/random_compat/* /src/wp-includes/Requests/* + /src/wp-includes/php-ai-client/* /src/wp-includes/SimplePie/* /src/wp-includes/sodium_compat/* /src/wp-includes/Text/* @@ -138,6 +86,9 @@ /tests/phpunit/build* /tests/phpunit/data/* + + /tests/phpstan/* + /tools/* @@ -163,110 +114,104 @@ /src/wp-content/themes/(?!twenty)* - - - /src/wp-includes/wp-db\.php - /tests/phpunit/tests/db/charset\.php - + + /src/wp-content/languages/* - - - /tests/phpunit/tests/db\.php - - - /tests/phpunit/tests/db\.php - - - /tests/phpunit/tests/db\.php - - - /tests/phpunit/tests/admin/includesSchema\.php - /tests/phpunit/tests/multisite/site\.php - - - - /src/wp-includes/l10n\.php - /tests/phpunit/tests/l10n\.php - /tests/phpunit/tests/l10n/loadTextdomainJustInTime\.php - + - - - /tests/* - + - - /wp-config\.php - /wp-config-sample\.php - /wp-tests-config\.php - /wp-tests-config-sample\.php - - - - /wp-config\.php - /wp-config-sample\.php - - + - - - /src/wp-includes/template\.php + + + warning + + /tests/phpunit/* - - - - /src/_index\.php - /src/wp-admin/_index\.php - /src/wp-content/themes/twentythirteen/taxonomy-post_format\.php - /src/wp-content/themes/twentyfourteen/taxonomy-post_format\.php + + warning + + + warning + + + warning + + + warning - - + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - + @@ -282,52 +227,113 @@ + - - - warning - - - warning - - - warning - - - + * - - - /tests/phpunit/tests/* + + + /wp-config\.php + /wp-config-sample\.php + - - /tests/phpunit/tests/* + + + /wp-config\.php + /wp-config-sample\.php + /wp-tests-config\.php + /wp-tests-config-sample\.php + + + + + /wp-includes/html-api/class-wp-html-processor\.php + /wp-includes/html-api/class-wp-html-doctype-info\.php + + + /wp-includes/compat-utf8\.php + /wp-includes/class-wp-block-processor\.php + + + + + /wp-tests-config-sample\.php - + /tests/phpunit/tests/* + /src/wp-admin/includes/class-wp-filesystem-ftpsockets\.php /src/wp-includes/class-wp-oembed\.php /src/wp-includes/class-wp-oembed-controller\.php /src/wp-includes/class-wp-xmlrpc-server\.php /src/wp-includes/class-wp-text-diff-renderer-inline\.php + + /src/wp-admin/includes/class-wp-list-table-compat\.php + /src/wp-includes/class-wp-dependency\.php /src/wp-includes/class-wp-editor\.php /src/wp-includes/class-wp-xmlrpc-server\.php - /src/wp-includes/wp-db\.php - /src/wp-includes/class-wp-dependency\.php + /src/wp-includes/class-wpdb\.php + + + + + /src/wp-includes/class-wpdb\.php + /tests/phpunit/tests/db/charset\.php + + + + + /tests/phpunit/tests/db\.php + + + /tests/phpunit/tests/db\.php + + + /tests/phpunit/tests/db\.php + + + /tests/phpunit/tests/admin/includesSchema\.php + /tests/phpunit/tests/multisite/site\.php + + + + + /tests/phpunit/* + + + + + /src/wp-includes/template\.php @@ -339,4 +345,26 @@ /src/wp-content/themes/twentyfourteen/inc/widgets\.php /src/wp-content/themes/twentyfourteen/inc/featured-content\.php + + + + /src/_index\.php + /src/wp-admin/_index\.php + /src/wp-content/themes/twentythirteen/taxonomy-post_format\.php + /src/wp-content/themes/twentyfourteen/taxonomy-post_format\.php + + + + + /src/wp-includes/l10n\.php + /tests/phpunit/tests/l10n\.php + /tests/phpunit/tests/l10n/loadTextdomainJustInTime\.php + + + + + /tests/* + + diff --git a/phpstan.neon.dist b/phpstan.neon.dist new file mode 100644 index 0000000000000..e74e6ec1a441b --- /dev/null +++ b/phpstan.neon.dist @@ -0,0 +1,36 @@ +# PHPStan configuration for WordPress Core. +# +# To overload this configuration, copy this file to phpstan.neon and adjust as needed. +# +# https://phpstan.org/config-reference + +includes: + # The base configuration file for using PHPStan with the WordPress core codebase. + - tests/phpstan/base.neon + + # The baseline file includes preexisting errors in the codebase that should be ignored. + # https://phpstan.org/user-guide/baseline + - tests/phpstan/baseline.php + +parameters: + # https://phpstan.org/user-guide/rule-levels + level: 0 + reportUnmatchedIgnoredErrors: true + + ignoreErrors: + # Level 0: + - # Inner functions aren't supported by PHPStan. + message: '#Function wxr_[a-z_]+ not found#' + path: src/wp-admin/includes/export.php + - + identifier: function.inner + path: src/wp-admin/includes/export.php + count: 13 + - + identifier: function.inner + path: src/wp-admin/includes/file.php + count: 1 + - + identifier: function.inner + path: src/wp-includes/canonical.php + count: 1 diff --git a/phpunit.xml.dist b/phpunit.xml.dist index 20267a5e9113d..4b6c149867c7d 100644 --- a/phpunit.xml.dist +++ b/phpunit.xml.dist @@ -6,6 +6,7 @@ colors="true" beStrictAboutTestsThatDoNotTestAnything="true" beStrictAboutOutputDuringTests="true" + failOnRisky="true" convertErrorsToExceptions="true" convertWarningsToExceptions="true" convertNoticesToExceptions="true" @@ -28,6 +29,7 @@ ms-files ms-required external-http + html-api-html5lib-tests @@ -43,9 +45,9 @@ src/wp-includes/ID3 src/wp-includes/IXR - src/wp-includes/random_compat src/wp-includes/PHPMailer src/wp-includes/Requests + src/wp-includes/php-ai-client src/wp-includes/SimplePie src/wp-includes/sodium_compat src/wp-includes/Text diff --git a/src/index.php b/src/index.php index bb9757a487f7e..544acab805b09 100644 --- a/src/index.php +++ b/src/index.php @@ -11,22 +11,29 @@ define( 'ABSPATH', __DIR__ . '/' ); } -if ( file_exists( ABSPATH . 'wp-includes/js/dist/edit-post.js' ) ) { +/* + * Load the actual index.php file if the assets were already built. + * Note: WPINC is not defined yet, it is defined later in wp-settings.php. + */ +if ( file_exists( ABSPATH . 'wp-includes/js/jquery/jquery.js' ) && is_dir( ABSPATH . 'wp-includes/build' ) ) { require_once ABSPATH . '_index.php'; return; } define( 'WPINC', 'wp-includes' ); +require_once ABSPATH . WPINC . '/version.php'; +require_once ABSPATH . WPINC . '/compat.php'; require_once ABSPATH . WPINC . '/load.php'; +// Check for the required PHP version and for the MySQL extension or a database drop-in. +wp_check_php_mysql_versions(); + // Standardize $_SERVER variables across setups. wp_fix_server_vars(); -require_once ABSPATH . WPINC . '/functions.php'; define( 'WP_CONTENT_DIR', ABSPATH . 'wp-content' ); -require_once ABSPATH . WPINC . '/version.php'; +require_once ABSPATH . WPINC . '/functions.php'; -wp_check_php_mysql_versions(); wp_load_translations_early(); // Die with an error message. @@ -51,8 +58,8 @@ $die .= ''; $die .= '

' . sprintf( - /* translators: 1: NPM URL, 2: Handbook URL. */ - __( 'This requires NPM. Learn more about setting up your local development environment.' ), + /* translators: 1: npm URL, 2: Handbook URL. */ + __( 'This requires npm. Learn more about setting up your local development environment.' ), 'https://www.npmjs.com/get-npm', __( 'https://make.wordpress.org/core/handbook/tutorials/installing-wordpress-locally/' ) ) . '

'; diff --git a/src/js/_enqueues/admin/application-passwords.js b/src/js/_enqueues/admin/application-passwords.js index ae9fe43b0c1a5..c79cdb8b4037f 100644 --- a/src/js/_enqueues/admin/application-passwords.js +++ b/src/js/_enqueues/admin/application-passwords.js @@ -60,7 +60,7 @@ name: response.name, password: response.password } ) ); - $( '.new-application-password-notice' ).trigger( 'focus' ); + $( '.new-application-password-notice' ).attr( 'tabindex', '-1' ).trigger( 'focus' ); $appPassTbody.prepend( tmplAppPassRow( response ) ); diff --git a/src/js/_enqueues/admin/comment.js b/src/js/_enqueues/admin/comment.js index 4e4f3c5ed98d4..188caab2c30c5 100644 --- a/src/js/_enqueues/admin/comment.js +++ b/src/js/_enqueues/admin/comment.js @@ -70,7 +70,7 @@ jQuery( function($) { * @param {Event} event The event object. * @return {void} */ - $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse - multiple OK cancels. + $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. var aa = $('#aa').val(), mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val(), newD = new Date( aa, mm - 1, jj, hh, mn ); diff --git a/src/js/_enqueues/admin/common.js b/src/js/_enqueues/admin/common.js index d5e2d9ad5841e..2a7daba0d2dc4 100644 --- a/src/js/_enqueues/admin/common.js +++ b/src/js/_enqueues/admin/common.js @@ -353,6 +353,21 @@ window.setPostThumbnailL10n = window.setPostThumbnailL10n || { window.setPostThumbnailL10n = deprecateL10nObject( 'setPostThumbnailL10n', window.setPostThumbnailL10n, '5.5.0' ); +/** + * Removed in 6.5.0, needed for back-compatibility. + * + * @since 4.5.0 + * @deprecated 6.5.0 + */ +window.uiAutocompleteL10n = window.uiAutocompleteL10n || { + noResults: '', + oneResult: '', + manyResults: '', + itemSelected: '' +}; + +window.uiAutocompleteL10n = deprecateL10nObject( 'uiAutocompleteL10n', window.uiAutocompleteL10n, '6.5.0' ); + /** * Removed in 3.3.0, needed for back-compatibility. * @@ -404,12 +419,18 @@ window.columns = { */ saveManageColumnsState : function() { var hidden = this.hidden(); - $.post(ajaxurl, { - action: 'hidden-columns', - hidden: hidden, - screenoptionnonce: $('#screenoptionnonce').val(), - page: pagenow - }); + $.post( + ajaxurl, + { + action: 'hidden-columns', + hidden: hidden, + screenoptionnonce: $('#screenoptionnonce').val(), + page: pagenow + }, + function() { + wp.a11y.speak( __( 'Screen Options updated.' ) ); + } + ); }, /** @@ -617,7 +638,7 @@ window.screenMeta = { * @return {void} */ panel.slideDown( 'fast', function() { - panel.trigger( 'focus' ); + panel.removeClass( 'hidden' ).trigger( 'focus' ); button.addClass( 'screen-meta-active' ).attr( 'aria-expanded', true ); }); @@ -646,6 +667,7 @@ window.screenMeta = { button.removeClass( 'screen-meta-active' ).attr( 'aria-expanded', false ); $('.screen-meta-toggle').css('visibility', ''); panel.parent().hide(); + panel.addClass( 'hidden' ); }); $document.trigger( 'screen:options:close' ); @@ -751,9 +773,15 @@ $availableStructureTags.on( 'click', function() { selectionStart = $permalinkStructure[ 0 ].selectionStart, selectionEnd = $permalinkStructure[ 0 ].selectionEnd, textToAppend = $( this ).text().trim(), - textToAnnounce = $( this ).attr( 'data-added' ), + textToAnnounce, newSelectionStart; + if ( $( this ).hasClass( 'active' ) ) { + textToAnnounce = $( this ).attr( 'data-removed' ); + } else { + textToAnnounce = $( this ).attr( 'data-added' ); + } + // Remove structure tag if already part of the structure. if ( -1 !== permalinkStructureValue.indexOf( textToAppend ) ) { permalinkStructureValue = permalinkStructureValue.replace( textToAppend + '/', '' ); @@ -880,27 +908,6 @@ $( function() { $document.trigger( 'wp-collapse-menu', { state: menuState } ); }); - /** - * Handles the `aria-haspopup` attribute on the current menu item when it has a submenu. - * - * @since 4.4.0 - * - * @return {void} - */ - function currentMenuItemHasPopup() { - var $current = $( 'a.wp-has-current-submenu' ); - - if ( 'folded' === menuState ) { - // When folded or auto-folded and not responsive view, the current menu item does have a fly-out sub-menu. - $current.attr( 'aria-haspopup', 'true' ); - } else { - // When expanded or in responsive view, reset aria-haspopup. - $current.attr( 'aria-haspopup', 'false' ); - } - } - - $document.on( 'wp-menu-state-set wp-collapse-menu wp-responsive-activate wp-responsive-deactivate', currentMenuItemHasPopup ); - /** * Ensures an admin submenu is within the visual viewport. * @@ -931,7 +938,7 @@ $( function() { adjustment = maxtop; } - if ( adjustment > 1 ) { + if ( adjustment > 1 && $('#wp-admin-bar-menu-toggle').is(':hidden') ) { $submenu.css( 'margin-top', '-' + adjustment + 'px' ); } else { $submenu.css( 'margin-top', '' ); @@ -1113,7 +1120,7 @@ $( function() { }); } - $document.on( 'wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error', makeNoticesDismissible ); + $document.on( 'wp-updates-notice-added wp-plugin-install-error wp-plugin-update-error wp-plugin-delete-error wp-theme-install-error wp-theme-delete-error wp-notice-added', makeNoticesDismissible ); // Init screen meta. screenMeta.init(); @@ -1147,7 +1154,7 @@ $( function() { lastClicked = this; // Toggle the "Select all" checkboxes depending if the other ones are all checked or not. - var unchecked = $(this).closest('tbody').find(':checkbox').filter(':visible:enabled').not(':checked'); + var unchecked = $(this).closest('tbody').find('tr').find(':checkbox').filter(':visible:enabled').not(':checked'); /** * Determines if all checkboxes are checked. @@ -1275,6 +1282,87 @@ $( function() { // Marry the secondary "Change role to" controls to the primary controls: marryControls( $('#new_role'), $('#changeit'), $('#new_role2'), $('#changeit2') ); + var addAdminNotice = function( data ) { + var $notice = $( data.selector ), + $headerEnd = $( '.wp-header-end' ), + type, + dismissible, + $adminNotice; + + delete data.selector; + + dismissible = ( data.dismissible && data.dismissible === true ) ? ' is-dismissible' : ''; + type = ( data.type ) ? data.type : 'info'; + + $adminNotice = '

' + data.message + '

'; + + // Check if this admin notice already exists. + if ( ! $notice.length ) { + $notice = $( '#' + data.id ); + } + + if ( $notice.length ) { + $notice.replaceWith( $adminNotice ); + } else if ( $headerEnd.length ) { + $headerEnd.after( $adminNotice ); + } else { + if ( 'customize' === pagenow ) { + $( '.customize-themes-notifications' ).append( $adminNotice ); + } else { + $( '.wrap' ).find( '> h1' ).after( $adminNotice ); + } + } + + $document.trigger( 'wp-notice-added' ); + }; + + $( '.bulkactions' ).parents( 'form' ).on( 'submit', function( event ) { + var form = this, + submitterName = event.originalEvent && event.originalEvent.submitter ? event.originalEvent.submitter.name : false, + currentPageSelector = form.querySelector( '#current-page-selector' ); + + if ( currentPageSelector && currentPageSelector.defaultValue !== currentPageSelector.value ) { + return; // Pagination form submission. + } + + // Observe submissions from posts lists for 'bulk_action' or users lists for 'new_role'. + var bulkFieldRelations = { + 'bulk_action' : window.bulkActionObserverIds.bulk_action, + 'changeit' : window.bulkActionObserverIds.changeit + }; + if ( ! Object.keys( bulkFieldRelations ).includes( submitterName ) ) { + return; + } + + var values = new FormData(form); + var value = values.get( bulkFieldRelations[ submitterName ] ) || '-1'; + + // Check that the action is not the default one. + if ( value !== '-1' ) { + // Check that at least one item is selected. + var itemsSelected = form.querySelectorAll( '.wp-list-table tbody .check-column input[type="checkbox"]:checked' ); + + if ( itemsSelected.length > 0 ) { + return; + } + } + event.preventDefault(); + event.stopPropagation(); + $( 'html, body' ).animate( { scrollTop: 0 } ); + + var errorMessage = value !== '-1' ? + __( 'Please select at least one item to perform this action on.' ) : + __( 'Please select a bulk action to perform.' ); + addAdminNotice( { + id: value !== '-1' ? 'no-items-selected' : 'no-bulk-action-selected', + type: 'error', + message: errorMessage, + dismissible: true, + } ); + + wp.a11y.speak( errorMessage ); + }); + /** * Shows row actions on focus of its parent container element or any other elements contained within. * @@ -1309,7 +1397,7 @@ $( function() { }); /** - * Handles tab keypresses in theme and plugin editor textareas. + * Handles tab keypresses in theme and plugin file editor textareas. * * @param {Event} e The event object. * @@ -1400,8 +1488,8 @@ $( function() { * @return {void} */ $('#contextual-help-link, #show-settings-link').on( 'focus.scroll-into-view', function(e){ - if ( e.target.scrollIntoView ) - e.target.scrollIntoView(false); + if ( e.target.scrollIntoViewIfNeeded ) + e.target.scrollIntoViewIfNeeded(false); }); /** @@ -1673,8 +1761,10 @@ $( function() { // Modify functionality based on custom activate/deactivate event. $document.on( 'wp-responsive-activate.wp-responsive', function() { self.activate(); + self.toggleAriaHasPopup( 'add' ); }).on( 'wp-responsive-deactivate.wp-responsive', function() { self.deactivate(); + self.toggleAriaHasPopup( 'remove' ); }); $( '#wp-admin-bar-menu-toggle a' ).attr( 'aria-expanded', 'false' ); @@ -1695,39 +1785,62 @@ $( function() { } } ); - // Close sidebar when focus moves outside of toggle and sidebar. - $( '#wp-admin-bar-menu-toggle, #adminmenumain' ).on( 'focusout', function() { - var focusIsInToggle, focusIsInSidebar; - - if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) ) { + // Close sidebar when target moves outside of toggle and sidebar. + $( document ).on( 'click', function( event ) { + if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) || ! document.hasFocus() ) { return; } - // A brief delay is required to allow focus to switch to another element. - setTimeout( function() { - focusIsInToggle = $.contains( $( '#wp-admin-bar-menu-toggle' )[0], $( ':focus' )[0] ); - focusIsInSidebar = $.contains( $( '#adminmenumain' )[0], $( ':focus' )[0] ); + var focusIsInToggle = $.contains( $( '#wp-admin-bar-menu-toggle' )[0], event.target ); + var focusIsInSidebar = $.contains( $( '#adminmenuwrap' )[0], event.target ); - if ( ! focusIsInToggle && ! focusIsInSidebar ) { - $( '#wp-admin-bar-menu-toggle' ).trigger( 'click.wp-responsive' ); - } - }, 10 ); + if ( ! focusIsInToggle && ! focusIsInSidebar ) { + $( '#wp-admin-bar-menu-toggle' ).trigger( 'click.wp-responsive' ); + } } ); + // Close sidebar when a keypress completes outside of toggle and sidebar. + $( document ).on( 'keyup', function( event ) { + var toggleButton = $( '#wp-admin-bar-menu-toggle' )[0]; + if ( ! $wpwrap.hasClass( 'wp-responsive-open' ) ) { + return; + } + if ( 27 === event.keyCode ) { + $( toggleButton ).trigger( 'click.wp-responsive' ); + $( toggleButton ).find( 'a' ).trigger( 'focus' ); + } else { + if ( 9 === event.keyCode ) { + var sidebar = $( '#adminmenuwrap' )[0]; + var focusedElement = event.relatedTarget || document.activeElement; + // A brief delay is required to allow focus to switch to another element. + setTimeout( function() { + var focusIsInToggle = $.contains( toggleButton, focusedElement ); + var focusIsInSidebar = $.contains( sidebar, focusedElement ); + + if ( ! focusIsInToggle && ! focusIsInSidebar ) { + $( toggleButton ).trigger( 'click.wp-responsive' ); + } + }, 10 ); + } + } + }); + // Add menu events. $adminmenu.on( 'click.wp-responsive', 'li.wp-has-submenu > a', function( event ) { if ( ! $adminmenu.data('wp-responsive') ) { return; } - + let state = ( 'false' === $( this ).attr( 'aria-expanded' ) ) ? 'true' : 'false'; $( this ).parent( 'li' ).toggleClass( 'selected' ); + $( this ).attr( 'aria-expanded', state ); + $( this ).trigger( 'focus' ); event.preventDefault(); }); self.trigger(); $document.on( 'wp-window-resized.wp-responsive', this.trigger.bind( this ) ); - // This needs to run later as UI Sortable may be initialized later on $(document).ready(). + // This needs to run later as UI Sortable may be initialized when the document is ready. $window.on( 'load.wp-responsive', this.maybeDisableSortables ); $document.on( 'postbox-toggled', this.maybeDisableSortables ); @@ -1793,6 +1906,34 @@ $( function() { this.maybeDisableSortables(); }, + /** + * Toggles the aria-haspopup attribute for the responsive admin menu. + * + * The aria-haspopup attribute is only necessary for the responsive menu. + * See ticket https://core.trac.wordpress.org/ticket/43095 + * + * @since 6.6.0 + * + * @param {string} action Whether to add or remove the aria-haspopup attribute. + * + * @return {void} + */ + toggleAriaHasPopup: function( action ) { + var elements = $adminmenu.find( '[data-ariahaspopup]' ); + + if ( action === 'add' ) { + elements.each( function() { + $( this ).attr( 'aria-haspopup', 'menu' ).attr( 'aria-expanded', 'false' ); + } ); + + return; + } + + elements.each( function() { + $( this ).removeAttr( 'aria-haspopup' ).removeAttr( 'aria-expanded' ); + } ); + }, + /** * Sets the responsiveness and enables the overlay based on the viewport width. * @@ -1990,7 +2131,6 @@ $( function() { window.wpResponsive.init(); setPinMenu(); setMenuState(); - currentMenuItemHasPopup(); makeNoticesDismissible(); aria_button_if_js(); @@ -2096,3 +2236,123 @@ $( function( $ ) { })(); }( jQuery, window )); + +/** + * Freeze animated plugin icons when reduced motion is enabled. + * + * When the user has enabled the 'prefers-reduced-motion' setting, this module + * stops animations for all GIFs on the page with the class 'plugin-icon' or + * plugin icon images in the update plugins table. + * + * @since 6.4.0 + */ +(function() { + // Private variables and methods. + var priv = {}, + pub = {}, + mediaQuery; + + // Initialize pauseAll to false; it will be set to true if reduced motion is preferred. + priv.pauseAll = false; + if ( window.matchMedia ) { + mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); + if ( ! mediaQuery || mediaQuery.matches ) { + priv.pauseAll = true; + } + } + + // Method to replace animated GIFs with a static frame. + priv.freezeAnimatedPluginIcons = function( img ) { + var coverImage = function() { + var width = img.width; + var height = img.height; + var canvas = document.createElement( 'canvas' ); + + // Set canvas dimensions. + canvas.width = width; + canvas.height = height; + + // Copy classes from the image to the canvas. + canvas.className = img.className; + + // Check if the image is inside a specific table. + var isInsideUpdateTable = img.closest( '#update-plugins-table' ); + + if ( isInsideUpdateTable ) { + // Transfer computed styles from image to canvas. + var computedStyles = window.getComputedStyle( img ), + i, max; + for ( i = 0, max = computedStyles.length; i < max; i++ ) { + var propName = computedStyles[ i ]; + var propValue = computedStyles.getPropertyValue( propName ); + canvas.style[ propName ] = propValue; + } + } + + // Draw the image onto the canvas. + canvas.getContext( '2d' ).drawImage( img, 0, 0, width, height ); + + // Set accessibility attributes on canvas. + canvas.setAttribute( 'aria-hidden', 'true' ); + canvas.setAttribute( 'role', 'presentation' ); + + // Insert canvas before the image and set the image to be near-invisible. + var parent = img.parentNode; + parent.insertBefore( canvas, img ); + img.style.opacity = 0.01; + img.style.width = '0px'; + img.style.height = '0px'; + }; + + // If the image is already loaded, apply the coverImage function. + if ( img.complete ) { + coverImage(); + } else { + // Otherwise, wait for the image to load. + img.addEventListener( 'load', coverImage, true ); + } + }; + + // Public method to freeze all relevant GIFs on the page. + pub.freezeAll = function() { + var images = document.querySelectorAll( '.plugin-icon, #update-plugins-table img' ); + for ( var x = 0; x < images.length; x++ ) { + if ( /\.gif(?:\?|$)/i.test( images[ x ].src ) ) { + priv.freezeAnimatedPluginIcons( images[ x ] ); + } + } + }; + + // Only run the freezeAll method if the user prefers reduced motion. + if ( true === priv.pauseAll ) { + pub.freezeAll(); + } + + // Listen for jQuery AJAX events. + ( function( $ ) { + if ( window.pagenow === 'plugin-install' ) { + // Only listen for ajaxComplete if this is the plugin-install.php page. + $( document ).ajaxComplete( function( event, xhr, settings ) { + + // Check if this is the 'search-install-plugins' request. + if ( settings.data && typeof settings.data === 'string' && settings.data.includes( 'action=search-install-plugins' ) ) { + // Recheck if the user prefers reduced motion. + if ( window.matchMedia ) { + var mediaQuery = window.matchMedia( '(prefers-reduced-motion: reduce)' ); + if ( mediaQuery.matches ) { + pub.freezeAll(); + } + } else { + // Fallback for browsers that don't support matchMedia. + if ( true === priv.pauseAll ) { + pub.freezeAll(); + } + } + } + } ); + } + } )( jQuery ); + + // Expose public methods. + return pub; +})(); diff --git a/src/js/_enqueues/admin/edit-comments.js b/src/js/_enqueues/admin/edit-comments.js index 96ca1f9f48993..78ee78de759ff 100644 --- a/src/js/_enqueues/admin/edit-comments.js +++ b/src/js/_enqueues/admin/edit-comments.js @@ -14,7 +14,7 @@ var getCount, updateCount, updateCountText, updatePending, updateApproved, updateHtmlTitle, updateDashboardText, updateInModerationText, adminTitle = document.title, isDashboard = $('#dashboard_right_now').length, titleDiv, titleRegEx, - __ = wp.i18n.__; + __ = wp.i18n.__, _x = wp.i18n._x; /** * Extracts a number from the content of a jQuery element. @@ -370,7 +370,8 @@ window.setCommentsList = function() { } else { if ( settings.data.id == replyID ) - replyButton.text( __( 'Reply' ) ); + /* translators: Comment reply button text. */ + replyButton.text( _x( 'Reply', 'verb' ) ); c.find( '.row-actions span.view' ).removeClass( 'hidden' ).end() .find( 'div.comment_status' ).html( '1' ); @@ -1012,14 +1013,17 @@ window.commentReply = { if ( c.hasClass('unapproved') ) { replyButton.text( __( 'Approve and Reply' ) ); } else { - replyButton.text( __( 'Reply' ) ); + /* translators: Comment reply button text. */ + replyButton.text( _x( 'Reply', 'verb' ) ); } $('#replyrow').fadeIn(300, function(){ $(this).show(); }); } setTimeout(function() { - var rtop, rbottom, scrollTop, vp, scrollBottom; + var rtop, rbottom, scrollTop, vp, scrollBottom, + isComposing = false, + isContextMenuOpen = false; rtop = $('#replyrow').offset().top; rbottom = rtop + $('#replyrow').height(); @@ -1032,10 +1036,28 @@ window.commentReply = { else if ( rtop - 20 < scrollTop ) window.scroll(0, rtop - 35); - $('#replycontent').trigger( 'focus' ).on( 'keyup', function(e){ - if ( e.which == 27 ) - commentReply.revert(); // Close on Escape. - }); + $( '#replycontent' ) + .trigger( 'focus' ) + .on( 'contextmenu keydown', function ( e ) { + // Check if the context menu is open and set state. + if ( e.type === 'contextmenu' ) { + isContextMenuOpen = true; + } + + // Update the context menu state if the Escape key is pressed. + if ( e.type === 'keydown' && e.which === 27 && isContextMenuOpen ) { + isContextMenuOpen = false; + } + } ) + .on( 'keyup', function( e ) { + // Close on Escape unless Input Method Editors (IMEs) are in use or the context menu is open. + if ( e.which === 27 && ! isComposing && ! isContextMenuOpen ) { + commentReply.revert(); + } + } ) + .on( 'compositionstart', function() { + isComposing = true; + } ); }, 600); return false; @@ -1180,6 +1202,7 @@ window.commentReply = { if ( er ) { $errorNotice.removeClass( 'hidden' ); $error.html( er ); + wp.a11y.speak( er ); } }, @@ -1217,7 +1240,7 @@ window.commentReply = { discardCommentChanges: function() { var editRow = $( '#replyrow' ); - if ( this.originalContent === $( '#replycontent', editRow ).val() ) { + if ( '' === $( '#replycontent', editRow ).val() || this.originalContent === $( '#replycontent', editRow ).val() ) { return true; } diff --git a/src/js/_enqueues/admin/inline-edit-post.js b/src/js/_enqueues/admin/inline-edit-post.js index 4d297e0c1afc9..6e9f4e9f20503 100644 --- a/src/js/_enqueues/admin/inline-edit-post.js +++ b/src/js/_enqueues/admin/inline-edit-post.js @@ -128,10 +128,18 @@ window.wp = window.wp || {}; inlineEditPost.edit( this ); }); + // Clone quick edit categories for the bulk editor. + var beCategories = $( '#inline-edit fieldset.inline-edit-categories' ).clone(); + + // Make "id" attributes globally unique. + beCategories.find( '*[id]' ).each( function() { + this.id = 'bulk-edit-' + this.id; + }); + $('#bulk-edit').find('fieldset:first').after( - $('#inline-edit fieldset.inline-edit-categories').clone() + beCategories ).siblings( 'fieldset:last' ).prepend( - $('#inline-edit label.inline-edit-tags').clone() + $( '#inline-edit .inline-edit-tags-wrap' ).clone() ); $('select[name="_status"] option[value="future"]', bulkRow).remove(); @@ -140,7 +148,12 @@ window.wp = window.wp || {}; * Adds onclick events to the apply buttons. */ $('#doaction').on( 'click', function(e){ - var n; + var n, + $itemsSelected = $( '#posts-filter .check-column input[type="checkbox"]:checked' ); + + if ( $itemsSelected.length < 1 ) { + return; + } t.whichBulkButtonId = $( this ).attr( 'id' ); n = t.whichBulkButtonId.substr( 2 ); @@ -178,6 +191,8 @@ window.wp = window.wp || {}; */ setBulk : function(){ var te = '', type = this.type, c = true; + var checkedPosts = $( 'tbody th.check-column input[type="checkbox"]:checked' ); + var categories = {}; this.revert(); $( '#bulk-edit td' ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length ); @@ -197,9 +212,15 @@ window.wp = window.wp || {}; // If the checkbox for a post is selected, add the post to the edit list. if ( $(this).prop('checked') ) { c = false; - var id = $(this).val(), theTitle; - theTitle = $('#inline_'+id+' .post_title').html() || wp.i18n.__( '(no title)' ); - te += '
X'+theTitle+'
'; + var id = $( this ).val(), + theTitle = $( '#inline_' + id + ' .post_title' ).html() || wp.i18n.__( '(no title)' ), + buttonVisuallyHiddenText = wp.i18n.sprintf( + /* translators: %s: Post title. */ + wp.i18n.__( 'Remove “%s” from Bulk Edit' ), + theTitle + ); + + te += '
  • '; } }); @@ -208,18 +229,73 @@ window.wp = window.wp || {}; return this.revert(); } - // Add onclick events to the delete-icons in the bulk editors the post title list. - $('#bulk-titles').html(te); + // Populate the list of items to bulk edit. + $( '#bulk-titles' ).html( '
      ' + te + '
    ' ); + + // Gather up some statistics on which of these checked posts are in which categories. + checkedPosts.each( function() { + var id = $( this ).val(); + var checked = $( '#category_' + id ).text().split( ',' ); + + checked.map( function( cid ) { + categories[ cid ] || ( categories[ cid ] = 0 ); + // Just record that this category is checked. + categories[ cid ]++; + } ); + } ); + + // Compute initial states. + $( '.inline-edit-categories input[name="post_category[]"]' ).each( function() { + if ( categories[ $( this ).val() ] == checkedPosts.length ) { + // If the number of checked categories matches the number of selected posts, then all posts are in this category. + $( this ).prop( 'checked', true ); + } else if ( categories[ $( this ).val() ] > 0 ) { + // If the number is less than the number of selected posts, then it's indeterminate. + $( this ).prop( 'indeterminate', true ); + if ( ! $( this ).parent().find( 'input[name="indeterminate_post_category[]"]' ).length ) { + // Get the term label text. + var label = $( this ).parent().text(); + // Set indeterminate states for the backend. Add accessible text for indeterminate inputs. + $( this ).after( '' ).attr( 'aria-label', label.trim() + ': ' + wp.i18n.__( 'Some selected posts have this category' ) ); + } + } + } ); + + $( '.inline-edit-categories input[name="post_category[]"]:indeterminate' ).on( 'change', function() { + // Remove accessible label text. Remove the indeterminate flags as there was a specific state change. + $( this ).removeAttr( 'aria-label' ).parent().find( 'input[name="indeterminate_post_category[]"]' ).remove(); + } ); + + $( '.inline-edit-save button' ).on( 'click', function() { + $( '.inline-edit-categories input[name="post_category[]"]' ).prop( 'indeterminate', false ); + } ); + /** - * Binds on click events to the checkboxes before the posts in the table. + * Binds on click events to handle the list of items to bulk edit. * * @listens click */ - $('#bulk-titles a').on( 'click', function(){ - var id = $(this).attr('id').substr(1); - - $('table.widefat input[value="' + id + '"]').prop('checked', false); - $('#ttle'+id).remove(); + $( '#bulk-titles .ntdelbutton' ).click( function() { + var $this = $( this ), + id = $this.attr( 'id' ).substr( 1 ), + $prev = $this.parent().prev().children( '.ntdelbutton' ), + $next = $this.parent().next().children( '.ntdelbutton' ); + + $( 'input#cb-select-all-1, input#cb-select-all-2' ).prop( 'checked', false ); + $( 'table.widefat input[value="' + id + '"]' ).prop( 'checked', false ); + $( '#_' + id ).parent().remove(); + wp.a11y.speak( wp.i18n.__( 'Item removed.' ), 'assertive' ); + + // Move focus to a proper place when items are removed. + if ( $next.length ) { + $next.focus(); + } else if ( $prev.length ) { + $prev.focus(); + } else { + $( '#bulk-titles-list' ).remove(); + inlineEditPost.revert(); + wp.a11y.speak( wp.i18n.__( 'All selected items have been removed. Select new items to use Bulk Actions.' ) ); + } }); // Enable auto-complete for tags when editing posts. @@ -238,6 +314,8 @@ window.wp = window.wp || {}; } ); } + // Set initial focus on the Bulk Edit region. + $( '#bulk-edit .inline-edit-wrapper' ).attr( 'tabindex', '-1' ).focus(); // Scrolls to the top of the table where the editor is rendered. $('html, body').animate( { scrollTop: 0 }, 'fast' ); }, @@ -270,6 +348,10 @@ window.wp = window.wp || {}; editRow = $('#inline-edit').clone(true); $( 'td', editRow ).attr( 'colspan', $( 'th:visible, td:visible', '.widefat:first thead' ).length ); + // Remove the ID from the copied row and let the `for` attribute reference the hidden ID. + $( 'td', editRow ).find('#quick-edit-legend').removeAttr('id'); + $( 'td', editRow ).find('p[id^="quick-edit-"]').removeAttr('id'); + $(t.what+id).removeClass('is-expanded').hide().after(editRow).after(''); // Populate fields in the quick edit window. @@ -277,7 +359,7 @@ window.wp = window.wp || {}; if ( !$(':input[name="post_author"] option[value="' + $('.post_author', rowData).text() + '"]', editRow).val() ) { // The post author no longer has edit capabilities, so we need to add them to the list of authors. - $(':input[name="post_author"]', editRow).prepend(''); + $(':input[name="post_author"]', editRow).prepend(''); } if ( $( ':input[name="post_author"] option', editRow ).length === 1 ) { $('label.inline-edit-author', editRow).hide(); @@ -348,9 +430,14 @@ window.wp = window.wp || {}; }); // Handle the post status. + var post_date_string = $(':input[name="aa"]').val() + '-' + $(':input[name="mm"]').val() + '-' + $(':input[name="jj"]').val(); + post_date_string += ' ' + $(':input[name="hh"]').val() + ':' + $(':input[name="mn"]').val() + ':' + $(':input[name="ss"]').val(); + var post_date = new Date( post_date_string ); status = $('._status', rowData).text(); - if ( 'future' !== status ) { + if ( 'future' !== status && Date.now() > post_date ) { $('select[name="_status"] option[value="future"]', editRow).remove(); + } else { + $('select[name="_status"] option[value="publish"]', editRow).remove(); } pw = $( '.inline-edit-password-input' ).prop( 'disabled', false ); @@ -521,18 +608,19 @@ $( function() { inlineEditPost.init(); } ); // Show/hide locks on posts. $( function() { - // Set the heartbeat interval to 15 seconds. + // Set the heartbeat interval to 10 seconds. if ( typeof wp !== 'undefined' && wp.heartbeat ) { - wp.heartbeat.interval( 15 ); + wp.heartbeat.interval( 10 ); } }).on( 'heartbeat-tick.wp-check-locked-posts', function( e, data ) { - var locked = data['wp-check-locked-posts'] || {}; + var locked = data['wp-check-locked-posts'] || {}, + lockedClass = 'wp-locked'; $('#the-list tr').each( function(i, el) { var key = el.id, row = $(el), lock_data, avatar; if ( locked.hasOwnProperty( key ) ) { - if ( ! row.hasClass('wp-locked') ) { + if ( ! row.hasClass( lockedClass ) ) { lock_data = locked[key]; row.find('.column-title .locked-text').text( lock_data.text ); row.find('.check-column checkbox').prop('checked', false); @@ -548,10 +636,10 @@ $( function() { } ); row.find('.column-title .locked-avatar').empty().append( avatar ); } - row.addClass('wp-locked'); + row.addClass( lockedClass ); } - } else if ( row.hasClass('wp-locked') ) { - row.removeClass( 'wp-locked' ).find( '.locked-info span' ).empty(); + } else if ( row.hasClass( lockedClass ) ) { + row.removeClass( lockedClass ).find( '.locked-info span' ).empty(); } }); }).on( 'heartbeat-send.wp-check-locked-posts', function( e, data ) { diff --git a/src/js/_enqueues/admin/link.js b/src/js/_enqueues/admin/link.js index 1456ba9530ede..ed4bd5e3efb94 100644 --- a/src/js/_enqueues/admin/link.js +++ b/src/js/_enqueues/admin/link.js @@ -19,16 +19,37 @@ jQuery( function($) { * * @return {boolean} Always returns false to prevent the default behavior. */ - $('#category-tabs a').on( 'click', function(){ + $('#category-tabs a').on( 'click keyup keydown', function( event ){ var t = $(this).attr('href'); - $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); - $('.tabs-panel').hide(); - $(t).show(); - if ( '#categories-all' == t ) - deleteUserSetting('cats'); - else - setUserSetting('cats','pop'); - return false; + if ( event.type === 'keydown' && event.key === ' ' ) { + event.preventDefault(); + } + if ( ( event.type === 'keyup' && event.key === ' ' ) || ( event.type === 'keydown' && event.key === 'Enter' ) || event.type === 'click' ) { + event.preventDefault(); + $('#category-tabs a').removeAttr( 'aria-selected' ).attr( 'tabindex', '-1' ); + $(this).attr( 'aria-selected', 'true' ).removeAttr( 'tabindex' ); + $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); + $('.tabs-panel').hide(); + $(t).show(); + if ( '#categories-all' == t ) { + deleteUserSetting('cats'); + } else { + setUserSetting('cats','pop'); + } + return false; + } + if ( event.type === 'keyup' && ( event.key === 'ArrowRight' || event.key === 'ArrowLeft' ) ) { + $(this).attr( 'tabindex', '-1' ); + let next = $(this).parent('li').next(); + let prev = $(this).parent('li').prev(); + if ( next.length > 0 ) { + next.find('a').removeAttr( 'tabindex'); + next.find('a').trigger( 'focus' ); + } else { + prev.find('a').removeAttr( 'tabindex'); + prev.find('a').trigger( 'focus' ); + } + } }); if ( getUserSetting('cats') ) $('#category-tabs a[href="#categories-pop"]').trigger( 'click' ); diff --git a/src/js/_enqueues/admin/media.js b/src/js/_enqueues/admin/media.js index 13d46e485b6db..db61220b50e5f 100644 --- a/src/js/_enqueues/admin/media.js +++ b/src/js/_enqueues/admin/media.js @@ -10,7 +10,7 @@ * @requires jQuery */ -/* global ajaxurl, _wpMediaGridSettings, showNotice, findPosts */ +/* global ajaxurl, _wpMediaGridSettings, showNotice, findPosts, ClipboardJS */ ( function( $ ){ window.findPosts = { @@ -141,7 +141,11 @@ * @return {void} */ $( function() { - var settings, $mediaGridWrap = $( '#wp-media-grid' ); + var settings, + $mediaGridWrap = $( '#wp-media-grid' ), + copyAttachmentURLClipboard = new ClipboardJS( '.copy-attachment-url.media-library' ), + copyAttachmentURLSuccessTimeout, + previousSuccessElement = null; // Opens a manage media frame into the grid. if ( $mediaGridWrap.length && window.wp && window.wp.media ) { @@ -205,5 +209,42 @@ $( '.find-box-inside' ).on( 'click', 'tr', function() { $( this ).find( '.found-radio input' ).prop( 'checked', true ); }); + + /** + * Handles media list copy media URL button. + * + * @since 6.0.0 + * + * @param {MouseEvent} event A click event. + * @return {void} + */ + copyAttachmentURLClipboard.on( 'success', function( event ) { + var triggerElement = $( event.trigger ), + successElement = $( '.success', triggerElement.closest( '.copy-to-clipboard-container' ) ); + + // Clear the selection and move focus back to the trigger. + event.clearSelection(); + + // Checking if the previousSuccessElement is present, adding the hidden class to it. + if ( previousSuccessElement ) { + previousSuccessElement.addClass( 'hidden' ); + } + + // Show success visual feedback. + clearTimeout( copyAttachmentURLSuccessTimeout ); + successElement.removeClass( 'hidden' ); + + // Hide success visual feedback after 3 seconds since last success and unfocus the trigger. + copyAttachmentURLSuccessTimeout = setTimeout( function() { + successElement.addClass( 'hidden' ); + // No need to store the previous success element further. + previousSuccessElement = null; + }, 3000 ); + + previousSuccessElement = successElement; + + // Handle success audible feedback. + wp.a11y.speak( wp.i18n.__( 'The file URL has been copied to your clipboard' ) ); + } ); }); })( jQuery ); diff --git a/src/js/_enqueues/admin/password-toggle.js b/src/js/_enqueues/admin/password-toggle.js new file mode 100644 index 0000000000000..5bfaa3b122c01 --- /dev/null +++ b/src/js/_enqueues/admin/password-toggle.js @@ -0,0 +1,40 @@ +/** + * Adds functionality for password visibility buttons to toggle between text and password input types. + * + * @since 6.3.0 + * @output wp-admin/js/password-toggle.js + */ + +( function () { + var toggleElements, status, input, icon, label, __ = wp.i18n.__; + + toggleElements = document.querySelectorAll( '.pwd-toggle' ); + + toggleElements.forEach( function (toggle) { + toggle.classList.remove( 'hide-if-no-js' ); + toggle.addEventListener( 'click', togglePassword ); + } ); + + function togglePassword() { + status = this.getAttribute( 'data-toggle' ); + input = this.parentElement.children.namedItem( 'pwd' ); + icon = this.getElementsByClassName( 'dashicons' )[ 0 ]; + label = this.getElementsByClassName( 'text' )[ 0 ]; + + if ( 0 === parseInt( status, 10 ) ) { + this.setAttribute( 'data-toggle', 1 ); + this.setAttribute( 'aria-label', __( 'Hide password' ) ); + input.setAttribute( 'type', 'text' ); + label.innerHTML = __( 'Hide' ); + icon.classList.remove( 'dashicons-visibility' ); + icon.classList.add( 'dashicons-hidden' ); + } else { + this.setAttribute( 'data-toggle', 0 ); + this.setAttribute( 'aria-label', __( 'Show password' ) ); + input.setAttribute( 'type', 'password' ); + label.innerHTML = __( 'Show' ); + icon.classList.remove( 'dashicons-hidden' ); + icon.classList.add( 'dashicons-visibility' ); + } + } +} )(); diff --git a/src/js/_enqueues/admin/post.js b/src/js/_enqueues/admin/post.js index b0fca99135f14..d50fe6007d33b 100644 --- a/src/js/_enqueues/admin/post.js +++ b/src/js/_enqueues/admin/post.js @@ -135,21 +135,26 @@ window.wp = window.wp || {}; * @global */ window.WPRemoveThumbnail = function(nonce){ - $.post(ajaxurl, { - action: 'set-post-thumbnail', post_id: $( '#post_ID' ).val(), thumbnail_id: -1, _ajax_nonce: nonce, cookie: encodeURIComponent( document.cookie ) - }, + $.post( + ajaxurl, { + action: 'set-post-thumbnail', + post_id: $( '#post_ID' ).val(), + thumbnail_id: -1, + _ajax_nonce: nonce, + cookie: encodeURIComponent( document.cookie ) + }, /** * Handle server response * * @param {string} str Response, will be '0' when an error occurred otherwise contains link to add Featured Image. */ function(str){ - if ( str == '0' ) { - alert( __( 'Could not set that as the thumbnail image. Try a different attachment.' ) ); - } else { - WPSetThumbnailHTML(str); + if ( str == '0' ) { + alert( __( 'Could not set that as the thumbnail image. Try a different attachment.' ) ); + } else { + WPSetThumbnailHTML(str); + } } - } ); }; @@ -207,7 +212,9 @@ window.wp = window.wp || {}; height: 64, alt: '', src: received.lock_error.avatar_src, - srcset: received.lock_error.avatar_src_2x ? received.lock_error.avatar_src_2x + ' 2x' : undefined + srcset: received.lock_error.avatar_src_2x ? + received.lock_error.avatar_src_2x + ' 2x' : + undefined } ); wrap.find('div.post-locked-avatar').empty().append( avatar ); } @@ -336,9 +343,9 @@ jQuery( function($) { } }).filter(':visible').find('.wp-tab-first').trigger( 'focus' ); - // Set the heartbeat interval to 15 seconds if post lock dialogs are enabled. + // Set the heartbeat interval to 10 seconds if post lock dialogs are enabled. if ( wp.heartbeat && $('#post-lock-dialog').length ) { - wp.heartbeat.interval( 15 ); + wp.heartbeat.interval( 10 ); } // The form is being submitted by the user. @@ -427,25 +434,6 @@ jQuery( function($) { $previewField.val(''); }); - // This code is meant to allow tabbing from Title to Post content. - $('#title').on( 'keydown.editor-focus', function( event ) { - var editor; - - if ( event.keyCode === 9 && ! event.ctrlKey && ! event.altKey && ! event.shiftKey ) { - editor = typeof tinymce != 'undefined' && tinymce.get('content'); - - if ( editor && ! editor.isHidden() ) { - editor.focus(); - } else if ( $textarea.length ) { - $textarea.trigger( 'focus' ); - } else { - return; - } - - event.preventDefault(); - } - }); - // Auto save new posts after a title is typed. if ( $( '#auto_draft' ).val() ) { $( '#title' ).on( 'blur', function() { @@ -504,7 +492,7 @@ jQuery( function($) { // See https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event. return __( 'The changes you made will be lost if you navigate away from this page.' ); } - }).on( 'unload.edit-post', function( event ) { + }).on( 'pagehide.edit-post', function( event ) { if ( ! releaseLock ) { return; } @@ -578,16 +566,35 @@ jQuery( function($) { } // @todo Move to jQuery 1.3+, support for multiple hierarchical taxonomies, see wp-lists.js. - $('a', '#' + taxonomy + '-tabs').on( 'click', function( e ) { - e.preventDefault(); + $('a', '#' + taxonomy + '-tabs').on( 'click keyup keydown', function( event ) { var t = $(this).attr('href'); - $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); - $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide(); - $(t).show(); - if ( '#' + taxonomy + '-all' == t ) { - deleteUserSetting( settingName ); - } else { - setUserSetting( settingName, 'pop' ); + if ( event.type === 'keydown' && event.key === ' ' ) { + event.preventDefault(); + } + if ( ( event.type === 'keyup' && event.key === ' ' ) || ( event.type === 'keydown' && event.key === 'Enter' ) || event.type === 'click' ) { + event.preventDefault(); + $('#' + taxonomy + '-tabs a').removeAttr( 'aria-selected' ).attr( 'tabindex', '-1' ); + $(this).attr( 'aria-selected', 'true' ).removeAttr( 'tabindex' ); + $(this).parent().addClass('tabs').siblings('li').removeClass('tabs'); + $('#' + taxonomy + '-tabs').siblings('.tabs-panel').hide(); + $(t).show(); + if ( '#' + taxonomy + '-all' == t ) { + deleteUserSetting( settingName ); + } else { + setUserSetting( settingName, 'pop' ); + } + } + if ( event.type === 'keyup' && ( event.key === 'ArrowRight' || event.key === 'ArrowLeft' ) ) { + $(this).attr( 'tabindex', '-1' ); + let next = $(this).parent('li').next(); + let prev = $(this).parent('li').prev(); + if ( next.length > 0 ) { + next.find('a').removeAttr( 'tabindex'); + next.find('a').trigger( 'focus' ); + } else { + prev.find('a').removeAttr( 'tabindex'); + prev.find('a').trigger( 'focus' ); + } } }); @@ -666,11 +673,17 @@ jQuery( function($) { }); // Sync checked items between "All {taxonomy}" and "Most used" lists. - $('#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop').on( 'click', 'li.popular-category > label input[type="checkbox"]', function() { - var t = $(this), c = t.is(':checked'), id = t.val(); - if ( id && t.parents('#taxonomy-'+taxonomy).length ) - $('#in-' + taxonomy + '-' + id + ', #in-popular-' + taxonomy + '-' + id).prop( 'checked', c ); - }); + $('#' + taxonomy + 'checklist, #' + taxonomy + 'checklist-pop').on( + 'click', + 'li.popular-category > label input[type="checkbox"]', + function() { + var t = $(this), c = t.is(':checked'), id = t.val(); + if ( id && t.parents('#taxonomy-'+taxonomy).length ) { + $('input#in-' + taxonomy + '-' + id + ', input[id^="in-' + taxonomy + '-' + id + '-"]').prop('checked', c); + $('input#in-popular-' + taxonomy + '-' + id).prop('checked', c); + } + } + ); }); // End cats. @@ -749,11 +762,28 @@ jQuery( function($) { mm = $('#mm').val(), jj = $('#jj').val(), hh = $('#hh').val(), mn = $('#mn').val(); attemptedDate = new Date( aa, mm - 1, jj, hh, mn ); - originalDate = new Date( $('#hidden_aa').val(), $('#hidden_mm').val() -1, $('#hidden_jj').val(), $('#hidden_hh').val(), $('#hidden_mn').val() ); - currentDate = new Date( $('#cur_aa').val(), $('#cur_mm').val() -1, $('#cur_jj').val(), $('#cur_hh').val(), $('#cur_mn').val() ); + originalDate = new Date( + $('#hidden_aa').val(), + $('#hidden_mm').val() -1, + $('#hidden_jj').val(), + $('#hidden_hh').val(), + $('#hidden_mn').val() + ); + currentDate = new Date( + $('#cur_aa').val(), + $('#cur_mm').val() -1, + $('#cur_jj').val(), + $('#cur_hh').val(), + $('#cur_mn').val() + ); // Catch unexpected date problems. - if ( attemptedDate.getFullYear() != aa || (1 + attemptedDate.getMonth()) != mm || attemptedDate.getDate() != jj || attemptedDate.getMinutes() != mn ) { + if ( + attemptedDate.getFullYear() != aa || + (1 + attemptedDate.getMonth()) != mm || + attemptedDate.getDate() != jj || + attemptedDate.getMinutes() != mn + ) { $timestampdiv.find('.timestamp-wrap').addClass('form-invalid'); return false; } else { @@ -761,7 +791,7 @@ jQuery( function($) { } // Determine what the publish should be depending on the date and post status. - if ( attemptedDate > currentDate && $('#original_post_status').val() != 'future' ) { + if ( attemptedDate > currentDate ) { publishOn = __( 'Schedule for:' ); $('#publish').val( _x( 'Schedule', 'post action/button label' ) ); } else if ( attemptedDate <= currentDate && $('#original_post_status').val() != 'publish' ) { @@ -820,7 +850,10 @@ jQuery( function($) { ); // Show or hide the "Save Draft" button. - if ( $('option:selected', postStatus).val() == 'private' || $('option:selected', postStatus).val() == 'publish' ) { + if ( + $('option:selected', postStatus).val() == 'private' || + $('option:selected', postStatus).val() == 'publish' + ) { $('#save-post').hide(); } else { $('#save-post').show(); @@ -858,7 +891,7 @@ jQuery( function($) { }); // Set the selected visibility as current. - $postVisibilitySelect.find('.save-post-visibility').on( 'click', function( event ) { // Crazyhorse - multiple OK cancels. + $postVisibilitySelect.find('.save-post-visibility').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. var visibilityLabel = '', selectedVisibility = $postVisibilitySelect.find('input:radio:checked').val(); $postVisibilitySelect.slideUp('fast'); @@ -914,7 +947,7 @@ jQuery( function($) { }); // Save the changed timestamp. - $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse - multiple OK cancels. + $timestampdiv.find('.save-timestamp').on( 'click', function( event ) { // Crazyhorse branch - multiple OK cancels. if ( updateText() ) { $timestampdiv.slideUp('fast'); $timestampdiv.siblings('a.edit-timestamp').show().trigger( 'focus' ); @@ -972,7 +1005,7 @@ jQuery( function($) { * @return {void} */ function editPermalink() { - var i, slug_value, + var i, slug_value, slug_label, $el, revert_e, c = 0, real_slug = $('#post_name'), @@ -994,7 +1027,10 @@ jQuery( function($) { $el = $( '#editable-post-name' ); revert_e = $el.html(); - buttons.html( ' ' ); + buttons.html( + ' ' + + '' + ); // Save permalink changes. buttons.children( '.save' ).on( 'click', function() { @@ -1048,8 +1084,12 @@ jQuery( function($) { c++; } slug_value = ( c > full.length / 4 ) ? '' : full; + slug_label = __( 'URL Slug' ); - $el.html( '' ).children( 'input' ).on( 'keydown', function( e ) { + $el.html( + '' + + '' + ).children( 'input' ).on( 'keydown', function( e ) { var key = e.which; // On [Enter], just save the new slug, don't save the post. if ( 13 === key ) { @@ -1135,7 +1175,7 @@ jQuery( function($) { } /** - * When the dragging stopped make sure we return focus and do a sanity check on the height. + * When the dragging stopped make sure we return focus and do a confidence check on the height. */ function endDrag() { var height, toolbarHeight; @@ -1160,7 +1200,7 @@ jQuery( function($) { $document.off( '.wp-editor-resize' ); - // Sanity check: normalize height to stay within acceptable ranges. + // Confidence check: normalize height to stay within acceptable ranges. if ( height && height > 50 && height < 5000 ) { setUserSetting( 'ed_size', height ); } @@ -1223,7 +1263,12 @@ jQuery( function($) { $textarea.on( 'keydown.wp-autosave', function( event ) { // Key [S] has code 83. if ( event.which === 83 ) { - if ( event.shiftKey || event.altKey || ( isMac && ( ! event.metaKey || event.ctrlKey ) ) || ( ! isMac && ! event.ctrlKey ) ) { + if ( + event.shiftKey || + event.altKey || + ( isMac && ( ! event.metaKey || event.ctrlKey ) ) || + ( ! isMac && ! event.ctrlKey ) + ) { return; } @@ -1260,8 +1305,6 @@ jQuery( function($) { // Clear the selection and move focus back to the trigger. event.clearSelection(); - // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680 - triggerElement.trigger( 'focus' ); // Show success visual feedback. clearTimeout( copyAttachmentURLSuccessTimeout ); diff --git a/src/js/_enqueues/admin/postbox.js b/src/js/_enqueues/admin/postbox.js index 4f521acd93ba3..b6d1b6569dc84 100644 --- a/src/js/_enqueues/admin/postbox.js +++ b/src/js/_enqueues/admin/postbox.js @@ -14,7 +14,7 @@ __ = wp.i18n.__; /** - * This object contains all function to handle the behaviour of the post boxes. The post boxes are the boxes you see + * This object contains all function to handle the behavior of the post boxes. The post boxes are the boxes you see * around the content on the edit page. * * @since 2.7.0 @@ -347,7 +347,7 @@ }, /** - * Initializes all the postboxes, mainly their sortable behaviour. + * Initializes all the postboxes, mainly their sortable behavior. * * @since 2.7.0 * @@ -461,13 +461,19 @@ closed = $( '.postbox' ).filter( '.closed' ).map( function() { return this.id; } ).get().join( ',' ); hidden = $( '.postbox' ).filter( ':hidden' ).map( function() { return this.id; } ).get().join( ',' ); - $.post(ajaxurl, { - action: 'closed-postboxes', - closed: closed, - hidden: hidden, - closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(), - page: page - }); + $.post( + ajaxurl, + { + action: 'closed-postboxes', + closed: closed, + hidden: hidden, + closedpostboxesnonce: jQuery('#closedpostboxesnonce').val(), + page: page + }, + function() { + wp.a11y.speak( __( 'Screen Options updated.' ) ); + } + ); }, /** diff --git a/src/js/_enqueues/admin/privacy-tools.js b/src/js/_enqueues/admin/privacy-tools.js index e5fceb86d78c6..4e12939be7621 100644 --- a/src/js/_enqueues/admin/privacy-tools.js +++ b/src/js/_enqueues/admin/privacy-tools.js @@ -47,7 +47,7 @@ jQuery( function( $ ) { $requestRow.after( function() { return '' + - '
    ' + + '' + diff --git a/src/js/_enqueues/admin/site-health.js b/src/js/_enqueues/admin/site-health.js index e9e5c06bdbb58..57d5c9cbcf289 100644 --- a/src/js/_enqueues/admin/site-health.js +++ b/src/js/_enqueues/admin/site-health.js @@ -15,6 +15,8 @@ jQuery( function( $ ) { isStatusTab = $( '.health-check-body.health-check-status-tab' ).length, isDebugTab = $( '.health-check-body.health-check-debug-tab' ).length, pathsSizesSection = $( '#health-check-accordion-block-wp-paths-sizes' ), + menuCounterWrapper = $( '#adminmenu .site-health-counter' ), + menuCounter = $( '#adminmenu .site-health-counter .count' ), successTimeout; // Debug information copy section. @@ -24,8 +26,6 @@ jQuery( function( $ ) { // Clear the selection and move focus back to the trigger. e.clearSelection(); - // Handle ClipboardJS focus bug, see https://github.com/zenorocha/clipboard.js/issues/680 - triggerElement.trigger( 'focus' ); // Show success visual feedback. clearTimeout( successTimeout ); @@ -34,10 +34,6 @@ jQuery( function( $ ) { // Hide success visual feedback after 3 seconds since last success. successTimeout = setTimeout( function() { successElement.addClass( 'hidden' ); - // Remove the visually hidden textarea so that it isn't perceived by assistive technologies. - if ( clipboard.clipboardAction.fakeElem && clipboard.clipboardAction.removeFake ) { - clipboard.clipboardAction.removeFake(); - } }, 3000 ); // Handle success audible feedback. @@ -48,6 +44,10 @@ jQuery( function( $ ) { $( '.health-check-accordion' ).on( 'click', '.health-check-accordion-trigger', function() { var isExpanded = ( 'true' === $( this ).attr( 'aria-expanded' ) ); + if ( $( this ).prop( 'id' ) ) { + window.location.hash = $( this ).prop( 'id' ); + } + if ( isExpanded ) { $( this ).attr( 'aria-expanded', 'false' ); $( '#' + $( this ).attr( 'aria-controls' ) ).attr( 'hidden', true ); @@ -57,6 +57,20 @@ jQuery( function( $ ) { } } ); + /* global setTimeout */ + wp.domReady( function() { + // Get hash from query string and open the related accordion. + var hash = window.location.hash; + + if ( hash ) { + var requestedPanel = $( hash ); + + if ( requestedPanel.is( '.health-check-accordion-trigger' ) ) { + requestedPanel.trigger( 'click' ); + } + } + } ); + // Site Health test handling. $( '.site-health-view-passed' ).on( 'click', function() { @@ -168,6 +182,19 @@ jQuery( function( $ ) { $( '.site-health-issue-count-title', issueWrapper ).html( heading ); } + menuCounter.text( SiteHealth.site_status.issues.critical ); + + if ( 0 < parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { + $( '#health-check-issues-critical' ).removeClass( 'hidden' ); + + menuCounterWrapper.removeClass( 'count-0' ); + } else { + menuCounterWrapper.addClass( 'count-0' ); + } + if ( 0 < parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) { + $( '#health-check-issues-recommended' ).removeClass( 'hidden' ); + } + $( '.issues', '#health-check-issues-' + issue.status ).append( template( issue ) ); } @@ -210,24 +237,16 @@ jQuery( function( $ ) { $circle.css( { strokeDashoffset: pct } ); - if ( 1 > parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { - $( '#health-check-issues-critical' ).addClass( 'hidden' ); - } - - if ( 1 > parseInt( SiteHealth.site_status.issues.recommended, 0 ) ) { - $( '#health-check-issues-recommended' ).addClass( 'hidden' ); - } - if ( 80 <= val && 0 === parseInt( SiteHealth.site_status.issues.critical, 0 ) ) { $wrapper.addClass( 'green' ).removeClass( 'orange' ); $progressLabel.text( __( 'Good' ) ); - wp.a11y.speak( __( 'All site health tests have finished running. Your site is looking good, and the results are now available on the page.' ) ); + announceTestsProgression( 'good' ); } else { $wrapper.addClass( 'orange' ).removeClass( 'green' ); $progressLabel.text( __( 'Should be improved' ) ); - wp.a11y.speak( __( 'All site health tests have finished running. There are items that should be addressed, and the results are now available on the page.' ) ); + announceTestsProgression( 'improvable' ); } if ( isStatusTab ) { @@ -376,7 +395,7 @@ jQuery( function( $ ) { // After 3 seconds announce that we're still waiting for directory sizes. var timeout = window.setTimeout( function() { - wp.a11y.speak( __( 'Please wait...' ) ); + announceTestsProgression( 'waiting-for-directory-sizes' ); }, 3000 ); wp.apiRequest( { @@ -387,7 +406,6 @@ jQuery( function( $ ) { var delay = ( new Date().getTime() ) - timestamp; $( '.health-check-wp-paths-sizes.spinner' ).css( 'visibility', 'hidden' ); - recalculateProgression(); if ( delay > 3000 ) { /* @@ -402,7 +420,7 @@ jQuery( function( $ ) { } window.setTimeout( function() { - wp.a11y.speak( __( 'All site health tests have finished running.' ) ); + recalculateProgression(); }, delay ); } else { // Cancel the announcement. @@ -449,4 +467,34 @@ jQuery( function( $ ) { $( '.health-check-offscreen-nav-wrapper' ).on( 'click', function() { $( this ).toggleClass( 'visible' ); } ); + + /** + * Announces to assistive technologies the tests progression status. + * + * @since 6.4.0 + * + * @param {string} type The type of message to be announced. + * + * @return {void} + */ + function announceTestsProgression( type ) { + // Only announce the messages in the Site Health pages. + if ( 'site-health' !== SiteHealth.screen ) { + return; + } + + switch ( type ) { + case 'good': + wp.a11y.speak( __( 'All site health tests have finished running. Your site is looking good.' ) ); + break; + case 'improvable': + wp.a11y.speak( __( 'All site health tests have finished running. There are items that should be addressed.' ) ); + break; + case 'waiting-for-directory-sizes': + wp.a11y.speak( __( 'Running additional tests... please wait.' ) ); + break; + default: + return; + } + } } ); diff --git a/src/js/_enqueues/admin/site-icon.js b/src/js/_enqueues/admin/site-icon.js new file mode 100644 index 0000000000000..a658f16e0b61a --- /dev/null +++ b/src/js/_enqueues/admin/site-icon.js @@ -0,0 +1,242 @@ +/** + * Handle the site icon setting in options-general.php. + * + * @since 6.5.0 + * @output wp-admin/js/site-icon.js + */ + +/* global jQuery, wp */ + +( function ( $ ) { + var $chooseButton = $( '#choose-from-library-button' ), + $iconPreview = $( '#site-icon-preview' ), + $browserIconPreview = $( '#browser-icon-preview' ), + $appIconPreview = $( '#app-icon-preview' ), + $hiddenDataField = $( '#site_icon_hidden_field' ), + $removeButton = $( '#js-remove-site-icon' ), + frame; + + /** + * Calculate image selection options based on the attachment dimensions. + * + * @since 6.5.0 + * + * @param {Object} attachment The attachment object representing the image. + * @return {Object} The image selection options. + */ + function calculateImageSelectOptions( attachment ) { + var realWidth = attachment.get( 'width' ), + realHeight = attachment.get( 'height' ), + xInit = 512, + yInit = 512, + ratio = xInit / yInit, + xImg = xInit, + yImg = yInit, + x1, + y1, + imgSelectOptions; + + if ( realWidth / realHeight > ratio ) { + yInit = realHeight; + xInit = yInit * ratio; + } else { + xInit = realWidth; + yInit = xInit / ratio; + } + + x1 = ( realWidth - xInit ) / 2; + y1 = ( realHeight - yInit ) / 2; + + imgSelectOptions = { + aspectRatio: xInit + ':' + yInit, + handles: true, + keys: true, + instance: true, + persistent: true, + imageWidth: realWidth, + imageHeight: realHeight, + minWidth: xImg > xInit ? xInit : xImg, + minHeight: yImg > yInit ? yInit : yImg, + x1: x1, + y1: y1, + x2: xInit + x1, + y2: yInit + y1, + }; + + return imgSelectOptions; + } + + /** + * Initializes the media frame for selecting or cropping an image. + * + * @since 6.5.0 + */ + $chooseButton.on( 'click', function () { + var $el = $( this ); + + // Create the media frame. + frame = wp.media( { + button: { + // Set the text of the button. + text: $el.data( 'update' ), + + // Don't close, we might need to crop. + close: false, + }, + states: [ + new wp.media.controller.Library( { + title: $el.data( 'choose-text' ), + library: wp.media.query( { type: 'image' } ), + date: false, + suggestedWidth: $el.data( 'size' ), + suggestedHeight: $el.data( 'size' ), + } ), + new wp.media.controller.SiteIconCropper( { + control: { + params: { + width: $el.data( 'size' ), + height: $el.data( 'size' ), + }, + }, + imgSelectOptions: calculateImageSelectOptions, + } ), + ], + } ); + + frame.on( 'cropped', function ( attachment ) { + $hiddenDataField.val( attachment.id ); + switchToUpdate( attachment ); + frame.close(); + + // Start over with a frame that is so fresh and so clean clean. + frame = null; + } ); + + // When an image is selected, run a callback. + frame.on( 'select', function () { + // Grab the selected attachment. + var attachment = frame.state().get( 'selection' ).first(); + + if ( + attachment.attributes.height === $el.data( 'size' ) && + $el.data( 'size' ) === attachment.attributes.width + ) { + switchToUpdate( attachment.attributes ); + frame.close(); + + // Set the value of the hidden input to the attachment id. + $hiddenDataField.val( attachment.id ); + } else { + frame.setState( 'cropper' ); + } + } ); + + frame.open(); + } ); + + /** + * Update the UI when a site icon is selected. + * + * @since 6.5.0 + * + * @param {array} attributes The attributes for the attachment. + */ + function switchToUpdate( attributes ) { + var i18nAppAlternativeString, i18nBrowserAlternativeString; + + if ( attributes.alt ) { + i18nAppAlternativeString = wp.i18n.sprintf( + /* translators: %s: The selected image alt text. */ + wp.i18n.__( 'App icon preview: Current image: %s' ), + attributes.alt + ); + i18nBrowserAlternativeString = wp.i18n.sprintf( + /* translators: %s: The selected image alt text. */ + wp.i18n.__( 'Browser icon preview: Current image: %s' ), + attributes.alt + ); + } else { + i18nAppAlternativeString = wp.i18n.sprintf( + /* translators: %s: The selected image filename. */ + wp.i18n.__( + 'App icon preview: The current image has no alternative text. The file name is: %s' + ), + attributes.filename + ); + i18nBrowserAlternativeString = wp.i18n.sprintf( + /* translators: %s: The selected image filename. */ + wp.i18n.__( + 'Browser icon preview: The current image has no alternative text. The file name is: %s' + ), + attributes.filename + ); + } + + // Set site-icon-img src and alternative text to app icon preview. + $appIconPreview.attr( { + src: attributes.url, + alt: i18nAppAlternativeString, + } ); + + // Set site-icon-img src and alternative text to browser preview. + $browserIconPreview.attr( { + src: attributes.url, + alt: i18nBrowserAlternativeString, + } ); + + // Remove hidden class from icon preview div and remove button. + $iconPreview.removeClass( 'hidden' ); + $removeButton.removeClass( 'hidden' ); + + // Set the global CSS variable for --site-icon-url to the selected image URL. + document.documentElement.style.setProperty( + '--site-icon-url', + 'url(' + attributes.url + ')' + ); + + // If the choose button is not in the update state, swap the classes. + if ( $chooseButton.attr( 'data-state' ) !== '1' ) { + $chooseButton.attr( { + class: $chooseButton.attr( 'data-alt-classes' ), + 'data-alt-classes': $chooseButton.attr( 'class' ), + 'data-state': '1', + } ); + } + + // Swap the text of the choose button. + $chooseButton.text( $chooseButton.attr( 'data-update-text' ) ); + } + + /** + * Handles the click event of the remove button. + * + * @since 6.5.0 + */ + $removeButton.on( 'click', function () { + $hiddenDataField.val( 'false' ); + $( this ).toggleClass( 'hidden' ); + $iconPreview.toggleClass( 'hidden' ); + $browserIconPreview.attr( { + src: '', + alt: '', + } ); + $appIconPreview.attr( { + src: '', + alt: '', + } ); + + /** + * Resets state to the button, for correct visual style and state. + * Updates the text of the button. + * Sets focus state to the button. + */ + $chooseButton + .attr( { + class: $chooseButton.attr( 'data-alt-classes' ), + 'data-alt-classes': $chooseButton.attr( 'class' ), + 'data-state': '', + } ) + .text( $chooseButton.attr( 'data-choose-text' ) ) + .trigger( 'focus' ); + } ); +} )( jQuery ); diff --git a/src/js/_enqueues/admin/tags-suggest.js b/src/js/_enqueues/admin/tags-suggest.js index 0381f0053cb22..d439f06ef0de8 100644 --- a/src/js/_enqueues/admin/tags-suggest.js +++ b/src/js/_enqueues/admin/tags-suggest.js @@ -4,12 +4,11 @@ * @output wp-admin/js/tags-suggest.js */ ( function( $ ) { - if ( typeof window.uiAutocompleteL10n === 'undefined' ) { - return; - } - var tempID = 0; var separator = wp.i18n._x( ',', 'tag delimiter' ) || ','; + var __ = wp.i18n.__, + _n = wp.i18n._n, + sprintf = wp.i18n.sprintf; function split( val ) { return val.split( new RegExp( separator + '\\s*' ) ); @@ -63,7 +62,8 @@ $.get( window.ajaxurl, { action: 'ajax-tag-search', tax: taxonomy, - q: term + q: term, + number: 20 } ).always( function() { $element.removeClass( 'ui-autocomplete-loading' ); // UI fails to remove this sometimes? } ).done( function( data ) { @@ -138,13 +138,17 @@ collision: 'none' }, messages: { - noResults: window.uiAutocompleteL10n.noResults, + noResults: __( 'No results found.' ), results: function( number ) { - if ( number > 1 ) { - return window.uiAutocompleteL10n.manyResults.replace( '%d', number ); - } - - return window.uiAutocompleteL10n.oneResult; + return sprintf( + /* translators: %d: Number of search results found. */ + _n( + '%d result found. Use up and down arrow keys to navigate.', + '%d results found. Use up and down arrow keys to navigate.', + number + ), + number + ); } } }, options ); diff --git a/src/js/_enqueues/admin/tags.js b/src/js/_enqueues/admin/tags.js index 6c25e55537d97..ff7761adb8d3e 100644 --- a/src/js/_enqueues/admin/tags.js +++ b/src/js/_enqueues/admin/tags.js @@ -31,6 +31,12 @@ jQuery( function($) { if ( r ) { data = t.attr('href').replace(/[^?]*\?/, '').replace(/action=delete/, 'action=delete-tag'); + tr.children().css('backgroundColor', '#faafaa'); + + // Disable pointer events and all form controls/links in the row + tr.css('pointer-events', 'none'); + tr.find(':input, a').prop('disabled', true).attr('tabindex', -1); + /** * Makes a request to the server to delete the term that corresponds to the * delete term button. @@ -40,8 +46,20 @@ jQuery( function($) { * @return {void} */ $.post(ajaxurl, data, function(r){ + var message; if ( '1' == r ) { $('#ajax-response').empty(); + let nextFocus = tr.next( 'tr' ).find( 'a.row-title' ); + let prevFocus = tr.prev( 'tr' ).find( 'a.row-title' ); + // If there is neither a next row or a previous row, focus the tag input field. + if ( nextFocus.length < 1 && prevFocus.length < 1 ) { + nextFocus = $( '#tag-name' ).trigger( 'focus' ); + } else { + if ( nextFocus.length < 1 ) { + nextFocus = prevFocus; + } + } + tr.fadeOut('normal', function(){ tr.remove(); }); /** @@ -53,23 +71,38 @@ jQuery( function($) { */ $('select#parent option[value="' + data.match(/tag_ID=(\d+)/)[1] + '"]').remove(); $('a.tag-link-' + data.match(/tag_ID=(\d+)/)[1]).remove(); - + nextFocus.trigger( 'focus' ); + message = wp.i18n.__( 'The selected tag has been deleted.' ); + } else if ( '-1' == r ) { - $('#ajax-response').empty().append('

    ' + wp.i18n.__( 'Sorry, you are not allowed to do that.' ) + '

    '); - tr.children().css('backgroundColor', ''); + message = wp.i18n.__( 'Sorry, you are not allowed to do that.' ); + $('#ajax-response').empty().append('

    ' + message + '

    '); + resetRowAfterFailure( tr ); } else { - $('#ajax-response').empty().append('

    ' + wp.i18n.__( 'Something went wrong.' ) + '

    '); - tr.children().css('backgroundColor', ''); + message = wp.i18n.__( 'An error occurred while processing your request. Please try again later.' ); + $('#ajax-response').empty().append('

    ' + message + '

    '); + resetRowAfterFailure( tr ); } + wp.a11y.speak( message, 'assertive' ); }); - - tr.children().css('backgroundColor', '#f33'); } return false; }); + /** + * Restores the original UI state of a table row after an AJAX failure. + * + * @param {jQuery} tr The table row to reset. + * @return {void} + */ + function resetRowAfterFailure( tr ) { + tr.children().css( 'backgroundColor', '' ); + tr.css( 'pointer-events', '' ); + tr.find( ':input, a' ).prop( 'disabled', false ).removeAttr( 'tabindex' ); + } + /** * Adds a deletion confirmation when removing a tag. * @@ -101,9 +134,6 @@ jQuery( function($) { $('#submit').on( 'click', function(){ var form = $(this).parents('form'); - if ( ! validateForm( form ) ) - return false; - if ( addingTerm ) { // If we're adding a term, noop the button to avoid duplicate requests. return false; @@ -127,8 +157,14 @@ jQuery( function($) { $('#ajax-response').empty(); res = wpAjax.parseAjaxResponse( r, 'ajax-response' ); - if ( ! res || res.errors ) + + if ( res.errors && res.responses[0].errors[0].code === 'empty_term_name' ) { + validateForm( form ); + } + + if ( ! res || res.errors ) { return; + } parent = form.find( 'select#parent' ).val(); @@ -155,7 +191,7 @@ jQuery( function($) { form.find( 'select#parent option:selected' ).after( '' ); } - $('input[type="text"]:visible, textarea:visible', form).val(''); + $('input:not([type="checkbox"]):not([type="radio"]):not([type="button"]):not([type="submit"]):not([type="reset"]):visible, textarea:visible', form).val(''); }); return false; diff --git a/src/js/_enqueues/admin/user-profile.js b/src/js/_enqueues/admin/user-profile.js index c2a403fe38799..5614078bb584a 100644 --- a/src/js/_enqueues/admin/user-profile.js +++ b/src/js/_enqueues/admin/user-profile.js @@ -2,10 +2,12 @@ * @output wp-admin/js/user-profile.js */ -/* global ajaxurl, pwsL10n, userProfileL10n */ +/* global ajaxurl, pwsL10n, userProfileL10n, ClipboardJS */ (function($) { var updateLock = false, + isSubmitting = false, __ = wp.i18n.__, + clipboard = new ClipboardJS( '.application-password-display .copy-button' ), $pass1Row, $pass1, $pass2, @@ -15,7 +17,14 @@ $submitButtons, $submitButton, currentPass, - $passwordWrapper; + $form, + originalFormContent, + $passwordWrapper, + successTimeout, + isMac = window.navigator.platform ? window.navigator.platform.indexOf( 'Mac' ) !== -1 : false, + ua = navigator.userAgent.toLowerCase(), + isSafari = window.safari !== 'undefined' && typeof window.safari === 'object', + isFirefox = ua.indexOf( 'firefox' ) !== -1; function generatePassword() { if ( typeof zxcvbn !== 'function' ) { @@ -32,6 +41,13 @@ showOrHideWeakPasswordCheckbox(); } + /* + * This works around a race condition when zxcvbn loads quickly and + * causes `generatePassword()` to run prior to the toggle button being + * bound. + */ + bindToggleButton(); + // Install screen. if ( 1 !== parseInt( $toggleButton.data( 'start-masked' ), 10 ) ) { // Show the password not masked if admin_password hasn't been posted yet. @@ -43,6 +59,11 @@ // Once zxcvbn loads, passwords strength is known. $( '#pw-weak-text-label' ).text( __( 'Confirm use of weak password' ) ); + + // Focus the password field if not the install screen. + if ( 'mailserver_pass' !== $pass1.prop('id' ) && ! $('#weblog_title').length ) { + $( $pass1 ).trigger( 'focus' ); + } } function bindPass1() { @@ -63,6 +84,8 @@ $pass1.removeClass( 'short bad good strong' ); showOrHideWeakPasswordCheckbox(); } ); + + bindCapsLockWarning( $pass1 ); } function resetToggle( show ) { @@ -79,7 +102,13 @@ } function bindToggleButton() { + if ( !! $toggleButton ) { + // Do not rebind. + return; + } $toggleButton = $pass1Row.find('.wp-hide-pw'); + + // Toggle between showing and hiding the password. $toggleButton.show().on( 'click', function () { if ( 'password' === $pass1.attr( 'type' ) ) { $pass1.attr( 'type', 'text' ); @@ -89,6 +118,14 @@ resetToggle( true ); } }); + + // Ensure the password input type is set to password when the form is submitted. + $pass1Row.closest( 'form' ).on( 'submit', function() { + if ( $pass1.attr( 'type' ) === 'text' ) { + $pass1.attr( 'type', 'password' ); + resetToggle( true ); + } + } ); } /** @@ -132,7 +169,9 @@ * @param {string} message The message to insert. */ function addInlineNotice( $this, success, message ) { - var resultDiv = $( '
    ' ); + var resultDiv = $( '
    ', { + role: 'alert' + } ); // Set up the notice div. resultDiv.addClass( 'notice inline' ); @@ -157,7 +196,7 @@ var $generateButton, $cancelButton; - $pass1Row = $( '.user-pass1-wrap, .user-pass-wrap, .reset-pass-submit' ); + $pass1Row = $( '.user-pass1-wrap, .user-pass-wrap, .mailserver-pass-wrap, .reset-pass-submit' ); // Hide the confirm password field when JavaScript support is enabled. $('.user-pass2-wrap').hide(); @@ -174,12 +213,14 @@ $submitButtons.prop( 'disabled', ! $weakCheckbox.prop( 'checked' ) ); } ); - $pass1 = $('#pass1'); + $pass1 = $('#pass1, #mailserver_pass'); if ( $pass1.length ) { bindPass1(); } else { // Password field for the login form. $pass1 = $( '#user_pass' ); + + bindCapsLockWarning( $pass1 ); } /* @@ -213,7 +254,7 @@ updateLock = true; // Make sure the password fields are shown. - $generateButton.attr( 'aria-expanded', 'true' ); + $generateButton.not( '.skip-aria-expanded' ).attr( 'aria-expanded', 'true' ); $passwordWrapper .show() .addClass( 'is-open' ); @@ -254,6 +295,8 @@ // Stop an empty password from being submitted as a change. $submitButtons.prop( 'disabled', false ); + + $generateButton.attr( 'aria-expanded', 'false' ); } ); $pass1Row.closest( 'form' ).on( 'submit', function () { @@ -293,32 +336,130 @@ $('#pass-strength-result').addClass('short').html( pwsL10n.mismatch ); break; default: - $('#pass-strength-result').addClass('short').html( pwsL10n['short'] ); + $('#pass-strength-result').addClass('short').html( pwsL10n.short ); } } - function showOrHideWeakPasswordCheckbox() { - var passStrength = $('#pass-strength-result')[0]; + /** + * Bind Caps Lock detection to a password input field. + * + * @param {jQuery} $input The password input field. + */ + function bindCapsLockWarning( $input ) { + var $capsWarning, + $capsIcon, + $capsText, + capsLockOn = false; + + // Skip warning on macOS Safari + Firefox (they show native indicators). + if ( isMac && ( isSafari || isFirefox ) ) { + return; + } + + $capsWarning = $( '
    ' ); + $capsIcon = $( '' ); + $capsText = $( '', { 'class': 'caps-warning-text', text: __( 'Caps lock is on.' ) } ); + $capsWarning.append( $capsIcon, $capsText ); - if ( passStrength.className ) { - $pass1.addClass( passStrength.className ); - if ( $( passStrength ).is( '.short, .bad' ) ) { - if ( ! $weakCheckbox.prop( 'checked' ) ) { - $submitButtons.prop( 'disabled', true ); + $input.parent( 'div' ).append( $capsWarning ); + + $input.on( 'keydown', function( jqEvent ) { + var event = jqEvent.originalEvent; + + // Skip if key is not a printable character. + // Key length > 1 usually means non-printable (e.g., "Enter", "Tab"). + if ( event.ctrlKey || event.metaKey || event.altKey || ! event.key || event.key.length !== 1 ) { + return; + } + + var state = isCapsLockOn( event ); + + // React when the state changes or if caps lock is on when the user starts typing. + if ( state !== capsLockOn ) { + capsLockOn = state; + + if ( capsLockOn ) { + $capsWarning.show(); + // Don't duplicate existing screen reader Caps lock notifications. + if ( event.key !== 'CapsLock' ) { + wp.a11y.speak( __( 'Caps lock is on.' ), 'assertive' ); + } + } else { + $capsWarning.hide(); } - $weakRow.show(); - } else { - if ( $( passStrength ).is( '.empty' ) ) { - $submitButtons.prop( 'disabled', true ); - $weakCheckbox.prop( 'checked', false ); + } + } ); + + $input.on( 'blur', function() { + if ( ! document.hasFocus() ) { + return; + } + capsLockOn = false; + $capsWarning.hide(); + } ); + } + + /** + * Determines if Caps Lock is currently enabled. + * + * On macOS Safari and Firefox, the native warning is preferred, + * so this function returns false to suppress custom warnings. + * + * @param {KeyboardEvent} e The keydown event object. + * + * @return {boolean} True if Caps Lock is on, false otherwise. + */ + function isCapsLockOn( event ) { + return event.getModifierState( 'CapsLock' ); + } + + function showOrHideWeakPasswordCheckbox() { + var passStrengthResult = $('#pass-strength-result'); + + if ( passStrengthResult.length ) { + var passStrength = passStrengthResult[0]; + + if ( passStrength.className ) { + $pass1.addClass( passStrength.className ); + if ( $( passStrength ).is( '.short, .bad' ) ) { + if ( ! $weakCheckbox.prop( 'checked' ) ) { + $submitButtons.prop( 'disabled', true ); + } + $weakRow.show(); } else { - $submitButtons.prop( 'disabled', false ); + if ( $( passStrength ).is( '.empty' ) ) { + $submitButtons.prop( 'disabled', true ); + $weakCheckbox.prop( 'checked', false ); + } else { + $submitButtons.prop( 'disabled', false ); + } + $weakRow.hide(); } - $weakRow.hide(); } } } + // Debug information copy section. + clipboard.on( 'success', function( e ) { + var triggerElement = $( e.trigger ), + successElement = $( '.success', triggerElement.closest( '.application-password-display' ) ); + + // Clear the selection and move focus back to the trigger. + e.clearSelection(); + + // Show success visual feedback. + clearTimeout( successTimeout ); + successElement.removeClass( 'hidden' ); + + // Hide success visual feedback after 3 seconds since last success. + successTimeout = setTimeout( function() { + successElement.addClass( 'hidden' ); + }, 3000 ); + + // Handle success audible feedback. + wp.a11y.speak( __( 'Application password has been copied to your clipboard.' ) ); + } ); + $( function() { var $colorpicker, $stylesheet, user_id, current_user_id, select = $( '#display_name' ), @@ -432,6 +573,12 @@ bindPasswordForm(); bindPasswordResetLink(); + $submitButtons.on( 'click', function() { + isSubmitting = true; + }); + + $form = $( '#your-profile, #createuser' ); + originalFormContent = $form.serialize(); }); $( '#destroy-sessions' ).on( 'click', function( e ) { @@ -443,10 +590,10 @@ }).done( function( response ) { $this.prop( 'disabled', true ); $this.siblings( '.notice' ).remove(); - $this.before( '

    ' + response.message + '

    ' ); + $this.before( '' ); }).fail( function( response ) { $this.siblings( '.notice' ).remove(); - $this.before( '

    ' + response.message + '

    ' ); + $this.before( '' ); }); e.preventDefault(); @@ -459,7 +606,10 @@ if ( true === updateLock ) { return __( 'Your new password has not been saved.' ); } - } ); + if ( originalFormContent !== $form.serialize() && ! isSubmitting ) { + return __( 'The changes you made will be lost if you navigate away from this page.' ); + } + }); /* * We need to generate a password as soon as the Reset Password page is loaded, diff --git a/src/js/_enqueues/deprecated/fakejshint.js b/src/js/_enqueues/deprecated/fakejshint.js new file mode 100644 index 0000000000000..2ab6326b96262 --- /dev/null +++ b/src/js/_enqueues/deprecated/fakejshint.js @@ -0,0 +1,53 @@ +/** + * JSHINT has some GPL Compatability issues, so we are faking it out and using esprima for validation + * Based on https://github.com/jquery/esprima/blob/gh-pages/demo/validate.js which is MIT licensed. + * This is now deprecated in favor of Espree. + * + * @since 4.9.3 + * @deprecated 7.0.0 + * @output wp-includes/js/codemirror/fakejshint.js + * @see https://core.trac.wordpress.org/ticket/42850 + * @see https://core.trac.wordpress.org/ticket/64558 + */ + +/* jshint -W057, -W058 */ +var fakeJSHINT = new function() { + var syntax, errors; + var that = this; + this.data = []; + this.convertError = function( error ){ + return { + line: error.lineNumber, + character: error.column, + reason: error.description, + code: 'E' + }; + }; + this.parse = function( code ){ + try { + syntax = window.esprima.parse(code, { tolerant: true, loc: true }); + errors = syntax.errors; + if ( errors.length > 0 ) { + for ( var i = 0; i < errors.length; i++) { + var error = errors[i]; + that.data.push( that.convertError( error ) ); + } + } else { + that.data = []; + } + } catch (e) { + that.data.push( that.convertError( e ) ); + } + }; +}; + +window.JSHINT = function( text ){ + fakeJSHINT.parse( text ); +}; +window.JSHINT.data = function(){ + return { + errors: fakeJSHINT.data + }; +}; + + diff --git a/src/js/_enqueues/lib/accordion.js b/src/js/_enqueues/lib/accordion.js index c420e8ccb88ef..9b52b179b22a6 100644 --- a/src/js/_enqueues/lib/accordion.js +++ b/src/js/_enqueues/lib/accordion.js @@ -7,18 +7,18 @@ * *
    *
    - *

    - *
    + *

    + *
    *
    *
    *
    - *

    - *
    + *

    + *
    *
    *
    *
    - *

    - *
    + *

    + *
    *
    *
    *
    @@ -34,13 +34,7 @@ $( function () { // Expand/Collapse accordion sections on click. - $( '.accordion-container' ).on( 'click keydown', '.accordion-section-title', function( e ) { - if ( e.type === 'keydown' && 13 !== e.which ) { // "Return" key. - return; - } - - e.preventDefault(); // Keep this AFTER the key filter above. - + $( '.accordion-container' ).on( 'click', '.accordion-section-title button', function() { accordionSwitch( $( this ) ); }); @@ -54,7 +48,6 @@ */ function accordionSwitch ( el ) { var section = el.closest( '.accordion-section' ), - sectionToggleControl = section.find( '[aria-expanded]' ).first(), container = section.closest( '.accordion-container' ), siblings = container.find( '.open' ), siblingsToggleControl = siblings.find( '[aria-expanded]' ).first(), @@ -86,8 +79,8 @@ }, 150); // If there's an element with an aria-expanded attribute, assume it's a toggle control and toggle the aria-expanded value. - if ( sectionToggleControl ) { - sectionToggleControl.attr( 'aria-expanded', String( sectionToggleControl.attr( 'aria-expanded' ) === 'false' ) ); + if ( el ) { + el.attr( 'aria-expanded', String( el.attr( 'aria-expanded' ) === 'false' ) ); } } diff --git a/src/js/_enqueues/lib/admin-bar.js b/src/js/_enqueues/lib/admin-bar.js index 8cb94ea72f69a..eb18c61f8b5b8 100644 --- a/src/js/_enqueues/lib/admin-bar.js +++ b/src/js/_enqueues/lib/admin-bar.js @@ -31,7 +31,7 @@ topMenuItems = adminBar.querySelectorAll( 'li.menupop' ); allMenuItems = adminBar.querySelectorAll( '.ab-item' ); - adminBarLogout = document.getElementById( 'wp-admin-bar-logout' ); + adminBarLogout = document.querySelector( '#wp-admin-bar-logout a' ); adminBarSearchForm = document.getElementById( 'adminbarsearch' ); shortlink = document.getElementById( 'wp-admin-bar-get-shortlink' ); skipLink = adminBar.querySelector( '.screen-reader-shortcut' ); @@ -95,11 +95,6 @@ } ); } - if ( skipLink ) { - // Focus the target of skip link after pressing Enter. - skipLink.addEventListener( 'keydown', focusTargetAfterEnter ); - } - if ( shortlink ) { shortlink.addEventListener( 'click', clickShortlink ); } @@ -149,7 +144,8 @@ function toggleHoverIfEnter( event ) { var wrapper; - if ( event.which !== 13 ) { + // Follow link if pressing Ctrl and/or Shift with Enter (opening in a new tab or window). + if ( event.which !== 13 || event.ctrlKey || event.shiftKey ) { return; } @@ -173,36 +169,7 @@ } /** - * Focus the target of skip link after pressing Enter. - * - * @since 5.3.1 - * - * @param {Event} event The keydown event. - */ - function focusTargetAfterEnter( event ) { - var id, userAgent; - - if ( event.which !== 13 ) { - return; - } - - id = event.target.getAttribute( 'href' ); - userAgent = navigator.userAgent.toLowerCase(); - - if ( userAgent.indexOf( 'applewebkit' ) > -1 && id && id.charAt( 0 ) === '#' ) { - setTimeout( function() { - var target = document.getElementById( id.replace( '#', '' ) ); - - if ( target ) { - target.setAttribute( 'tabIndex', '0' ); - target.focus(); - } - }, 100 ); - } - } - - /** - * Toogle hover class for mobile devices. + * Toggle hover class for mobile devices. * * @since 5.3.1 * @@ -336,6 +303,11 @@ element.className += className; } + + var menuItemToggle = element.querySelector( 'a' ); + if ( className === 'hover' && menuItemToggle && menuItemToggle.hasAttribute( 'aria-expanded' ) ) { + menuItemToggle.setAttribute( 'aria-expanded', 'true' ); + } } /** @@ -366,6 +338,11 @@ element.className = classes.replace( /^[\s]+|[\s]+$/g, '' ); } + + var menuItemToggle = element.querySelector( 'a' ); + if ( className === 'hover' && menuItemToggle && menuItemToggle.hasAttribute( 'aria-expanded' ) ) { + menuItemToggle.setAttribute( 'aria-expanded', 'false' ); + } } /** diff --git a/src/js/_enqueues/lib/ajax-response.js b/src/js/_enqueues/lib/ajax-response.js index 38816f3c38a9e..d1a05ca52c8b8 100644 --- a/src/js/_enqueues/lib/ajax-response.js +++ b/src/js/_enqueues/lib/ajax-response.js @@ -18,7 +18,7 @@ window.wpAjax = jQuery.extend( { return r; }, parseAjaxResponse: function( x, r, e ) { // 1 = good, 0 = strange (bad data?), -1 = you lack permission. - var parsed = {}, re = jQuery('#' + r).empty(), err = ''; + var parsed = {}, re = jQuery('#' + r).empty(), err = '', noticeMessage = ''; if ( x && typeof x === 'object' && x.getElementsByTagName('wp_ajax') ) { parsed.responses = []; @@ -29,6 +29,12 @@ window.wpAjax = jQuery.extend( { response.data = jQuery( 'response_data', child ).text(); response.supplemental = {}; if ( !jQuery( 'supplemental', child ).children().each( function() { + + if ( this.nodeName === 'notice' ) { + noticeMessage += jQuery(this).text(); + return; + } + response.supplemental[this.nodeName] = jQuery(this).text(); } ).length ) { response.supplemental = false; } response.errors = []; @@ -46,13 +52,28 @@ window.wpAjax = jQuery.extend( { } ).length ) { response.errors = false; } parsed.responses.push( response ); } ); - if ( err.length ) { re.html( '
    ' + err + '
    ' ); } + if ( err.length ) { + re.html( '' ); + wp.a11y.speak( err ); + } else if ( noticeMessage.length ) { + re.html( ''); + jQuery(document).trigger( 'wp-updates-notice-added' ); + wp.a11y.speak( noticeMessage ); + } return parsed; } - if ( isNaN(x) ) { return !re.html('

    ' + x + '

    '); } - x = parseInt(x,10); - if ( -1 === x ) { return !re.html('

    ' + wpAjax.noPerm + '

    '); } - else if ( 0 === x ) { return !re.html('

    ' + wpAjax.broken + '

    '); } + if ( isNaN( x ) ) { + wp.a11y.speak( x ); + return ! re.html( '' ); + } + x = parseInt( x, 10 ); + if ( -1 === x ) { + wp.a11y.speak( wpAjax.noPerm ); + return ! re.html( '' ); + } else if ( 0 === x ) { + wp.a11y.speak( wpAjax.broken ); + return ! re.html( '' ); + } return true; }, invalidateForm: function ( selector ) { @@ -62,7 +83,7 @@ window.wpAjax = jQuery.extend( { selector = jQuery( selector ); return !wpAjax.invalidateForm( selector.find('.form-required').filter( function() { return jQuery('input:visible', this).val() === ''; } ) ).length; } -}, wpAjax || { noPerm: 'Sorry, you are not allowed to do that.', broken: 'Something went wrong.' } ); +}, wpAjax || { noPerm: 'Sorry, you are not allowed to do that.', broken: 'An error occurred while processing your request. Please refresh the page and try again.' } ); // Basic form validation. jQuery( function($){ diff --git a/src/js/_enqueues/lib/auth-check.js b/src/js/_enqueues/lib/auth-check.js index 44ff15a153410..ff64573639a25 100644 --- a/src/js/_enqueues/lib/auth-check.js +++ b/src/js/_enqueues/lib/auth-check.js @@ -159,12 +159,23 @@ setShowTimeout(); }); }).on( 'heartbeat-tick.wp-auth-check', function( e, data ) { - if ( 'wp-auth-check' in data ) { + if ( ! ( 'wp-auth-check' in data ) ) { + return; + } + + var showOrHide = function () { if ( ! data['wp-auth-check'] && wrap.hasClass( 'hidden' ) && ! tempHidden ) { show(); } else if ( data['wp-auth-check'] && ! wrap.hasClass( 'hidden' ) ) { hide(); } + }; + + // This is necessary due to a race condition where the heartbeat-tick event may fire before DOMContentLoaded. + if ( wrap ) { + showOrHide(); + } else { + $( showOrHide ); } }); diff --git a/src/js/_enqueues/lib/codemirror/.eslintrc.json b/src/js/_enqueues/lib/codemirror/.eslintrc.json new file mode 100644 index 0000000000000..231dcc0fb3e3c --- /dev/null +++ b/src/js/_enqueues/lib/codemirror/.eslintrc.json @@ -0,0 +1,17 @@ +{ + "overrides": [ + { + "files": [ "javascript-lint.js" ], + "parserOptions": { + "sourceType": "module", + "ecmaVersion": 2020 + } + }, + { + "files": [ "htmlhint-kses.js" ], + "parserOptions": { + "ecmaVersion": 2020 + } + } + ] +} diff --git a/src/js/_enqueues/lib/codemirror/htmlhint-kses.js b/src/js/_enqueues/lib/codemirror/htmlhint-kses.js new file mode 100644 index 0000000000000..e08c4c0f5b756 --- /dev/null +++ b/src/js/_enqueues/lib/codemirror/htmlhint-kses.js @@ -0,0 +1,47 @@ +/* global HTMLHint */ +/* eslint no-magic-numbers: ["error", { "ignore": [1] }] */ +HTMLHint.addRule( { + id: 'kses', + description: 'Element or attribute cannot be used.', + + /** + * Initialize. + * + * @this {import('htmlhint/types').Rule} + * @param {import('htmlhint').HTMLParser} parser - Parser. + * @param {import('htmlhint').Reporter} reporter - Reporter. + * @param {Record>} options - KSES options. + * @return {void} + */ + init: function ( parser, reporter, options ) { + 'use strict'; + + parser.addListener( 'tagstart', ( event ) => { + const tagName = event.tagName.toLowerCase(); + if ( ! options[ tagName ] ) { + reporter.error( + `Tag <${ event.tagName }> is not allowed.`, + event.line, + event.col, + this, + event.raw + ); + return; + } + + const allowedAttributes = options[ tagName ]; + const column = event.col + event.tagName.length + 1; + for ( const attribute of event.attrs ) { + if ( ! allowedAttributes[ attribute.name.toLowerCase() ] ) { + reporter.error( + `Tag attribute [${ attribute.raw }] is not allowed.`, + event.line, + column + attribute.index, + this, + attribute.raw + ); + } + } + } ); + }, +} ); diff --git a/src/js/_enqueues/lib/codemirror/javascript-lint.js b/src/js/_enqueues/lib/codemirror/javascript-lint.js new file mode 100644 index 0000000000000..2c96798a20ae3 --- /dev/null +++ b/src/js/_enqueues/lib/codemirror/javascript-lint.js @@ -0,0 +1,129 @@ +/** + * CodeMirror JavaScript linter. + * + * @since 7.0.0 + */ + +import CodeMirror from 'codemirror'; + +/** + * CodeMirror Lint Error. + * + * @see https://codemirror.net/5/doc/manual.html#addon_lint + * + * @typedef {Object} CodeMirrorLintError + * @property {string} message - Error message. + * @property {'error'} severity - Severity. + * @property {CodeMirror.Position} from - From position. + * @property {CodeMirror.Position} to - To position. + */ + +/** + * JSHint options supported by Espree. + * + * @see https://jshint.com/docs/options/ + * @see https://www.npmjs.com/package/espree#options + * + * @typedef {Object} SupportedJSHintOptions + * @property {import('espree').Options['ecmaVersion']} [esversion] - "This option is used to specify the ECMAScript version to which the code must adhere." + * @property {boolean} [es5] - "This option enables syntax first defined in the ECMAScript 5.1 specification. This includes allowing reserved keywords as object properties." + * @property {boolean} [es3] - "This option tells JSHint that your code needs to adhere to ECMAScript 3 specification. Use this option if you need your program to be executable in older browsers—such as Internet Explorer 6/7/8/9—and other legacy JavaScript environments." + * @property {boolean} [module] - "This option informs JSHint that the input code describes an ECMAScript 6 module. All module code is interpreted as strict mode code." + * @property {'implied'} [strict] - "This option requires the code to run in ECMAScript 5's strict mode." + * @property {string} [espreeModuleUrl] - The URL to the espree script module. + */ + +/** + * Validates JavaScript. + * + * @since 7.0.0 + * + * @param {string} text - Source. + * @param {SupportedJSHintOptions} options - Linting options. + * @returns {Promise} + */ +async function validator( text, options ) { + if ( ! options.espreeModuleUrl ) { + return []; + } + + const errors = /** @type {CodeMirrorLintError[]} */ []; + try { + const espree = await import( /* webpackIgnore: true */ options.espreeModuleUrl ); + espree.parse( text, { + ...getEspreeOptions( options ), + loc: true, + } ); + } catch ( error ) { + const enhancedError = /** @type {Error & { lineNumber?: number, column?: number }} */ ( error ); + if ( + // This is an `EnhancedSyntaxError` in Espree: . + error instanceof SyntaxError && + typeof enhancedError.lineNumber === 'number' && + typeof enhancedError.column === 'number' + ) { + const line = enhancedError.lineNumber - 1; + errors.push( /** @type {CodeMirrorLintError} */ ( { + message: error.message, + severity: 'error', + from: CodeMirror.Pos( line, enhancedError.column - 1 ), + to: CodeMirror.Pos( line, enhancedError.column ), + } ) ); + } else { + console.warn( '[CodeMirror] Unable to lint JavaScript:', error ); // jshint ignore:line + } + } + + return errors; +} + +CodeMirror.registerHelper( 'lint', 'javascript', validator ); + +/** + * Gets the options for Espree from the supported JSHint options. + * + * @since 7.0.0 + * + * @param {SupportedJSHintOptions} options - Linting options for JSHint. + * @return {{ + * ecmaVersion?: import('espree').Options['ecmaVersion'], + * sourceType?: 'module'|'script', + * ecmaFeatures?: { + * impliedStrict?: true + * } + * }} + */ +function getEspreeOptions( options ) { + /** @type {{ impliedStrict?: true }} */ + const ecmaFeatures = {}; + if ( options.strict === 'implied' ) { + ecmaFeatures.impliedStrict = true; + } + + return { + ecmaVersion: getEcmaVersion( options ), + sourceType: options.module ? 'module' : 'script', + ecmaFeatures, + }; +} + +/** + * Gets the ECMAScript version. + * + * @since 7.0.0 + * + * @param {SupportedJSHintOptions} options - Options. + * @return {import('espree').Options['ecmaVersion']} ECMAScript version. + */ +function getEcmaVersion( options ) { + if ( options.esversion ) { + return options.esversion; + } + if ( options.es5 ) { + return 5; + } + if ( options.es3 ) { + return 3; + } + return 'latest'; +} diff --git a/src/js/_enqueues/lib/comment-reply.js b/src/js/_enqueues/lib/comment-reply.js index 5d20672058a03..b916dc54d16de 100644 --- a/src/js/_enqueues/lib/comment-reply.js +++ b/src/js/_enqueues/lib/comment-reply.js @@ -59,7 +59,7 @@ window.addComment = ( function( window ) { * @since 5.1.1 */ function ready() { - // Initialise the events. + // Initialize the events. init(); // Set up a MutationObserver to check for comments loaded late. @@ -98,7 +98,7 @@ window.addComment = ( function( window ) { // Submit the comment form when the user types [Ctrl] or [Cmd] + [Enter]. var submitFormHandler = function( e ) { - if ( ( e.metaKey || e.ctrlKey ) && e.keyCode === 13 ) { + if ( ( e.metaKey || e.ctrlKey ) && e.keyCode === 13 && document.activeElement.tagName.toLowerCase() !== 'a' ) { commentFormElement.removeEventListener( 'keydown', submitFormHandler ); e.preventDefault(); // The submit button ID is 'submit' so we can't call commentFormElement.submit(). Click it instead. diff --git a/src/js/_enqueues/lib/dialog.js b/src/js/_enqueues/lib/dialog.js index 327d8768b8739..0da56f53ac18e 100644 --- a/src/js/_enqueues/lib/dialog.js +++ b/src/js/_enqueues/lib/dialog.js @@ -17,7 +17,7 @@ this._super(); // WebKit leaves focus in the TinyMCE editor unless we shift focus. - this.element.focus(); + this.element.trigger('focus'); this._trigger('refresh'); } }); diff --git a/src/js/_enqueues/lib/embed-template.js b/src/js/_enqueues/lib/embed-template.js index 86fb7d2c844f5..ae58e4e967d7a 100644 --- a/src/js/_enqueues/lib/embed-template.js +++ b/src/js/_enqueues/lib/embed-template.js @@ -18,6 +18,13 @@ }, '*' ); } + /** + * Send the height message to the parent window. + */ + function sendHeightMessage() { + sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); + } + function onLoad() { if ( loaded ) { return; @@ -138,13 +145,11 @@ } // Send this document's height to the parent (embedding) site. - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); + sendHeightMessage(); // Send the document's height again after the featured image has been loaded. if ( featured_image ) { - featured_image.addEventListener( 'load', function() { - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); - } ); + featured_image.addEventListener( 'load', sendHeightMessage ); } /** @@ -184,9 +189,36 @@ clearTimeout( resizing ); - resizing = setTimeout( function () { - sendEmbedMessage( 'height', Math.ceil( document.body.getBoundingClientRect().height ) ); - }, 100 ); + resizing = setTimeout( sendHeightMessage, 100 ); + } + + /** + * Message handler. + * + * @param {MessageEvent} event + */ + function onMessage( event ) { + var data = event.data; + + if ( ! data ) { + return; + } + + if ( event.source !== window.parent ) { + return; + } + + if ( ! ( data.secret || data.message ) ) { + return; + } + + if ( data.secret !== secret ) { + return; + } + + if ( 'ready' === data.message ) { + sendHeightMessage(); + } } /** @@ -212,5 +244,6 @@ document.addEventListener( 'DOMContentLoaded', onLoad, false ); window.addEventListener( 'load', onLoad, false ); window.addEventListener( 'resize', onResize, false ); + window.addEventListener( 'message', onMessage, false ); } })( window, document ); diff --git a/src/js/_enqueues/lib/emoji-loader.js b/src/js/_enqueues/lib/emoji-loader.js index ad39965900f08..561d3656a29e1 100644 --- a/src/js/_enqueues/lib/emoji-loader.js +++ b/src/js/_enqueues/lib/emoji-loader.js @@ -2,220 +2,435 @@ * @output wp-includes/js/wp-emoji-loader.js */ -( function( window, document, settings ) { - var src, ready, ii, tests; - - // Create a canvas element for testing native browser support of emoji. - var canvas = document.createElement( 'canvas' ); - var context = canvas.getContext && canvas.getContext( '2d' ); - - /** - * Checks if two sets of Emoji characters render the same visually. - * - * @since 4.9.0 - * - * @private - * - * @param {number[]} set1 Set of Emoji character codes. - * @param {number[]} set2 Set of Emoji character codes. - * - * @return {boolean} True if the two sets render the same. - */ - function emojiSetsRenderIdentically( set1, set2 ) { - var stringFromCharCode = String.fromCharCode; +/* eslint-env es6 */ - // Cleanup from previous test. - context.clearRect( 0, 0, canvas.width, canvas.height ); - context.fillText( stringFromCharCode.apply( this, set1 ), 0, 0 ); - var rendered1 = canvas.toDataURL(); +// Note: This is loaded as a script module, so there is no need for an IIFE to prevent pollution of the global scope. - // Cleanup from previous test. - context.clearRect( 0, 0, canvas.width, canvas.height ); - context.fillText( stringFromCharCode.apply( this, set2 ), 0, 0 ); - var rendered2 = canvas.toDataURL(); +/** + * Emoji Settings as exported in PHP via _print_emoji_detection_script(). + * @typedef WPEmojiSettings + * @type {object} + * @property {?object} source + * @property {?string} source.concatemoji + * @property {?string} source.twemoji + * @property {?string} source.wpemoji + */ - return rendered1 === rendered2; - } +const settings = /** @type {WPEmojiSettings} */ ( + JSON.parse( document.getElementById( 'wp-emoji-settings' ).textContent ) +); - /** - * Detects if the browser supports rendering emoji or flag emoji. - * - * Flag emoji are a single glyph made of two characters, so some browsers - * (notably, Firefox OS X) don't support them. - * - * @since 4.2.0 - * - * @private - * - * @param {string} type Whether to test for support of "flag" or "emoji". - * - * @return {boolean} True if the browser can render emoji, false if it cannot. - */ - function browserSupportsEmoji( type ) { - var isIdentical; +// For compatibility with other scripts that read from this global, in particular wp-includes/js/wp-emoji.js (source file: js/_enqueues/wp/emoji.js). +window._wpemojiSettings = settings; - if ( ! context || ! context.fillText ) { - return false; - } +/** + * Support tests. + * @typedef SupportTests + * @type {object} + * @property {?boolean} flag + * @property {?boolean} emoji + */ - /* - * Chrome on OS X added native emoji rendering in M41. Unfortunately, - * it doesn't work when the font is bolder than 500 weight. So, we - * check for bold rendering support to avoid invisible emoji in Chrome. - */ - context.textBaseline = 'top'; - context.font = '600 32px Arial'; - - switch ( type ) { - case 'flag': - /* - * Test for Transgender flag compatibility. This flag is shortlisted for the Emoji 13 spec, - * but has landed in Twemoji early, so we can add support for it, too. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (white flag emoji + transgender symbol). - */ - isIdentical = emojiSetsRenderIdentically( - [ 0x1F3F3, 0xFE0F, 0x200D, 0x26A7, 0xFE0F ], - [ 0x1F3F3, 0xFE0F, 0x200B, 0x26A7, 0xFE0F ] - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for UN flag compatibility. This is the least supported of the letter locale flags, - * so gives us an easy test for full support. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly ([U] + [N]). - */ - isIdentical = emojiSetsRenderIdentically( - [ 0xD83C, 0xDDFA, 0xD83C, 0xDDF3 ], - [ 0xD83C, 0xDDFA, 0x200B, 0xD83C, 0xDDF3 ] - ); - - if ( isIdentical ) { - return false; - } - - /* - * Test for English flag compatibility. England is a country in the United Kingdom, it - * does not have a two letter locale code but rather an five letter sub-division code. - * - * To test for support, we try to render it, and compare the rendering to how it would look if - * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). - */ - isIdentical = emojiSetsRenderIdentically( - [ 0xD83C, 0xDFF4, 0xDB40, 0xDC67, 0xDB40, 0xDC62, 0xDB40, 0xDC65, 0xDB40, 0xDC6E, 0xDB40, 0xDC67, 0xDB40, 0xDC7F ], - [ 0xD83C, 0xDFF4, 0x200B, 0xDB40, 0xDC67, 0x200B, 0xDB40, 0xDC62, 0x200B, 0xDB40, 0xDC65, 0x200B, 0xDB40, 0xDC6E, 0x200B, 0xDB40, 0xDC67, 0x200B, 0xDB40, 0xDC7F ] - ); - - return ! isIdentical; - case 'emoji': - /* - * Burning Love: Just a hunk, a hunk of burnin' love. - * - * To test for Emoji 13.1 support, try to render a new emoji: Heart on Fire! - * - * The Heart on Fire emoji is a ZWJ sequence combining ❤️ Red Heart, a Zero Width Joiner and 🔥 Fire. - * - * 0x2764, 0xfe0f == Red Heart emoji. - * 0x200D == Zero-Width Joiner (ZWJ) that links the two code points for the new emoji or - * 0x200B == Zero-Width Space (ZWS) that is rendered for clients not supporting the new emoji. - * 0xD83D, 0xDD25 == Fire. - * - * When updating this test for future Emoji releases, ensure that individual emoji that make up the - * sequence come from older emoji standards. - */ - isIdentical = emojiSetsRenderIdentically( - [0x2764, 0xfe0f, 0x200D, 0xD83D, 0xDD25], - [0x2764, 0xfe0f, 0x200B, 0xD83D, 0xDD25] - ); - - return ! isIdentical; +const sessionStorageKey = 'wpEmojiSettingsSupports'; +const tests = [ 'flag', 'emoji' ]; + +/** + * Checks whether the browser supports offloading to a Worker. + * + * @since 6.3.0 + * + * @private + * + * @returns {boolean} + */ +function supportsWorkerOffloading() { + return ( + typeof Worker !== 'undefined' && + typeof OffscreenCanvas !== 'undefined' && + typeof URL !== 'undefined' && + URL.createObjectURL && + typeof Blob !== 'undefined' + ); +} + +/** + * @typedef SessionSupportTests + * @type {object} + * @property {number} timestamp + * @property {SupportTests} supportTests + */ + +/** + * Get support tests from session. + * + * @since 6.3.0 + * + * @private + * + * @returns {?SupportTests} Support tests, or null if not set or older than 1 week. + */ +function getSessionSupportTests() { + try { + /** @type {SessionSupportTests} */ + const item = JSON.parse( + sessionStorage.getItem( sessionStorageKey ) + ); + if ( + typeof item === 'object' && + typeof item.timestamp === 'number' && + new Date().valueOf() < item.timestamp + 604800 && // Note: Number is a week in seconds. + typeof item.supportTests === 'object' + ) { + return item.supportTests; } + } catch ( e ) {} + return null; +} + +/** + * Persist the supports in session storage. + * + * @since 6.3.0 + * + * @private + * + * @param {SupportTests} supportTests Support tests. + */ +function setSessionSupportTests( supportTests ) { + try { + /** @type {SessionSupportTests} */ + const item = { + supportTests: supportTests, + timestamp: new Date().valueOf() + }; + + sessionStorage.setItem( + sessionStorageKey, + JSON.stringify( item ) + ); + } catch ( e ) {} +} + +/** + * Checks if two sets of Emoji characters render the same visually. + * + * This is used to determine if the browser is rendering an emoji with multiple data points + * correctly. set1 is the emoji in the correct form, using a zero-width joiner. set2 is the emoji + * in the incorrect form, using a zero-width space. If the two sets render the same, then the browser + * does not support the emoji correctly. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.9.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} set1 Set of Emoji to test. + * @param {string} set2 Set of Emoji to test. + * + * @return {boolean} True if the two sets render the same. + */ +function emojiSetsRenderIdentically( context, set1, set2 ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set1, 0, 0 ); + const rendered1 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); + + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( set2, 0, 0 ); + const rendered2 = new Uint32Array( + context.getImageData( + 0, + 0, + context.canvas.width, + context.canvas.height + ).data + ); + + return rendered1.every( ( rendered2Data, index ) => { + return rendered2Data === rendered2[ index ]; + } ); +} + +/** + * Checks if the center point of a single emoji is empty. + * + * This is used to determine if the browser is rendering an emoji with a single data point + * correctly. The center point of an incorrectly rendered emoji will be empty. A correctly + * rendered emoji will have a non-zero value at the center point. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.8.2 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} emoji Emoji to test. + * + * @return {boolean} True if the center point is empty. + */ +function emojiRendersEmptyCenterPoint( context, emoji ) { + // Cleanup from previous test. + context.clearRect( 0, 0, context.canvas.width, context.canvas.height ); + context.fillText( emoji, 0, 0 ); - return false; + // Test if the center point (16, 16) is empty (0,0,0,0). + const centerPoint = context.getImageData(16, 16, 1, 1); + for ( let i = 0; i < centerPoint.data.length; i++ ) { + if ( centerPoint.data[ i ] !== 0 ) { + // Stop checking the moment it's known not to be empty. + return false; + } } - /** - * Adds a script to the head of the document. - * - * @ignore - * - * @since 4.2.0 - * - * @param {Object} src The url where the script is located. - * @return {void} - */ - function addScript( src ) { - var script = document.createElement( 'script' ); + return true; +} + +/** + * Determines if the browser properly renders Emoji that Twemoji can supplement. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 4.2.0 + * + * @private + * + * @param {CanvasRenderingContext2D} context 2D Context. + * @param {string} type Whether to test for support of "flag" or "emoji". + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {boolean} True if the browser can render emoji, false if it cannot. + */ +function browserSupportsEmoji( context, type, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let isIdentical; - script.src = src; - script.defer = script.type = 'text/javascript'; - document.getElementsByTagName( 'head' )[0].appendChild( script ); + switch ( type ) { + case 'flag': + /* + * Test for Transgender flag compatibility. Added in Unicode 13. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (white flag emoji + transgender symbol). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDFF3\uFE0F\u200D\u26A7\uFE0F', // as a zero-width joiner sequence + '\uD83C\uDFF3\uFE0F\u200B\u26A7\uFE0F' // separated by a zero-width space + ); + + if ( isIdentical ) { + return false; + } + + /* + * Test for Sark flag compatibility. This is the least supported of the letter locale flags, + * so gives us an easy test for full support. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly ([C] + [Q]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + '\uD83C\uDDE8\uD83C\uDDF6', // as the sequence of two code points + '\uD83C\uDDE8\u200B\uD83C\uDDF6' // as the two code points separated by a zero-width space + ); + + if ( isIdentical ) { + return false; + } + + /* + * Test for English flag compatibility. England is a country in the United Kingdom, it + * does not have a two letter locale code but rather a five letter sub-division code. + * + * To test for support, we try to render it, and compare the rendering to how it would look if + * the browser doesn't render it correctly (black flag emoji + [G] + [B] + [E] + [N] + [G]). + */ + isIdentical = emojiSetsRenderIdentically( + context, + // as the flag sequence + '\uD83C\uDFF4\uDB40\uDC67\uDB40\uDC62\uDB40\uDC65\uDB40\uDC6E\uDB40\uDC67\uDB40\uDC7F', + // with each code point separated by a zero-width space + '\uD83C\uDFF4\u200B\uDB40\uDC67\u200B\uDB40\uDC62\u200B\uDB40\uDC65\u200B\uDB40\uDC6E\u200B\uDB40\uDC67\u200B\uDB40\uDC7F' + ); + + return ! isIdentical; + case 'emoji': + /* + * Is there a large, hairy, humanoid mythical creature living in the browser? + * + * To test for Emoji 17.0 support, try to render a new emoji: Hairy Creature. + * + * The hairy creature emoji is a single code point emoji. Testing for browser + * support required testing the center point of the emoji to see if it is empty. + * + * 0xD83E 0x1FAC8 (\uD83E\u1FAC8) == 🫈 Hairy creature. + * + * When updating this test, please ensure that the emoji is either a single code point + * or switch to using the emojiSetsRenderIdentically function and testing with a zero-width + * joiner vs a zero-width space. + */ + const notSupported = emojiRendersEmptyCenterPoint( context, '\uD83E\u1FAC8' ); + return ! notSupported; } - tests = Array( 'flag', 'emoji' ); + return false; +} - settings.supports = { - everything: true, - everythingExceptFlag: true - }; +/** + * Checks emoji support tests. + * + * This function may be serialized to run in a Worker. Therefore, it cannot refer to variables from the containing + * scope. Everything must be passed by parameters. + * + * @since 6.3.0 + * + * @private + * + * @param {string[]} tests Tests. + * @param {Function} browserSupportsEmoji Reference to browserSupportsEmoji function, needed due to minification. + * @param {Function} emojiSetsRenderIdentically Reference to emojiSetsRenderIdentically function, needed due to minification. + * @param {Function} emojiRendersEmptyCenterPoint Reference to emojiRendersEmptyCenterPoint function, needed due to minification. + * + * @return {SupportTests} Support tests. + */ +function testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ) { + let canvas; + if ( + typeof WorkerGlobalScope !== 'undefined' && + self instanceof WorkerGlobalScope + ) { + canvas = new OffscreenCanvas( 300, 150 ); // Dimensions are default for HTMLCanvasElement. + } else { + canvas = document.createElement( 'canvas' ); + } + + const context = canvas.getContext( '2d', { willReadFrequently: true } ); /* - * Tests the browser support for flag emojis and other emojis, and adjusts the - * support settings accordingly. + * Chrome on OS X added native emoji rendering in M41. Unfortunately, + * it doesn't work when the font is bolder than 500 weight. So, we + * check for bold rendering support to avoid invisible emoji in Chrome. */ - for( ii = 0; ii < tests.length; ii++ ) { - settings.supports[ tests[ ii ] ] = browserSupportsEmoji( tests[ ii ] ); + context.textBaseline = 'top'; + context.font = '600 32px Arial'; - settings.supports.everything = settings.supports.everything && settings.supports[ tests[ ii ] ]; + const supports = {}; + tests.forEach( ( test ) => { + supports[ test ] = browserSupportsEmoji( context, test, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + } ); + return supports; +} - if ( 'flag' !== tests[ ii ] ) { - settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && settings.supports[ tests[ ii ] ]; - } - } +/** + * Adds a script to the head of the document. + * + * @ignore + * + * @since 4.2.0 + * + * @param {string} src The url where the script is located. + * + * @return {void} + */ +function addScript( src ) { + const script = document.createElement( 'script' ); + script.src = src; + script.defer = true; + document.head.appendChild( script ); +} - settings.supports.everythingExceptFlag = settings.supports.everythingExceptFlag && ! settings.supports.flag; +settings.supports = { + everything: true, + everythingExceptFlag: true +}; - // Sets DOMReady to false and assigns a ready function to settings. - settings.DOMReady = false; - settings.readyCallback = function() { - settings.DOMReady = true; - }; +// Obtain the emoji support from the browser, asynchronously when possible. +new Promise( ( resolve ) => { + let supportTests = getSessionSupportTests(); + if ( supportTests ) { + resolve( supportTests ); + return; + } - // When the browser can not render everything we need to load a polyfill. - if ( ! settings.supports.everything ) { - ready = function() { - settings.readyCallback(); - }; + if ( supportsWorkerOffloading() ) { + try { + // Note that the functions are being passed as arguments due to minification. + const workerScript = + 'postMessage(' + + testEmojiSupports.toString() + + '(' + + [ + JSON.stringify( tests ), + browserSupportsEmoji.toString(), + emojiSetsRenderIdentically.toString(), + emojiRendersEmptyCenterPoint.toString() + ].join( ',' ) + + '));'; + const blob = new Blob( [ workerScript ], { + type: 'text/javascript' + } ); + const worker = new Worker( URL.createObjectURL( blob ), { name: 'wpTestEmojiSupports' } ); + worker.onmessage = ( event ) => { + supportTests = event.data; + setSessionSupportTests( supportTests ); + worker.terminate(); + resolve( supportTests ); + }; + return; + } catch ( e ) {} + } + supportTests = testEmojiSupports( tests, browserSupportsEmoji, emojiSetsRenderIdentically, emojiRendersEmptyCenterPoint ); + setSessionSupportTests( supportTests ); + resolve( supportTests ); +} ) + // Once the browser emoji support has been obtained from the session, finalize the settings. + .then( ( supportTests ) => { /* - * Cross-browser version of adding a dom ready event. + * Tests the browser support for flag emojis and other emojis, and adjusts the + * support settings accordingly. */ - if ( document.addEventListener ) { - document.addEventListener( 'DOMContentLoaded', ready, false ); - window.addEventListener( 'load', ready, false ); - } else { - window.attachEvent( 'onload', ready ); - document.attachEvent( 'onreadystatechange', function() { - if ( 'complete' === document.readyState ) { - settings.readyCallback(); - } - } ); - } + for ( const test in supportTests ) { + settings.supports[ test ] = supportTests[ test ]; - src = settings.source || {}; + settings.supports.everything = + settings.supports.everything && settings.supports[ test ]; - if ( src.concatemoji ) { - addScript( src.concatemoji ); - } else if ( src.wpemoji && src.twemoji ) { - addScript( src.twemoji ); - addScript( src.wpemoji ); + if ( 'flag' !== test ) { + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + settings.supports[ test ]; + } } - } -} )( window, document, window._wpemojiSettings ); + settings.supports.everythingExceptFlag = + settings.supports.everythingExceptFlag && + ! settings.supports.flag; + + // When the browser can not render everything we need to load a polyfill. + if ( ! settings.supports.everything ) { + const src = settings.source || {}; + + if ( src.concatemoji ) { + addScript( src.concatemoji ); + } else if ( src.wpemoji && src.twemoji ) { + addScript( src.twemoji ); + addScript( src.wpemoji ); + } + } + } ); diff --git a/src/js/_enqueues/lib/gallery.js b/src/js/_enqueues/lib/gallery.js index d493946c5b0f5..761b36db97a00 100644 --- a/src/js/_enqueues/lib/gallery.js +++ b/src/js/_enqueues/lib/gallery.js @@ -88,8 +88,6 @@ jQuery( function($) { } }); -jQuery(window).on( 'unload', function () { window.tinymce = window.tinyMCE = window.wpgallery = null; } ); // Cleanup. - /* gallery settings */ window.tinymce = null; diff --git a/src/js/_enqueues/lib/image-edit.js b/src/js/_enqueues/lib/image-edit.js index 18d3099f62cf5..d1603051d7078 100644 --- a/src/js/_enqueues/lib/image-edit.js +++ b/src/js/_enqueues/lib/image-edit.js @@ -11,7 +11,7 @@ var __ = wp.i18n.__; /** - * Contains all the methods to initialise and control the image editor. + * Contains all the methods to initialize and control the image editor. * * @namespace imageEdit */ @@ -22,25 +22,61 @@ _view : false, /** - * Handle crop tool clicks. + * Enable crop tool. */ - handleCropToolClick: function( postid, nonce, cropButton ) { + toggleCropTool: function( postid, nonce, cropButton ) { var img = $( '#image-preview-' + postid ), selection = this.iasapi.getSelection(); - // Ensure selection is available, otherwise reset to full image. - if ( isNaN( selection.x1 ) ) { - this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': img.innerWidth(), 'y2': img.innerHeight(), 'width': img.innerWidth(), 'height': img.innerHeight() } ); - selection = this.iasapi.getSelection(); + imageEdit.toggleControls( cropButton ); + var $el = $( cropButton ); + var state = ( $el.attr( 'aria-expanded' ) === 'true' ) ? 'true' : 'false'; + // Crop tools have been closed. + if ( 'false' === state ) { + // Cancel selection, but do not unset inputs. + this.iasapi.cancelSelection(); + imageEdit.setDisabled($('.imgedit-crop-clear'), 0); + } else { + imageEdit.setDisabled($('.imgedit-crop-clear'), 1); + // Get values from inputs to restore previous selection. + var startX = ( $( '#imgedit-start-x-' + postid ).val() ) ? $('#imgedit-start-x-' + postid).val() : 0; + var startY = ( $( '#imgedit-start-y-' + postid ).val() ) ? $('#imgedit-start-y-' + postid).val() : 0; + var width = ( $( '#imgedit-sel-width-' + postid ).val() ) ? $('#imgedit-sel-width-' + postid).val() : img.innerWidth(); + var height = ( $( '#imgedit-sel-height-' + postid ).val() ) ? $('#imgedit-sel-height-' + postid).val() : img.innerHeight(); + // Ensure selection is available, otherwise reset to full image. + if ( isNaN( selection.x1 ) ) { + this.setCropSelection( postid, { 'x1': startX, 'y1': startY, 'x2': width, 'y2': height, 'width': width, 'height': height } ); + selection = this.iasapi.getSelection(); + } + + // If we don't already have a selection, select the entire image. + if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) { + this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true ); + this.iasapi.setOptions( { show: true } ); + this.iasapi.update(); + } else { + this.iasapi.setSelection( startX, startY, width, height, true ); + this.iasapi.setOptions( { show: true } ); + this.iasapi.update(); + } } + }, - // If we don't already have a selection, select the entire image. - if ( 0 === selection.x1 && 0 === selection.y1 && 0 === selection.x2 && 0 === selection.y2 ) { - this.iasapi.setSelection( 0, 0, img.innerWidth(), img.innerHeight(), true ); - this.iasapi.setOptions( { show: true } ); - this.iasapi.update(); - } else { + /** + * Handle crop tool clicks. + */ + handleCropToolClick: function( postid, nonce, cropButton ) { + if ( cropButton.classList.contains( 'imgedit-crop-clear' ) ) { + this.iasapi.cancelSelection(); + imageEdit.setDisabled($('.imgedit-crop-apply'), 0); + + $('#imgedit-sel-width-' + postid).val(''); + $('#imgedit-sel-height-' + postid).val(''); + $('#imgedit-start-x-' + postid).val('0'); + $('#imgedit-start-y-' + postid).val('0'); + $('#imgedit-selection-' + postid).val(''); + } else { // Otherwise, perform the crop. imageEdit.crop( postid, nonce , cropButton ); } @@ -107,21 +143,27 @@ * @return {void} */ init : function(postid) { - var t = this, old = $('#image-editor-' + t.postid), - x = t.intval( $('#imgedit-x-' + postid).val() ), - y = t.intval( $('#imgedit-y-' + postid).val() ); + var t = this, old = $('#image-editor-' + t.postid); if ( t.postid !== postid && old.length ) { t.close(t.postid); } - t.hold.w = t.hold.ow = x; - t.hold.h = t.hold.oh = y; - t.hold.xy_ratio = x / y; t.hold.sizer = parseFloat( $('#imgedit-sizer-' + postid).val() ); t.postid = postid; $('#imgedit-response-' + postid).empty(); + $('#imgedit-panel-' + postid).on( 'keypress', function(e) { + var nonce = $( '#imgedit-nonce-' + postid ).val(); + if ( e.which === 26 && e.ctrlKey ) { + imageEdit.undo( postid, nonce ); + } + + if ( e.which === 25 && e.ctrlKey ) { + imageEdit.redo( postid, nonce ); + } + }); + $('#imgedit-panel-' + postid).on( 'keypress', 'input[type="text"]', function(e) { var k = e.keyCode; @@ -141,6 +183,29 @@ $( document ).on( 'image-editor-ui-ready', this.focusManager ); }, + /** + * Calculate the image size and save it to memory. + * + * @since 6.7.0 + * + * @memberof imageEdit + * + * @param {number} postid The post ID. + * + * @return {void} + */ + calculateImgSize: function( postid ) { + var t = this, + x = t.intval( $( '#imgedit-x-' + postid ).val() ), + y = t.intval( $( '#imgedit-y-' + postid ).val() ); + + t.hold.w = t.hold.ow = x; + t.hold.h = t.hold.oh = y; + t.hold.xy_ratio = x / y; + t.hold.sizer = parseFloat( $( '#imgedit-sizer-' + postid ).val() ); + t.currentCropSelection = null; + }, + /** * Toggles the wait/load icon in the editor. * @@ -169,6 +234,127 @@ } }, + /** + * Shows or hides image menu popup. + * + * @since 6.3.0 + * + * @memberof imageEdit + * + * @param {HTMLElement} el The activated control element. + * + * @return {boolean} Always returns false. + */ + togglePopup : function(el) { + var $el = $( el ); + var $targetEl = $( el ).attr( 'aria-controls' ); + var $target = $( '#' + $targetEl ); + $el + .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ); + // Open menu and set z-index to appear above image crop area if it is enabled. + $target + .toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' ).css( { 'z-index' : 200000 } ); + // Move focus to first item in menu when opening menu. + if ( 'true' === $el.attr( 'aria-expanded' ) ) { + $target.find( 'button' ).first().trigger( 'focus' ); + } + + return false; + }, + + /** + * Observes whether the popup should remain open based on focus position. + * + * @since 6.4.0 + * + * @memberof imageEdit + * + * @param {HTMLElement} el The activated control element. + * + * @return {boolean} Always returns false. + */ + monitorPopup : function() { + var $parent = document.querySelector( '.imgedit-rotate-menu-container' ); + var $toggle = document.querySelector( '.imgedit-rotate-menu-container .imgedit-rotate' ); + + setTimeout( function() { + var $focused = document.activeElement; + var $contains = $parent.contains( $focused ); + + // If $focused is defined and not inside the menu container, close the popup. + if ( $focused && ! $contains ) { + if ( 'true' === $toggle.getAttribute( 'aria-expanded' ) ) { + imageEdit.togglePopup( $toggle ); + } + } + }, 100 ); + + return false; + }, + + /** + * Navigate popup menu by arrow keys. + * + * @since 6.3.0 + * @since 6.7.0 Added the event parameter. + * + * @memberof imageEdit + * + * @param {Event} event The key or click event. + * @param {HTMLElement} el The current element. + * + * @return {boolean} Always returns false. + */ + browsePopup : function(event, el) { + var $el = $( el ); + var $collection = $( el ).parent( '.imgedit-popup-menu' ).find( 'button' ); + var $index = $collection.index( $el ); + var $prev = $index - 1; + var $next = $index + 1; + var $last = $collection.length; + if ( $prev < 0 ) { + $prev = $last - 1; + } + if ( $next === $last ) { + $next = 0; + } + var target = false; + if ( event.keyCode === 40 ) { + target = $collection.get( $next ); + } else if ( event.keyCode === 38 ) { + target = $collection.get( $prev ); + } + if ( target ) { + target.focus(); + event.preventDefault(); + } + + return false; + }, + + /** + * Close popup menu and reset focus on feature activation. + * + * @since 6.3.0 + * + * @memberof imageEdit + * + * @param {HTMLElement} el The current element. + * + * @return {boolean} Always returns false. + */ + closePopup : function(el) { + var $parent = $(el).parent( '.imgedit-popup-menu' ); + var $controlledID = $parent.attr( 'id' ); + var $target = $( 'button[aria-controls="' + $controlledID + '"]' ); + $target + .attr( 'aria-expanded', 'false' ).trigger( 'focus' ); + $parent + .toggleClass( 'imgedit-popup-menu-open' ).slideToggle( 'fast' ); + + return false; + }, + /** * Shows or hides the image edit help box. * @@ -189,6 +375,28 @@ return false; }, + /** + * Shows or hides image edit input fields when enabled. + * + * @since 6.3.0 + * + * @memberof imageEdit + * + * @param {HTMLElement} el The element to trigger the edit panel. + * + * @return {boolean} Always returns false. + */ + toggleControls : function(el) { + var $el = $( el ); + var $target = $( '#' + $el.attr( 'aria-controls' ) ); + $el + .attr( 'aria-expanded', 'false' === $el.attr( 'aria-expanded' ) ? 'true' : 'false' ); + $target + .parent( '.imgedit-group' ).toggleClass( 'imgedit-panel-active' ); + + return false; + }, + /** * Gets the value from the image edit target. * @@ -202,10 +410,16 @@ * @param {number} postid The post ID. * * @return {string} The value from the imagedit-save-target input field when available, - * or 'full' when not available. + * 'full' when not selected, or 'all' if it doesn't exist. */ - getTarget : function(postid) { - return $('input[name="imgedit-target-' + postid + '"]:checked', '#imgedit-save-target-' + postid).val() || 'full'; + getTarget : function( postid ) { + var element = $( '#imgedit-save-target-' + postid ); + + if ( element.length ) { + return element.find( 'input[name="imgedit-target-' + postid + '"]:checked' ).val() || 'full'; + } + + return 'all'; }, /** @@ -226,7 +440,8 @@ */ scaleChanged : function( postid, x, el ) { var w = $('#imgedit-scale-width-' + postid), h = $('#imgedit-scale-height-' + postid), - warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = ''; + warn = $('#imgedit-scale-warn-' + postid), w1 = '', h1 = '', + scaleBtn = $('#imgedit-scale-button'); if ( false === this.validateNumeric( el ) ) { return; @@ -242,8 +457,10 @@ if ( ( h1 && h1 > this.hold.oh ) || ( w1 && w1 > this.hold.ow ) ) { warn.css('visibility', 'visible'); + scaleBtn.prop('disabled', true); } else { warn.css('visibility', 'hidden'); + scaleBtn.prop('disabled', false); } }, @@ -328,7 +545,7 @@ for ( n in history ) { i = history[n]; if ( i.hasOwnProperty('c') ) { - op[n] = { 'c': { 'x': i.c.x, 'y': i.c.y, 'w': i.c.w, 'h': i.c.h } }; + op[n] = { 'c': { 'x': i.c.x, 'y': i.c.y, 'w': i.c.w, 'h': i.c.h, 'r': i.c.r } }; } else if ( i.hasOwnProperty('r') ) { op[n] = { 'r': i.r.r }; } else if ( i.hasOwnProperty('f') ) { @@ -402,12 +619,14 @@ } if ( $('#imgedit-history-' + postid).val() && $('#imgedit-undone-' + postid).val() === '0' ) { - $('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false); + $('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', false); } else { - $('input.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true); + $('button.imgedit-submit-btn', '#imgedit-panel-' + postid).prop('disabled', true); } + var successMessage = __( 'Image updated.' ); t.toggleEditor(postid, 0); + wp.a11y.speak( successMessage, 'assertive' ); }) .on( 'error', function() { var errorMessage = __( 'Could not load the preview image. Please reload the page and try again.' ); @@ -435,7 +654,7 @@ * * @return {boolean|void} Executes a post request that refreshes the page * when the action is performed. - * Returns false if a invalid action is given, + * Returns false if an invalid action is given, * or when the action cannot be performed. */ action : function(postid, nonce, action) { @@ -636,7 +855,7 @@ btn.removeClass( 'button-activated' ); spin.removeClass( 'is-active' ); } ); - // Initialise the Image Editor now that everything is ready. + // Initialize the Image Editor now that everything is ready. imageEdit.init( postid ); } ); @@ -661,6 +880,7 @@ if ( 'undefined' === typeof this.hold.sizer ) { this.init( postid ); } + this.calculateImgSize( postid ); this.initCrop(postid, img, parent); this.setCropSelection( postid, { 'x1': 0, 'y1': 0, 'x2': 0, 'y2': 0, 'width': img.innerWidth(), 'height': img.innerHeight() } ); @@ -689,7 +909,7 @@ elementToSetFocusTo = $( '.imgedit-wrap' ).find( ':tabbable:first' ); } - elementToSetFocusTo.trigger( 'focus' ); + elementToSetFocusTo.attr( 'tabindex', '-1' ).trigger( 'focus' ); }, 100 ); }, @@ -744,13 +964,16 @@ * * @return {void} */ - parent.children().on( 'mousedown, touchstart', function(e){ - var ratio = false, sel, defRatio; - - if ( e.shiftKey ) { - sel = t.iasapi.getSelection(); - defRatio = t.getSelRatio(postid); - ratio = ( sel && sel.width && sel.height ) ? sel.width + ':' + sel.height : defRatio; + parent.children().on( 'mousedown touchstart', function(e) { + var ratio = false, + sel = t.iasapi.getSelection(), + cx = t.intval( $( '#imgedit-crop-width-' + postid ).val() ), + cy = t.intval( $( '#imgedit-crop-height-' + postid ).val() ); + + if ( cx && cy ) { + ratio = t.getSelRatio( postid ); + } else if ( e.shiftKey && sel && sel.width && sel.height ) { + ratio = sel.width + ':' + sel.height; } t.iasapi.setOptions({ @@ -768,6 +991,8 @@ */ onSelectStart: function() { imageEdit.setDisabled($('#imgedit-crop-sel-' + postid), 1); + imageEdit.setDisabled($('.imgedit-crop-clear'), 1); + imageEdit.setDisabled($('.imgedit-crop-apply'), 1); }, /** * Event triggered when the selection is ended. @@ -781,6 +1006,9 @@ */ onSelectEnd: function(img, c) { imageEdit.setCropSelection(postid, c); + if ( ! $('#imgedit-crop > *').is(':visible') ) { + imageEdit.toggleControls($('.imgedit-crop.button')); + } }, /** @@ -794,9 +1022,17 @@ * @return {void} */ onSelectChange: function(img, c) { - var sizer = imageEdit.hold.sizer; - selW.val( imageEdit.round(c.width / sizer) ); - selH.val( imageEdit.round(c.height / sizer) ); + var sizer = imageEdit.hold.sizer, + oldSel = imageEdit.currentCropSelection; + + if ( oldSel != null && oldSel.width == c.width && oldSel.height == c.height ) { + return; + } + + selW.val( Math.min( imageEdit.hold.w, imageEdit.round( c.width / sizer ) ) ); + selH.val( Math.min( imageEdit.hold.h, imageEdit.round( c.height / sizer ) ) ); + + t.currentCropSelection = c; } }); }, @@ -814,7 +1050,11 @@ * @return {boolean} */ setCropSelection : function(postid, c) { - var sel; + var sel, + selW = $( '#imgedit-sel-width-' + postid ), + selH = $( '#imgedit-sel-height-' + postid ), + sizer = this.hold.sizer, + hold = this.hold; c = c || 0; @@ -823,11 +1063,21 @@ this.setDisabled( $( '#imgedit-crop-sel-' + postid ), 1 ); $('#imgedit-sel-width-' + postid).val(''); $('#imgedit-sel-height-' + postid).val(''); + $('#imgedit-start-x-' + postid).val('0'); + $('#imgedit-start-y-' + postid).val('0'); $('#imgedit-selection-' + postid).val(''); return false; } - sel = { 'x': c.x1, 'y': c.y1, 'w': c.width, 'h': c.height }; + // adjust the selection within the bounds of the image on 100% scale + var excessW = hold.w - ( Math.round( c.x1 / sizer ) + parseInt( selW.val() ) ); + var excessH = hold.h - ( Math.round( c.y1 / sizer ) + parseInt( selH.val() ) ); + var x = Math.round( c.x1 / sizer ) + Math.min( 0, excessW ); + var y = Math.round( c.y1 / sizer ) + Math.min( 0, excessH ); + + // use 100% scaling to prevent rounding errors + sel = { 'r': 1, 'x': x, 'y': y, 'w': selW.val(), 'h': selH.val() }; + this.setDisabled($('.imgedit-crop', '#imgedit-panel-' + postid), 1); $('#imgedit-selection-' + postid).val( JSON.stringify(sel) ); }, @@ -953,8 +1203,13 @@ if ( $(t).hasClass('disabled') ) { return false; } - + this.closePopup(t); this.addStep({ 'r': { 'r': angle, 'fw': this.hold.h, 'fh': this.hold.w }}, postid, nonce); + + // Clear the selection fields after rotating. + $( '#imgedit-sel-width-' + postid ).val( '' ); + $( '#imgedit-sel-height-' + postid ).val( '' ); + this.currentCropSelection = null; }, /** @@ -975,8 +1230,13 @@ if ( $(t).hasClass('disabled') ) { return false; } - + this.closePopup(t); this.addStep({ 'f': { 'f': axis, 'fw': this.hold.w, 'fh': this.hold.h }}, postid, nonce); + + // Clear the selection fields after flipping. + $( '#imgedit-sel-width-' + postid ).val( '' ); + $( '#imgedit-sel-height-' + postid ).val( '' ); + this.currentCropSelection = null; }, /** @@ -1009,8 +1269,11 @@ } // Clear the selection fields after cropping. - $('#imgedit-sel-width-' + postid).val(''); - $('#imgedit-sel-height-' + postid).val(''); + $( '#imgedit-sel-width-' + postid ).val( '' ); + $( '#imgedit-sel-height-' + postid ).val( '' ); + $( '#imgedit-start-x-' + postid ).val( '0' ); + $( '#imgedit-start-y-' + postid ).val( '0' ); + this.currentCropSelection = null; }, /** @@ -1094,10 +1357,14 @@ */ setNumSelection : function( postid, el ) { var sel, elX = $('#imgedit-sel-width-' + postid), elY = $('#imgedit-sel-height-' + postid), + elX1 = $('#imgedit-start-x-' + postid), elY1 = $('#imgedit-start-y-' + postid), + xS = this.intval( elX1.val() ), yS = this.intval( elY1.val() ), x = this.intval( elX.val() ), y = this.intval( elY.val() ), img = $('#image-preview-' + postid), imgh = img.height(), imgw = img.width(), sizer = this.hold.sizer, x1, y1, x2, y2, ias = this.iasapi; + this.currentCropSelection = null; + if ( false === this.validateNumeric( el ) ) { return; } @@ -1112,27 +1379,28 @@ return false; } - if ( x && y && ( sel = ias.getSelection() ) ) { + if ( ( ( x && y ) || ( xS && yS ) ) && ( sel = ias.getSelection() ) ) { x2 = sel.x1 + Math.round( x * sizer ); y2 = sel.y1 + Math.round( y * sizer ); - x1 = sel.x1; - y1 = sel.y1; + x1 = ( xS === sel.x1 ) ? sel.x1 : Math.round( xS * sizer ); + y1 = ( yS === sel.y1 ) ? sel.y1 : Math.round( yS * sizer ); if ( x2 > imgw ) { x1 = 0; x2 = imgw; - elX.val( Math.round( x2 / sizer ) ); + elX.val( Math.min( this.hold.w, Math.round( x2 / sizer ) ) ); } if ( y2 > imgh ) { y1 = 0; y2 = imgh; - elY.val( Math.round( y2 / sizer ) ); + elY.val( Math.min( this.hold.h, Math.round( y2 / sizer ) ) ); } ias.setSelection( x1, y1, x2, y2 ); ias.update(); this.setCropSelection(postid, ias.getSelection()); + this.currentCropSelection = ias.getSelection(); } }, @@ -1202,10 +1470,21 @@ if ( r > h ) { r = h; + var errorMessage = __( 'Selected crop ratio exceeds the boundaries of the image. Try a different ratio.' ); + + $( '#imgedit-crop-' + postid ) + .prepend( '' ); + + wp.a11y.speak( errorMessage, 'assertive' ); if ( n ) { - $('#imgedit-crop-height-' + postid).val(''); + $('#imgedit-crop-height-' + postid).val( '' ); } else { - $('#imgedit-crop-width-' + postid).val(''); + $('#imgedit-crop-width-' + postid).val( ''); + } + } else { + var error = $( '#imgedit-crop-' + postid ).find( '.notice-error' ); + if ( 'undefined' !== typeof( error ) ) { + error.remove(); } } @@ -1228,7 +1507,7 @@ * void when it is. */ validateNumeric: function( el ) { - if ( ! this.intval( $( el ).val() ) ) { + if ( false === this.intval( $( el ).val() ) ) { $( el ).val( '' ); return false; } diff --git a/src/js/_enqueues/lib/link.js b/src/js/_enqueues/lib/link.js index 89adf33796588..a2500a9069648 100644 --- a/src/js/_enqueues/lib/link.js +++ b/src/js/_enqueues/lib/link.js @@ -105,6 +105,7 @@ var ed, $body = $( document.body ); + $( '#wpwrap' ).attr( 'aria-hidden', 'true' ); $body.addClass( 'modal-open' ); wpLink.modalOpen = true; @@ -161,7 +162,7 @@ if ( wpLink.isMCE() ) { wpLink.mceRefresh( url, text ); } else { - // For the Text editor the "Link text" field is always shown. + // For the Code editor the "Link text" field is always shown. if ( ! inputs.wrap.hasClass( 'has-text-field' ) ) { inputs.wrap.addClass( 'has-text-field' ); } @@ -281,6 +282,7 @@ close: function( reset ) { $( document.body ).removeClass( 'modal-open' ); + $( '#wpwrap' ).removeAttr( 'aria-hidden' ); wpLink.modalOpen = false; if ( reset !== 'noReset' ) { @@ -321,7 +323,7 @@ var html = ''; diff --git a/src/js/_enqueues/lib/lists.js b/src/js/_enqueues/lib/lists.js index aa6796cc0f35d..d7e888c85b9ab 100644 --- a/src/js/_enqueues/lib/lists.js +++ b/src/js/_enqueues/lib/lists.js @@ -218,7 +218,7 @@ wpList = { * Example 1: data-wp-lists="delete:the-comment-list:comment-{comment_ID}:66cc66:unspam=1" * Example 2: data-wp-lists="dim:the-comment-list:comment-{comment_ID}:unapproved:e7e7d3:e7e7d3:new=approved" * - * Returns an unassociated array with the following data: + * Returns an unassociative array with the following data: * data[0] - Data identifier: 'list', 'add', 'delete', or 'dim'. * data[1] - ID of the corresponding list. If data[0] is 'list', the type of list ('comment', 'category', etc). * data[2] - ID of the parent element of all inputs necessary for the request. @@ -747,7 +747,7 @@ wpList = { return list.wpList.add( this ); } ); - $element.on( 'click', 'a[data-wp-lists^="add:' + list.id + ':"], input[data-wp-lists^="add:' + list.id + ':"]', function() { + $element.on( 'click', '[data-wp-lists^="add:' + list.id + ':"], input[data-wp-lists^="add:' + list.id + ':"]', function() { return list.wpList.add( this ); } ); diff --git a/src/js/_enqueues/lib/nav-menu.js b/src/js/_enqueues/lib/nav-menu.js index 655d904bc316c..79917c8447f1a 100644 --- a/src/js/_enqueues/lib/nav-menu.js +++ b/src/js/_enqueues/lib/nav-menu.js @@ -216,6 +216,8 @@ checkboxes.prop( 'checked', false ); t.find( '.button-controls .select-all' ).prop( 'checked', false ); t.find( '.button-controls .spinner' ).removeClass( 'is-active' ); + t.updateParentDropdown(); + t.updateOrderDropdown(); }); }); }, @@ -288,6 +290,121 @@ }); }); return this; + }, + updateParentDropdown : function() { + return this.each(function(){ + var menuItems = $( '#menu-to-edit li' ), + parentDropdowns = $( '.edit-menu-item-parent' ); + + $.each( parentDropdowns, function() { + var parentDropdown = $( this ), + currentItemID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-db-id' ).val() ), + currentParentID = parseInt( parentDropdown.closest( 'li.menu-item' ).find( '.menu-item-data-parent-id' ).val() ), + currentItem = parentDropdown.closest( 'li.menu-item' ), + currentMenuItemChild = currentItem.childMenuItems(), + excludeMenuItem = /** @type {number[]} */ [ currentItemID ]; + + parentDropdown.empty(); + + if ( currentMenuItemChild.length > 0 ) { + $.each( currentMenuItemChild, function(){ + var childItem = $(this), + childID = parseInt( childItem.find( '.menu-item-data-db-id' ).val() ); + + excludeMenuItem.push( childID ); + }); + } + + parentDropdown.append( + $( '
  • ' ); + const p = $( '

    ', { text: noResults } ); + li.append( p ); + $('.categorychecklist', panel).empty().append( li ); $( '.spinner', panel ).removeClass( 'is-active' ); wrapper.addClass( 'has-no-menu-item' ); + wp.a11y.speak( noResults, 'assertive' ); return; } @@ -1482,6 +1815,7 @@ }); $('.categorychecklist', panel).html( $items ); + wp.a11y.speak( wp.i18n.sprintf( wp.i18n.__( '%d Search Results Found' ), $items.length ), 'assertive' ); $( '.spinner', panel ).removeClass( 'is-active' ); wrapper.removeClass( 'has-no-menu-item' ); @@ -1513,6 +1847,9 @@ ins.removeClass( 'menu-instructions-inactive' ); } api.refreshAdvancedAccessibility(); + wp.a11y.speak( menus.itemRemoved ); + $( '#menu-to-edit' ).updateParentDropdown(); + $( '#menu-to-edit' ).updateOrderDropdown(); }); }, @@ -1549,4 +1886,19 @@ }); }); + // Show bulk action. + $( document ).on( 'menu-item-added', function() { + if ( ! $( '.bulk-actions' ).is( ':visible' ) ) { + $( '.bulk-actions' ).show(); + } + } ); + + // Hide bulk action. + $( document ).on( 'menu-removing-item', function( e, el ) { + var menuElement = $( el ).parents( '#menu-to-edit' ); + if ( menuElement.find( 'li' ).length === 1 && $( '.bulk-actions' ).is( ':visible' ) ) { + $( '.bulk-actions' ).hide(); + } + } ); + })(jQuery); diff --git a/src/js/_enqueues/vendor/README.md b/src/js/_enqueues/vendor/README.md index 5230e66953363..fef43e3ec205a 100644 --- a/src/js/_enqueues/vendor/README.md +++ b/src/js/_enqueues/vendor/README.md @@ -1,6 +1,6 @@ # src/js/enqueues/vendor -In this directory you'll find vendor JavaScript packages that cannot be installed through NPM, but are included in WordPress. Below we've documented the sources for those packages. +In this directory you'll find vendor JavaScript packages that cannot be installed through npm, but are included in WordPress. Below we've documented the sources for those packages. ## Folder dependencies @@ -10,8 +10,7 @@ In this directory you'll find vendor JavaScript packages that cannot be installe - jcrop: https://github.com/tapmodo/Jcrop - mediaelement: https://github.com/mediaelement/mediaelement - plupload: https://github.com/moxiecode/plupload -- swfupload: https://github.com/WordPress/secure-swfupload -- thickbox: http://codylindley.com/thickbox/ +- thickbox: https://codylindley.com/thickbox/ - tinymce: https://www.tiny.cloud/get-tiny/self-hosted/ - Download "TinyMCE Dev Package". This package is needed because it includes the `compat3x` plugin. @@ -62,14 +61,13 @@ In this directory you'll find vendor JavaScript packages that cannot be installe - farbtastic: https://github.com/mattfarina/farbtastic - iris: https://github.com/Automattic/Iris - json2: https://github.com/douglascrockford/JSON-js -- jquery/jquery.color: https://github.com/jquery/jquery-color. Package is on NPM but not published by maintainer. +- jquery/jquery.color: https://github.com/jquery/jquery-color. Package is on npm but not published by maintainer. - jquery/jquery.hotkeys: https://github.com/tzuryby/jquery.hotkeys -- jquery/jquery.masonry: Old version for BC purposes, can't include two versions with NPM. The newer version is included through NPM and built to `wp-includes/js/masonry.min.js` +- jquery/jquery.masonry: Old version for BC purposes, can't include two versions with npm. The newer version is included through npm and built to `wp-includes/js/masonry.min.js` - jquery/jquery.query: https://github.com/blairmitchelmore/jquery.plugins/blob/master/jquery.query.js - jquery/jquery.schedule: https://github.com/rse/jquery-schedule - jquery/jquery.serializeobject: https://github.com/cowboy/jquery-misc/blob/master/jquery.ba-serializeobject.js - jquery/jquery.table-hotkeys: WP version can be downloaded at https://code.google.com/archive/p/js-hotkeys/downloads?page=2. A newer version is available at https://github.com/jeresig/jquery.hotkeys. - jquery/jquery.ui.touch-punch.js https://github.com/furf/jquery-ui-touch-punch/blob/master/jquery.ui.touch-punch.js -- swfobject: https://github.com/swfobject/swfobject - tw-sack: https://github.com/abritinthebay/simpleajaxcodekit - zxcvbn: https://github.com/dropbox/zxcvbn cannot automatically be installed as the frequency lists need to be manually ROT13 transformed. diff --git a/src/js/_enqueues/vendor/codemirror/codemirror.min.css b/src/js/_enqueues/vendor/codemirror/codemirror.min.css deleted file mode 100644 index 35a247a2b21b6..0000000000000 --- a/src/js/_enqueues/vendor/codemirror/codemirror.min.css +++ /dev/null @@ -1,11 +0,0 @@ -/*! This file is auto-generated from CodeMirror - github:codemirror/CodeMirror#ee20357d279bf9edfed0047d3bf2a75b5f0a040f - -CodeMirror, copyright (c) by Marijn Haverbeke and others -Distributed under an MIT license: http://codemirror.net/LICENSE - -This is CodeMirror (http://codemirror.net), a code editor -implemented in JavaScript on top of the browser's DOM. - -You can find some technical background for some of the code below -at http://marijnhaverbeke.nl/blog/#cm-internals . -*/.CodeMirror-Tern-tooltip,.CodeMirror-hints{-webkit-box-shadow:2px 3px 5px rgba(0,0,0,.2)}.CodeMirror{font-family:monospace;height:300px;color:#000;direction:ltr}.CodeMirror-lines{padding:4px 0}.CodeMirror pre{padding:0 4px}.CodeMirror-gutter-filler,.CodeMirror-scrollbar-filler{background-color:#fff}.CodeMirror-gutters{border-right:1px solid #ddd;background-color:#f7f7f7;white-space:nowrap}.CodeMirror-linenumber{padding:0 3px 0 5px;min-width:20px;text-align:right;color:#999;white-space:nowrap}.CodeMirror-guttermarker{color:#000}.CodeMirror-guttermarker-subtle{color:#999}.CodeMirror-cursor{border-left:1px solid #000;border-right:none;width:0}.CodeMirror div.CodeMirror-secondarycursor{border-left:1px solid silver}.cm-fat-cursor .CodeMirror-cursor{width:auto;border:0!important;background:#7e7}.cm-fat-cursor div.CodeMirror-cursors{z-index:1}.cm-animate-fat-cursor{width:auto;border:0;-webkit-animation:blink 1.06s steps(1) infinite;-moz-animation:blink 1.06s steps(1) infinite;animation:blink 1.06s steps(1) infinite;background-color:#7e7}@-moz-keyframes blink{50%{background-color:transparent}}@-webkit-keyframes blink{50%{background-color:transparent}}@keyframes blink{50%{background-color:transparent}}.cm-tab{display:inline-block;text-decoration:inherit}.CodeMirror-rulers{position:absolute;left:0;right:0;top:-50px;bottom:-20px;overflow:hidden}.CodeMirror-ruler{border-left:1px solid #ccc;top:0;bottom:0;position:absolute}.cm-s-default .cm-header{color:#00f}.cm-s-default .cm-quote{color:#090}.cm-negative{color:#d44}.cm-positive{color:#292}.cm-header,.cm-strong{font-weight:700}.cm-em{font-style:italic}.cm-link{text-decoration:underline}.cm-strikethrough{text-decoration:line-through}.cm-s-default .cm-keyword{color:#708}.cm-s-default .cm-atom{color:#219}.cm-s-default .cm-number{color:#164}.cm-s-default .cm-def{color:#00f}.cm-s-default .cm-variable-2{color:#05a}.cm-s-default .cm-type,.cm-s-default .cm-variable-3{color:#085}.cm-s-default .cm-comment{color:#a50}.cm-s-default .cm-string{color:#a11}.cm-s-default .cm-string-2{color:#f50}.cm-s-default .cm-meta,.cm-s-default .cm-qualifier{color:#555}.cm-s-default .cm-builtin{color:#30a}.cm-s-default .cm-bracket{color:#997}.cm-s-default .cm-tag{color:#170}.cm-s-default .cm-attribute{color:#00c}.cm-s-default .cm-hr{color:#999}.cm-s-default .cm-link{color:#00c}.cm-invalidchar,.cm-s-default .cm-error{color:red}.CodeMirror-composing{border-bottom:2px solid}div.CodeMirror span.CodeMirror-matchingbracket{color:#0f0}div.CodeMirror span.CodeMirror-nonmatchingbracket{color:#f22}.CodeMirror-matchingtag{background:rgba(255,150,0,.3)}.CodeMirror-activeline-background{background:#e8f2ff}.CodeMirror{position:relative;overflow:hidden;background:#fff}.CodeMirror-scroll{overflow:scroll!important;margin-bottom:-30px;margin-right:-30px;padding-bottom:30px;height:100%;outline:0;position:relative}.CodeMirror-sizer{position:relative;border-right:30px solid transparent}.CodeMirror-gutter-filler,.CodeMirror-hscrollbar,.CodeMirror-scrollbar-filler,.CodeMirror-vscrollbar{position:absolute;z-index:6;display:none}.CodeMirror-vscrollbar{right:0;top:0;overflow-x:hidden;overflow-y:scroll}.CodeMirror-hscrollbar{bottom:0;left:0;overflow-y:hidden;overflow-x:scroll}.CodeMirror-scrollbar-filler{right:0;bottom:0}.CodeMirror-gutter-filler{left:0;bottom:0}.CodeMirror-gutters{position:absolute;left:0;top:0;min-height:100%;z-index:3}.CodeMirror-gutter{white-space:normal;height:100%;display:inline-block;vertical-align:top;margin-bottom:-30px}.CodeMirror-gutter-wrapper{position:absolute;z-index:4;background:0 0!important;border:none!important}.CodeMirror-gutter-background{position:absolute;top:0;bottom:0;z-index:4}.CodeMirror-gutter-elt{position:absolute;cursor:default;z-index:4}.CodeMirror-gutter-wrapper ::selection{background-color:transparent}.CodeMirror-gutter-wrapper ::-moz-selection{background-color:transparent}.CodeMirror-lines{cursor:text;min-height:1px}.CodeMirror pre{-moz-border-radius:0;-webkit-border-radius:0;border-radius:0;border-width:0;background:0 0;font-family:inherit;font-size:inherit;margin:0;white-space:pre;word-wrap:normal;line-height:inherit;color:inherit;z-index:2;position:relative;overflow:visible;-webkit-tap-highlight-color:transparent;-webkit-font-variant-ligatures:contextual;font-variant-ligatures:contextual}.CodeMirror-wrap pre{word-wrap:break-word;white-space:pre-wrap;word-break:normal}.CodeMirror-linebackground{position:absolute;left:0;right:0;top:0;bottom:0;z-index:0}.CodeMirror-linewidget{position:relative;z-index:2;overflow:auto}.CodeMirror-rtl pre{direction:rtl}.CodeMirror-code{outline:0}.CodeMirror-gutter,.CodeMirror-gutters,.CodeMirror-linenumber,.CodeMirror-scroll,.CodeMirror-sizer{-moz-box-sizing:content-box;box-sizing:content-box}.CodeMirror-measure{position:absolute;width:100%;height:0;overflow:hidden;visibility:hidden}.CodeMirror-cursor{position:absolute;pointer-events:none}.CodeMirror-measure pre{position:static}div.CodeMirror-cursors{visibility:hidden;position:relative;z-index:3}.CodeMirror-focused div.CodeMirror-cursors,div.CodeMirror-dragcursors{visibility:visible}.CodeMirror-selected{background:#d9d9d9}.CodeMirror-focused .CodeMirror-selected{background:#d7d4f0}.CodeMirror-crosshair{cursor:crosshair}.CodeMirror-line::selection,.CodeMirror-line>span::selection,.CodeMirror-line>span>span::selection{background:#d7d4f0}.CodeMirror-line::-moz-selection,.CodeMirror-line>span::-moz-selection,.CodeMirror-line>span>span::-moz-selection{background:#d7d4f0}.cm-searching{background-color:#ffa;background-color:rgba(255,255,0,.4)}.cm-force-border{padding-right:.1px}@media print{.CodeMirror div.CodeMirror-cursors{visibility:hidden}}.cm-tab-wrap-hack:after{content:''}span.CodeMirror-selectedtext{background:0 0}.CodeMirror-hints{position:absolute;z-index:10;overflow:hidden;list-style:none;margin:0;padding:2px;-moz-box-shadow:2px 3px 5px rgba(0,0,0,.2);box-shadow:2px 3px 5px rgba(0,0,0,.2);border-radius:3px;border:1px solid silver;background:#fff;font-size:90%;font-family:monospace;max-height:20em;overflow-y:auto}.CodeMirror-hint{margin:0;padding:0 4px;border-radius:2px;white-space:pre;color:#000;cursor:pointer}li.CodeMirror-hint-active{background:#08f;color:#fff}.CodeMirror-lint-markers{width:16px}.CodeMirror-lint-tooltip{background-color:#ffd;border:1px solid #000;border-radius:4px;color:#000;font-family:monospace;font-size:10pt;overflow:hidden;padding:2px 5px;position:fixed;white-space:pre;white-space:pre-wrap;z-index:100;max-width:600px;opacity:0;transition:opacity .4s;-moz-transition:opacity .4s;-webkit-transition:opacity .4s;-o-transition:opacity .4s;-ms-transition:opacity .4s}.CodeMirror-lint-mark-error,.CodeMirror-lint-mark-warning{background-position:left bottom;background-repeat:repeat-x}.CodeMirror-lint-mark-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg==)}.CodeMirror-lint-mark-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-error,.CodeMirror-lint-marker-warning{background-position:center center;background-repeat:no-repeat;cursor:pointer;display:inline-block;height:16px;width:16px;vertical-align:middle;position:relative}.CodeMirror-lint-message-error,.CodeMirror-lint-message-warning{padding-left:18px;background-position:top left;background-repeat:no-repeat}.CodeMirror-lint-marker-error,.CodeMirror-lint-message-error{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII=)}.CodeMirror-lint-marker-warning,.CodeMirror-lint-message-warning{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII=)}.CodeMirror-lint-marker-multiple{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC);background-repeat:no-repeat;background-position:right bottom;width:100%;height:100%}.CodeMirror-dialog{position:absolute;left:0;right:0;background:inherit;z-index:15;padding:.1em .8em;overflow:hidden;color:inherit}.CodeMirror-dialog-top{border-bottom:1px solid #eee;top:0}.CodeMirror-dialog-bottom{border-top:1px solid #eee;bottom:0}.CodeMirror-dialog input{border:none;outline:0;background:0 0;width:20em;color:inherit;font-family:monospace}.CodeMirror-dialog button{font-size:70%}.CodeMirror-fullscreen{position:fixed;top:0;left:0;right:0;bottom:0;height:auto;z-index:9}.CodeMirror-foldmarker{color:#00f;text-shadow:#b9f 1px 1px 2px,#b9f -1px -1px 2px,#b9f 1px -1px 2px,#b9f -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.CodeMirror-foldgutter{width:.7em}.CodeMirror-foldgutter-folded,.CodeMirror-foldgutter-open{cursor:pointer}.CodeMirror-foldgutter-open:after{content:"\25BE"}.CodeMirror-foldgutter-folded:after{content:"\25B8"}.CodeMirror-merge{position:relative;border:1px solid #ddd;white-space:pre}.CodeMirror-merge,.CodeMirror-merge .CodeMirror{height:350px}.CodeMirror-merge-2pane .CodeMirror-merge-pane{width:47%}.CodeMirror-merge-2pane .CodeMirror-merge-gap{width:6%}.CodeMirror-merge-3pane .CodeMirror-merge-pane{width:31%}.CodeMirror-merge-3pane .CodeMirror-merge-gap{width:3.5%}.CodeMirror-merge-pane{display:inline-block;white-space:normal;vertical-align:top}.CodeMirror-merge-pane-rightmost{position:absolute;right:0;z-index:1}.CodeMirror-merge-gap{z-index:2;display:inline-block;height:100%;-moz-box-sizing:border-box;box-sizing:border-box;overflow:hidden;border-left:1px solid #ddd;border-right:1px solid #ddd;position:relative;background:#f8f8f8}.CodeMirror-merge-collapsed-line .CodeMirror-gutter-elt,.CodeMirror-overlayscroll .CodeMirror-gutter-filler,.CodeMirror-overlayscroll .CodeMirror-scrollbar-filler{display:none}.CodeMirror-merge-scrolllock-wrap{position:absolute;bottom:0;left:50%}.CodeMirror-merge-scrolllock{position:relative;left:-50%;cursor:pointer;color:#555;line-height:1}.CodeMirror-merge-copy,.CodeMirror-merge-copy-reverse{color:#44c;cursor:pointer;position:absolute}.CodeMirror-merge-copybuttons-left,.CodeMirror-merge-copybuttons-right{position:absolute;left:0;top:0;right:0;bottom:0;line-height:1}.CodeMirror-merge-copy{z-index:3}.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy{left:2px}.CodeMirror-merge-copybuttons-right .CodeMirror-merge-copy{right:2px}.CodeMirror-merge-l-inserted,.CodeMirror-merge-r-inserted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12MwuCXy3+CWyH8GBgYGJgYkAABZbAQ9ELXurwAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-l-deleted,.CodeMirror-merge-r-deleted{background-image:url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAYAAACddGYaAAAAGUlEQVQI12M4Kyb2/6yY2H8GBgYGJgYkAABURgPz6Ks7wQAAAABJRU5ErkJggg==);background-position:bottom left;background-repeat:repeat-x}.CodeMirror-merge-r-chunk{background:#ffffe0}.CodeMirror-merge-r-chunk-start{border-top:1px solid #ee8}.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #ee8}.CodeMirror-merge-r-connect{fill:#ffffe0;stroke:#ee8;stroke-width:1px}.CodeMirror-merge-l-chunk{background:#eef}.CodeMirror-merge-l-chunk-start{border-top:1px solid #88e}.CodeMirror-merge-l-chunk-end{border-bottom:1px solid #88e}.CodeMirror-merge-l-connect{fill:#eef;stroke:#88e;stroke-width:1px}.CodeMirror-merge-l-chunk.CodeMirror-merge-r-chunk{background:#dfd}.CodeMirror-merge-l-chunk-start.CodeMirror-merge-r-chunk-start{border-top:1px solid #4e4}.CodeMirror-merge-l-chunk-end.CodeMirror-merge-r-chunk-end{border-bottom:1px solid #4e4}.CodeMirror-merge-collapsed-widget:before{content:"(...)"}.CodeMirror-merge-collapsed-widget{cursor:pointer;color:#88b;background:#eef;border:1px solid #ddf;font-size:90%;padding:0 3px;border-radius:4px}.CodeMirror-simplescroll-horizontal div,.CodeMirror-simplescroll-vertical div{position:absolute;background:#ccc;-moz-box-sizing:border-box;box-sizing:border-box;border:1px solid #bbb;border-radius:2px}.CodeMirror-simplescroll-horizontal,.CodeMirror-simplescroll-vertical{position:absolute;z-index:6;background:#eee}.CodeMirror-simplescroll-horizontal{bottom:0;left:0;height:8px}.CodeMirror-simplescroll-horizontal div{bottom:0;height:100%}.CodeMirror-simplescroll-vertical{right:0;top:0;width:8px}.CodeMirror-simplescroll-vertical div{right:0;width:100%}.CodeMirror-overlayscroll-horizontal div,.CodeMirror-overlayscroll-vertical div{position:absolute;background:#bcd;border-radius:3px}.CodeMirror-overlayscroll-horizontal,.CodeMirror-overlayscroll-vertical{position:absolute;z-index:6}.CodeMirror-overlayscroll-horizontal{bottom:0;left:0;height:6px}.CodeMirror-overlayscroll-horizontal div{bottom:0;height:100%}.CodeMirror-overlayscroll-vertical{right:0;top:0;width:6px}.CodeMirror-overlayscroll-vertical div{right:0;width:100%}.CodeMirror-search-match{background:gold;border-top:1px solid orange;border-bottom:1px solid orange;-moz-box-sizing:border-box;box-sizing:border-box;opacity:.5}.CodeMirror-Tern-completion{padding-left:22px;position:relative;line-height:1.5}.CodeMirror-Tern-completion:before{position:absolute;left:2px;bottom:2px;border-radius:50%;font-size:12px;font-weight:700;height:15px;width:15px;line-height:16px;text-align:center;color:#fff;-moz-box-sizing:border-box;box-sizing:border-box}.CodeMirror-Tern-completion-unknown:before{content:"?";background:#4bb}.CodeMirror-Tern-completion-object:before{content:"O";background:#77c}.CodeMirror-Tern-completion-fn:before{content:"F";background:#7c7}.CodeMirror-Tern-completion-array:before{content:"A";background:#c66}.CodeMirror-Tern-completion-number:before{content:"1";background:#999}.CodeMirror-Tern-completion-string:before{content:"S";background:#999}.CodeMirror-Tern-completion-bool:before{content:"B";background:#999}.CodeMirror-Tern-completion-guess{color:#999}.CodeMirror-Tern-tooltip{border:1px solid silver;border-radius:3px;color:#444;padding:2px 5px;font-size:90%;font-family:monospace;background-color:#fff;white-space:pre-wrap;max-width:40em;position:absolute;z-index:10;-moz-box-shadow:2px 3px 5px rgba(0,0,0,.2);box-shadow:2px 3px 5px rgba(0,0,0,.2);transition:opacity 1s;-moz-transition:opacity 1s;-webkit-transition:opacity 1s;-o-transition:opacity 1s;-ms-transition:opacity 1s}.CodeMirror-Tern-hint-doc{max-width:25em;margin-top:-3px}.CodeMirror-Tern-fname{color:#000}.CodeMirror-Tern-farg{color:#70a}.CodeMirror-Tern-farg-current{text-decoration:underline}.CodeMirror-Tern-type{color:#07c}.CodeMirror-Tern-fhint-guess{opacity:.7} \ No newline at end of file diff --git a/src/js/_enqueues/vendor/codemirror/codemirror.min.js b/src/js/_enqueues/vendor/codemirror/codemirror.min.js deleted file mode 100644 index 994abd3d23cfa..0000000000000 --- a/src/js/_enqueues/vendor/codemirror/codemirror.min.js +++ /dev/null @@ -1,29 +0,0 @@ -/*! This file is auto-generated from CodeMirror - github:codemirror/CodeMirror#ee20357d279bf9edfed0047d3bf2a75b5f0a040f - -CodeMirror, copyright (c) by Marijn Haverbeke and others -Distributed under an MIT license: http://codemirror.net/LICENSE - -This is CodeMirror (http://codemirror.net), a code editor -implemented in JavaScript on top of the browser's DOM. - -You can find some technical background for some of the code below -at http://marijnhaverbeke.nl/blog/#cm-internals . -*/ -!function a(b,c,d){function e(g,h){if(!c[g]){if(!b[g]){var i="function"==typeof require&&require;if(!h&&i)return i(g,!0);if(f)return f(g,!0);var j=new Error("Cannot find module '"+g+"'");throw j.code="MODULE_NOT_FOUND",j}var k=c[g]={exports:{}};b[g][0].call(k.exports,function(a){var c=b[g][1][a];return e(c?c:a)},k,k.exports,a,b,c,d)}return c[g].exports}for(var f="function"==typeof require&&require,g=0;g=0;h--){var i=d[h].from(),j=d[h].to();i.line>=c||(j.line>=c&&(j=g(c,0)),c=i.line,null==f?b.uncomment(i,j,a)?f="un":(b.lineComment(i,j,a),f="line"):"un"==f?b.uncomment(i,j,a):b.lineComment(i,j,a))}}),a.defineExtension("lineComment",function(a,h,i){i||(i=e);var j=this,k=d(j,a),l=j.getLine(a.line);if(null!=l&&!c(j,a,l)){var m=i.lineComment||k.lineComment;if(!m)return void((i.blockCommentStart||k.blockCommentStart)&&(i.fullLines=!0,j.blockComment(a,h,i)));var n=Math.min(0!=h.ch||h.line==a.line?h.line+1:h.line,j.lastLine()+1),o=null==i.padding?" ":i.padding,p=i.commentBlankLines||a.line==h.line;j.operation(function(){if(i.indent){for(var c=null,d=a.line;dh.length)&&(c=h)}for(var d=a.line;dl||h.operation(function(){if(0!=c.fullLines){var d=f.test(h.getLine(l));h.replaceRange(m+k,g(l)),h.replaceRange(j+m,g(a.line,0));var e=c.blockCommentLead||i.blockCommentLead;if(null!=e)for(var n=a.line+1;n<=l;++n)(n!=l||d)&&h.replaceRange(e+m,g(n,0))}else h.replaceRange(k,b),h.replaceRange(j,a)})}}),a.defineExtension("uncomment",function(a,b,c){c||(c=e);var h,i=this,j=d(i,a),k=Math.min(0!=b.ch||b.line==a.line?b.line:b.line-1,i.lastLine()),l=Math.min(a.line,k),m=c.lineComment||j.lineComment,n=[],o=null==c.padding?" ":c.padding;a:if(m){for(var p=l;p<=k;++p){var q=i.getLine(p),r=q.indexOf(m);if(r>-1&&!/comment/.test(i.getTokenTypeAt(g(p,r+1)))&&(r=-1),r==-1&&f.test(q))break a;if(r>-1&&f.test(q.slice(0,r)))break a;n.push(q)}if(i.operation(function(){for(var a=l;a<=k;++a){var b=n[a-l],c=b.indexOf(m),d=c+m.length;c<0||(b.slice(d,d+o.length)==o&&(d+=o.length),h=!0,i.replaceRange("",g(a,c),g(a,d)))}}),h)return!0}var s=c.blockCommentStart||j.blockCommentStart,t=c.blockCommentEnd||j.blockCommentEnd;if(!s||!t)return!1;var u=c.blockCommentLead||j.blockCommentLead,v=i.getLine(l),w=v.indexOf(s);if(w==-1)return!1;var x=k==l?v:i.getLine(k),y=x.indexOf(t,k==l?w+s.length:0);y==-1&&l!=k&&(x=i.getLine(--k),y=x.indexOf(t));var z=g(l,w+1),A=g(k,y+1);if(y==-1||!/comment/.test(i.getTokenTypeAt(z))||!/comment/.test(i.getTokenTypeAt(A))||i.getRange(z,A,"\n").indexOf(t)>-1)return!1;var B=v.lastIndexOf(s,a.ch),C=B==-1?-1:v.slice(0,a.ch).indexOf(t,B+s.length);if(B!=-1&&C!=-1&&C+t.length!=a.ch)return!1;C=x.indexOf(t,b.ch);var D=x.slice(b.ch).lastIndexOf(s,C-b.ch);return B=C==-1||D==-1?-1:b.ch+D,(C==-1||B==-1||B==b.ch)&&(i.operation(function(){i.replaceRange("",g(k,y-(o&&x.slice(y-o.length,y)==o?o.length:0)),g(k,y+t.length));var a=w+s.length;if(o&&v.slice(a,a+o.length)==o&&(a+=o.length),i.replaceRange("",g(l,w),g(l,a)),u)for(var b=l+1;b<=k;++b){var c=i.getLine(b),d=c.indexOf(u);if(d!=-1&&!f.test(c.slice(0,d))){var e=d+u.length;o&&c.slice(e,e+o.length)==o&&(e+=o.length),i.replaceRange("",g(b,d),g(b,e))}}}),!0)})})},{"../../lib/codemirror":59}],2:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){function b(b){if(b.getOption("disableInput"))return a.Pass;for(var d,e=b.listSelections(),f=[],g=0;g-1){if(j=l.slice(0,k),/\S/.test(j)){j="";for(var n=0;n-1&&!/\S/.test(l.slice(0,k))&&(j=l.slice(0,k));null!=j&&(j+=d.blockCommentContinue)}if(null==j&&d.lineComment&&c(b)){var l=b.getLine(h.line),k=l.indexOf(d.lineComment);k>-1&&(j=l.slice(0,k),/\S/.test(j)?j=null:j+=d.lineComment+l.slice(k+d.lineComment.length).match(/^\s*/)[0])}if(null==j)return a.Pass;f[g]="\n"+j}b.operation(function(){for(var a=e.length-1;a>=0;a--)b.replaceRange(f[a],e[a].from(),e[a].to(),"+insert")})}function c(a){var b=a.getOption("continueComments");return!b||"object"!=typeof b||b.continueLineComment!==!1}for(var d=["clike","css","javascript"],e=0;e=0;h--){var j=g[h].head;c.replaceRange("",o(j.line,j.ch-1),o(j.line,j.ch+1),"+delete")}}function g(c){var d=e(c),f=d&&b(d,"explode");if(!f||c.getOption("disableInput"))return a.Pass;for(var g=c.listSelections(),h=0;h0;return{anchor:new o(b.anchor.line,b.anchor.ch+(c?-1:1)),head:new o(b.head.line,b.head.ch+(c?1:-1))}}function i(c,d){var f=e(c);if(!f||c.getOption("disableInput"))return a.Pass;var g=b(f,"pairs"),i=g.indexOf(d);if(i==-1)return a.Pass;for(var k,n=b(f,"triples"),p=g.charAt(i+1)==d,q=c.listSelections(),r=i%2==0,s=0;s1&&n.indexOf(d)>=0&&c.getRange(o(v.line,v.ch-2),v)==d+d&&(v.ch<=2||c.getRange(o(v.line,v.ch-3),o(v.line,v.ch-2))!=d))t="addFour";else if(p){if(a.isWordChar(w)||!l(c,v,d))return a.Pass;t="both"}else{if(!r||c.getLine(v.line).length!=v.ch&&!j(w,g)&&!/\s/.test(w))return a.Pass;t="both"}else t=p&&m(c,v)?"both":n.indexOf(d)>=0&&c.getRange(v,o(v.line,v.ch+3))==d+d+d?"skipThree":"skip";if(k){if(k!=t)return a.Pass}else k=t}var x=i%2?g.charAt(i-1):d,y=i%2?d:g.charAt(i+1);c.operation(function(){if("skip"==k)c.execCommand("goCharRight");else if("skipThree"==k)for(var a=0;a<3;a++)c.execCommand("goCharRight");else if("surround"==k){for(var b=c.getSelections(),a=0;a-1&&c%2==1}function k(a,b){var c=a.getRange(o(b.line,b.ch-1),o(b.line,b.ch+1));return 2==c.length?c:null}function l(b,c,d){var e=b.getLine(c.line),f=b.getTokenAt(c);if(/\bstring2?\b/.test(f.type)||m(b,c))return!1;var g=new a.StringStream(e.slice(0,c.ch)+d+e.slice(c.ch),4);for(g.pos=g.start=f.start;;){var h=b.getMode().token(g,f.state);if(g.pos>=c.ch+1)return/\bstring2?\b/.test(h);g.start=g.pos}}function m(a,b){var c=a.getTokenAt(o(b.line,b.ch+1));return/\bstring/.test(c.type)&&c.start==b.ch}var n={pairs:"()[]{}''\"\"",triples:"",explode:"[]{}"},o=a.Pos;a.defineOption("autoCloseBrackets",!1,function(d,e,f){f&&f!=a.Init&&(d.removeKeyMap(p),d.state.closeBrackets=null),e&&(c(b(e,"pairs")),d.state.closeBrackets=e,d.addKeyMap(p))});var p={Backspace:f,Enter:g};c(n.pairs+"`")})},{"../../lib/codemirror":59}],10:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../fold/xml-fold")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../fold/xml-fold"],d):d(CodeMirror)}(function(a){function b(b){if(b.getOption("disableInput"))return a.Pass;for(var c=b.listSelections(),d=[],i=0;ij.ch&&(r=r.slice(0,r.length-k.end+j.ch));var s=r.toLowerCase();if(!r||"string"==k.type&&(k.end!=j.ch||!/[\"\']/.test(k.string.charAt(k.string.length-1))||1==k.string.length)||"tag"==k.type&&"closeTag"==m.type||k.string.indexOf("/")==k.string.length-1||p&&e(p,s)>-1||f(b,r,j,m,!0))return a.Pass;var t=q&&e(q,s)>-1;d[i]={indent:t,text:">"+(t?"\n\n":"")+"",newPos:t?a.Pos(j.line+1,0):a.Pos(j.line,j.ch+1)}}for(var i=c.length-1;i>=0;i--){var u=d[i];b.replaceRange(u.text,c[i].head,c[i].anchor,"+insert");var v=b.listSelections().slice(0);v[i]={head:u.newPos,anchor:u.newPos},b.setSelections(v),u.indent&&(b.indentLine(u.newPos.line,null,!0),b.indentLine(u.newPos.line+1,null,!0))}}function c(b,c){for(var d=b.listSelections(),e=[],g=c?"/":""!=b.getLine(i.line).charAt(j.end)&&(m+=">"),e[h]=m}b.replaceSelections(e),d=b.listSelections();for(var h=0;h'"]=function(a){return b(a)}),c.addKeyMap(g)}});var g=["area","base","br","col","command","embed","hr","img","input","keygen","link","meta","param","source","track","wbr"],h=["applet","blockquote","body","button","div","dl","fieldset","form","frameset","h1","h2","h3","h4","h5","h6","head","html","iframe","layer","legend","object","ol","p","select","table","ul"];a.commands.closeTag=function(a){return c(a)}})},{"../../lib/codemirror":59,"../fold/xml-fold":21}],11:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";var b=/^(\s*)(>[> ]*|[*+-] \[[x ]\]\s|[*+-]\s|(\d+)([.)]))(\s*)/,c=/^(\s*)(>[> ]*|[*+-] \[[x ]\]|[*+-]|(\d+)[.)])(\s*)$/,d=/[*+-]\s/;a.commands.newlineAndIndentContinueMarkdownList=function(e){if(e.getOption("disableInput"))return a.Pass;for(var f=e.listSelections(),g=[],h=0;h\s*$/.test(m)||e.replaceRange("",{line:i.line,ch:0},{line:i.line,ch:i.ch+1}),g[h]="\n";else{var o=n[1],p=n[5],q=d.test(n[2])||n[2].indexOf(">")>=0?n[2].replace("x"," "):parseInt(n[3],10)+1+n[4];g[h]="\n"+o+q+p}}e.replaceSelections(g)}})},{"../../lib/codemirror":59}],12:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){function b(a,b,d){var e=a.getLineHandle(b.line),f=b.ch-1,i=d&&d.afterCursor;null==i&&(i=/(^| )cm-fat-cursor($| )/.test(a.getWrapperElement().className));var j=!i&&f>=0&&h[e.text.charAt(f)]||h[e.text.charAt(++f)];if(!j)return null;var k=">"==j.charAt(1)?1:-1;if(d&&d.strict&&k>0!=(f==b.ch))return null;var l=a.getTokenTypeAt(g(b.line,f+1)),m=c(a,g(b.line,f+(k>0?1:0)),k,l||null,d);return null==m?null:{from:g(b.line,f),to:m&&m.pos,match:m&&m.ch==j.charAt(0),forward:k>0}}function c(a,b,c,d,e){for(var f=e&&e.maxScanLineLength||1e4,i=e&&e.maxScanLines||1e3,j=[],k=e&&e.bracketRegex?e.bracketRegex:/[(){}[\]]/,l=c>0?Math.min(b.line+i,a.lastLine()+1):Math.max(a.firstLine()-1,b.line-i),m=b.line;m!=l;m+=c){var n=a.getLine(m);if(n){var o=c>0?0:n.length-1,p=c>0?n.length:-1;if(!(n.length>f))for(m==b.line&&(o=b.ch-(c<0?1:0));o!=p;o+=c){var q=n.charAt(o);if(k.test(q)&&(void 0===d||a.getTokenTypeAt(g(m,o+1))==d)){var r=h[q];if(">"==r.charAt(1)==c>0)j.push(q);else{if(!j.length)return{pos:g(m,o),ch:q};j.pop()}}}}}return m-c!=(c>0?a.lastLine():a.firstLine())&&null}function d(a,c,d){for(var e=a.state.matchBrackets.maxHighlightLineLength||1e3,h=[],i=a.listSelections(),j=0;j",")":"(<","[":"]>","]":"[<","{":"}>","}":"{<"},i=null;a.defineOption("matchBrackets",!1,function(b,c,d){d&&d!=a.Init&&(b.off("cursorActivity",e),i&&(i(),i=null)),c&&(b.state.matchBrackets="object"==typeof c?c:{},b.on("cursorActivity",e))}),a.defineExtension("matchBrackets",function(){d(this,!0)}),a.defineExtension("findMatchingBracket",function(a,c,d){return(d||"boolean"==typeof c)&&(d?(d.strict=c,c=d):c=c?{strict:!0}:null),b(this,a,c)}),a.defineExtension("scanForBracket",function(a,b,d,e){return c(this,a,b,d,e)})})},{"../../lib/codemirror":59}],13:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../fold/xml-fold")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../fold/xml-fold"],d):d(CodeMirror)}(function(a){"use strict";function b(a){a.state.tagHit&&a.state.tagHit.clear(),a.state.tagOther&&a.state.tagOther.clear(),a.state.tagHit=a.state.tagOther=null}function c(c){c.state.failedTagMatch=!1,c.operation(function(){if(b(c),!c.somethingSelected()){var d=c.getCursor(),e=c.getViewport();e.from=Math.min(e.from,d.line),e.to=Math.max(d.line+1,e.to);var f=a.findMatchingTag(c,d,e);if(f){if(c.state.matchBothTags){var g="open"==f.at?f.open:f.close;g&&(c.state.tagHit=c.markText(g.from,g.to,{className:"CodeMirror-matchingtag"}))}var h="close"==f.at?f.open:f.close;h?c.state.tagOther=c.markText(h.from,h.to,{className:"CodeMirror-matchingtag"}):c.state.failedTagMatch=!0}}})}function d(a){a.state.failedTagMatch&&c(a)}a.defineOption("matchTags",!1,function(e,f,g){g&&g!=a.Init&&(e.off("cursorActivity",c),e.off("viewportChange",d),b(e)),f&&(e.state.matchBothTags="object"==typeof f&&f.bothTags,e.on("cursorActivity",c),e.on("viewportChange",d),c(e))}),a.commands.toMatchingTag=function(b){var c=a.findMatchingTag(b,b.getCursor());if(c){var d="close"==c.at?c.open:c.close;d&&b.extendSelection(d.to,d.from)}}})},{"../../lib/codemirror":59,"../fold/xml-fold":21}],14:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){a.defineOption("showTrailingSpace",!1,function(b,c,d){d==a.Init&&(d=!1),d&&!c?b.removeOverlay("trailingspace"):!d&&c&&b.addOverlay({token:function(a){for(var b=a.string.length,c=b;c&&/\s/.test(a.string.charAt(c-1));--c);return c>a.pos?(a.pos=c,null):(a.pos=b,"trailingspace")},name:"trailingspace"})})})},{"../../lib/codemirror":59}],15:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.registerHelper("fold","brace",function(b,c){function d(d){for(var h=c.ch,i=0;;){var j=h<=0?-1:g.lastIndexOf(d,h-1);if(j!=-1){if(1==i&&jb.lastLine())return null;var d=b.getTokenAt(a.Pos(c,1));if(/\S/.test(d.string)||(d=b.getTokenAt(a.Pos(c,d.end+1))),"keyword"!=d.type||"import"!=d.string)return null;for(var e=c,f=Math.min(b.lastLine(),c+10);e<=f;++e){var g=b.getLine(e),h=g.indexOf(";");if(h!=-1)return{startCh:d.end,end:a.Pos(e,h)}}}var e,f=c.line,g=d(f);if(!g||d(f-1)||(e=d(f-2))&&e.end.line==f-1)return null;for(var h=g.end;;){var i=d(h.line+1);if(null==i)break;h=i.end}return{from:b.clipPos(a.Pos(f,g.startCh+1)),to:h}}),a.registerHelper("fold","include",function(b,c){function d(c){if(cb.lastLine())return null;var d=b.getTokenAt(a.Pos(c,1));return/\S/.test(d.string)||(d=b.getTokenAt(a.Pos(c,d.end+1))),"meta"==d.type&&"#include"==d.string.slice(0,8)?d.start+8:void 0}var e=c.line,f=d(e);if(null==f||null!=d(e-1))return null;for(var g=e;;){var h=d(g+1);if(null==h)break;++g}return{from:a.Pos(e,f+1),to:b.clipPos(a.Pos(g))}})})},{"../../lib/codemirror":59}],16:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.registerGlobalHelper("fold","comment",function(a){return a.blockCommentStart&&a.blockCommentEnd},function(b,c){var d=b.getModeAt(c),e=d.blockCommentStart,f=d.blockCommentEnd;if(e&&f){for(var g,h=c.line,i=b.getLine(h),j=c.ch,k=0;;){var l=j<=0?-1:i.lastIndexOf(e,j-1);if(l!=-1){if(1==k&&lb.firstLine();)e=a.Pos(e.line-1,0),k=h(!1);if(k&&!k.cleared&&"unfold"!==g){var l=c(b,f);a.on(l,"mousedown",function(b){m.clear(),a.e_preventDefault(b)});var m=b.markText(k.from,k.to,{replacedWith:l,clearOnEnter:d(b,f,"clearOnEnter"),__isFold:!0});m.on("clear",function(c,d){a.signal(b,"unfold",b,c,d)}),a.signal(b,"fold",b,k.from,k.to)}}function c(a,b){var c=d(a,b,"widget");if("string"==typeof c){var e=document.createTextNode(c);c=document.createElement("span"),c.appendChild(e),c.className="CodeMirror-foldmarker"}else c&&(c=c.cloneNode(!0));return c}function d(a,b,c){if(b&&void 0!==b[c])return b[c];var d=a.options.foldOptions;return d&&void 0!==d[c]?d[c]:e[c]}a.newFoldFunction=function(a,c){return function(d,e){b(d,e,{rangeFinder:a, -widget:c})}},a.defineExtension("foldCode",function(a,c,d){b(this,a,c,d)}),a.defineExtension("isFolded",function(a){for(var b=this.findMarksAt(a),c=0;c=h&&(c=e(f.indicatorOpen))}a.setGutterMarker(b,f.gutter,c),++g})}function g(a){var b=a.getViewport(),c=a.state.foldGutter;c&&(a.operation(function(){f(a,b.from,b.to)}),c.from=b.from,c.to=b.to)}function h(a,b,c){var e=a.state.foldGutter;if(e){var f=e.options;if(c==f.gutter){var g=d(a,b);g?g.clear():a.foldCode(l(b,0),f.rangeFinder)}}}function i(a){var b=a.state.foldGutter;if(b){var c=b.options;b.from=b.to=0,clearTimeout(b.changeUpdate),b.changeUpdate=setTimeout(function(){g(a)},c.foldOnChangeTimeSpan||600)}}function j(a){var b=a.state.foldGutter;if(b){var c=b.options;clearTimeout(b.changeUpdate),b.changeUpdate=setTimeout(function(){var c=a.getViewport();b.from==b.to||c.from-b.to>20||b.from-c.to>20?g(a):a.operation(function(){c.fromb.to&&(f(a,b.to,c.to),b.to=c.to)})},c.updateViewportTimeSpan||400)}}function k(a,b){var c=a.state.foldGutter;if(c){var d=b.line;d>=c.from&&de))break;f=g}}return f?{from:a.Pos(d.line,c.getLine(d.line).length),to:a.Pos(f,c.getLine(f).length)}:void 0}})})},{"../../lib/codemirror":59}],20:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.registerHelper("fold","markdown",function(b,c){function d(c){var d=b.getTokenTypeAt(a.Pos(c,0));return d&&/\bheader\b/.test(d)}function e(a,b,c){var e=b&&b.match(/^#+/);return e&&d(a)?e[0].length:(e=c&&c.match(/^[=\-]+\s*$/),e&&d(a+1)?"="==c[0]?1:2:f)}var f=100,g=b.getLine(c.line),h=b.getLine(c.line+1),i=e(c.line,g,h);if(i!==f){for(var j=b.lastLine(),k=c.line,l=b.getLine(k+2);k=a.max))return a.ch=0,a.text=a.cm.getLine(++a.line),!0}function f(a){if(!(a.line<=a.min))return a.text=a.cm.getLine(--a.line),a.ch=a.text.length,!0}function g(a){for(;;){var b=a.text.indexOf(">",a.ch);if(b==-1){if(e(a))continue;return}{if(d(a,b+1)){var c=a.text.lastIndexOf("/",b),f=c>-1&&!/\S/.test(a.text.slice(c+1,b));return a.ch=b+1,f?"selfClose":"regular"}a.ch=b+1}}}function h(a){for(;;){var b=a.ch?a.text.lastIndexOf("<",a.ch-1):-1;if(b==-1){if(f(a))continue;return}if(d(a,b+1)){p.lastIndex=b,a.ch=b;var c=p.exec(a.text);if(c&&c.index==b)return c}else a.ch=b}}function i(a){for(;;){p.lastIndex=a.ch;var b=p.exec(a.text);if(!b){if(e(a))continue;return}{if(d(a,b.index+1))return a.ch=b.index+b[0].length,b;a.ch=b.index+1}}}function j(a){for(;;){var b=a.ch?a.text.lastIndexOf(">",a.ch-1):-1;if(b==-1){if(f(a))continue;return}{if(d(a,b+1)){var c=a.text.lastIndexOf("/",b),e=c>-1&&!/\S/.test(a.text.slice(c+1,b));return a.ch=b+1,e?"selfClose":"regular"}a.ch=b}}}function k(a,b){for(var c=[];;){var d,e=i(a),f=a.line,h=a.ch-(e?e[0].length:0);if(!e||!(d=g(a)))return;if("selfClose"!=d)if(e[1]){for(var j=c.length-1;j>=0;--j)if(c[j]==e[2]){c.length=j;break}if(j<0&&(!b||b==e[2]))return{tag:e[2],from:m(f,h),to:m(a.line,a.ch)}}else c.push(e[2])}}function l(a,b){for(var c=[];;){var d=j(a);if(!d)return;if("selfClose"!=d){var e=a.line,f=a.ch,g=h(a);if(!g)return;if(g[1])c.push(g[2]);else{for(var i=c.length-1;i>=0;--i)if(c[i]==g[2]){c.length=i;break}if(i<0&&(!b||b==g[2]))return{tag:g[2],from:m(a.line,a.ch),to:m(e,f)}}}else h(a)}}var m=a.Pos,n="A-Z_a-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD",o=n+"-:.0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040",p=new RegExp("<(/?)(["+n+"]["+o+"]*)","g");a.registerHelper("fold","xml",function(a,b){for(var d=new c(a,b.line,0);;){var e,f=i(d);if(!f||d.line!=b.line||!(e=g(d)))return;if(!f[1]&&"selfClose"!=e){var h=m(d.line,d.ch),j=k(d,f[2]);return j&&{from:h,to:j.from}}}}),a.findMatchingTag=function(a,d,e){var f=new c(a,d.line,d.ch,e);if(f.text.indexOf(">")!=-1||f.text.indexOf("<")!=-1){var i=g(f),j=i&&m(f.line,f.ch),n=i&&h(f);if(i&&n&&!(b(f,d)>0)){var o={from:m(f.line,f.ch),to:j,tag:n[2]};return"selfClose"==i?{open:o,close:null,at:"open"}:n[1]?{open:l(f,n[2]),close:o,at:"close"}:(f=new c(a,j.line,j.ch,e),{open:o,close:k(f,n[2]),at:"open"})}}},a.findEnclosingTag=function(a,b,d,e){for(var f=new c(a,b.line,b.ch,d);;){var g=l(f,e);if(!g)break;var h=new c(a,b.line,b.ch,d),i=k(h,g.tag);if(i)return{open:g,close:i}}},a.scanForClosingTag=function(a,b,d,e){var f=new c(a,b.line,b.ch,e?{from:0,to:e}:null);return k(f,d)}})},{"../../lib/codemirror":59}],22:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";var b=/[\w$]+/,c=500;a.registerHelper("hint","anyword",function(d,e){for(var f=e&&e.word||b,g=e&&e.range||c,h=d.getCursor(),i=d.getLine(h.line),j=h.ch,k=j;k&&f.test(i.charAt(k-1));)--k;for(var l=k!=j&&i.slice(k,j),m=e&&e.list||[],n={},o=new RegExp(f.source,"g"),p=-1;p<=1;p+=2)for(var q=h.line,r=Math.min(Math.max(q+p*g,d.firstLine()),d.lastLine())+p;q!=r;q+=p)for(var s,t=d.getLine(q);s=o.exec(t);)q==h.line&&s[0]===l||l&&0!=s[0].lastIndexOf(l,0)||Object.prototype.hasOwnProperty.call(n,s[0])||(n[s[0]]=!0,m.push(s[0]));return{list:m,from:a.Pos(h.line,k),to:a.Pos(h.line,j)}})})},{"../../lib/codemirror":59}],23:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../../mode/css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../../mode/css/css"],d):d(CodeMirror)}(function(a){"use strict";var b={link:1,visited:1,active:1,hover:1,focus:1,"first-letter":1,"first-line":1,"first-child":1,before:1,after:1,lang:1};a.registerHelper("hint","css",function(c){function d(a){for(var b in a)j&&0!=b.lastIndexOf(j,0)||l.push(b)}var e=c.getCursor(),f=c.getTokenAt(e),g=a.innerMode(c.getMode(),f.state);if("css"==g.mode.name){if("keyword"==f.type&&0=="!important".indexOf(f.string))return{list:["!important"],from:a.Pos(e.line,f.start),to:a.Pos(e.line,f.end)};var h=f.start,i=e.ch,j=f.string.slice(0,i-h);/[^\w$_-]/.test(j)&&(j="",h=i=e.ch);var k=a.resolveMode("text/css"),l=[],m=g.state.state;return"pseudo"==m||"variable-3"==f.type?d(b):"block"==m||"maybeprop"==m?d(k.propertyKeywords):"prop"==m||"parens"==m||"at"==m||"params"==m?(d(k.valueKeywords),d(k.colorKeywords)):"media"!=m&&"media_parens"!=m||(d(k.mediaTypes),d(k.mediaFeatures)),l.length?{list:l,from:a.Pos(e.line,h),to:a.Pos(e.line,i)}:void 0}})})},{"../../lib/codemirror":59,"../../mode/css/css":61}],24:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("./xml-hint")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./xml-hint"],d):d(CodeMirror)}(function(a){"use strict";function b(a){for(var b in l)l.hasOwnProperty(b)&&(a.attrs[b]=l[b])}function c(b,c){var d={schemaInfo:k};if(c)for(var e in c)d[e]=c[e];return a.hint.xml(b,d)}var d="ab aa af ak sq am ar an hy as av ae ay az bm ba eu be bn bh bi bs br bg my ca ch ce ny zh cv kw co cr hr cs da dv nl dz en eo et ee fo fj fi fr ff gl ka de el gn gu ht ha he hz hi ho hu ia id ie ga ig ik io is it iu ja jv kl kn kr ks kk km ki rw ky kv kg ko ku kj la lb lg li ln lo lt lu lv gv mk mg ms ml mt mi mr mh mn na nv nb nd ne ng nn no ii nr oc oj cu om or os pa pi fa pl ps pt qu rm rn ro ru sa sc sd se sm sg sr gd sn si sk sl so st es su sw ss sv ta te tg th ti bo tk tl tn to tr ts tt tw ty ug uk ur uz ve vi vo wa cy wo fy xh yi yo za zu".split(" "),e=["_blank","_self","_top","_parent"],f=["ascii","utf-8","utf-16","latin1","latin1"],g=["get","post","put","delete"],h=["application/x-www-form-urlencoded","multipart/form-data","text/plain"],i=["all","screen","print","embossed","braille","handheld","print","projection","screen","tty","tv","speech","3d-glasses","resolution [>][<][=] [X]","device-aspect-ratio: X/Y","orientation:portrait","orientation:landscape","device-height: [X]","device-width: [X]"],j={attrs:{}},k={a:{attrs:{href:null,ping:null,type:null,media:i,target:e,hreflang:d}},abbr:j,acronym:j,address:j,applet:j,area:{attrs:{alt:null,coords:null,href:null,target:null,ping:null,media:i,hreflang:d,type:null,shape:["default","rect","circle","poly"]}},article:j,aside:j,audio:{attrs:{src:null,mediagroup:null,crossorigin:["anonymous","use-credentials"],preload:["none","metadata","auto"],autoplay:["","autoplay"],loop:["","loop"],controls:["","controls"]}},b:j,base:{attrs:{href:null,target:e}},basefont:j,bdi:j,bdo:j,big:j,blockquote:{attrs:{cite:null}},body:j,br:j,button:{attrs:{form:null,formaction:null,name:null,value:null,autofocus:["","autofocus"],disabled:["","autofocus"],formenctype:h,formmethod:g,formnovalidate:["","novalidate"],formtarget:e,type:["submit","reset","button"]}},canvas:{attrs:{width:null,height:null}},caption:j,center:j,cite:j,code:j,col:{attrs:{span:null}},colgroup:{attrs:{span:null}},command:{attrs:{type:["command","checkbox","radio"],label:null,icon:null,radiogroup:null,command:null,title:null,disabled:["","disabled"],checked:["","checked"]}},data:{attrs:{value:null}},datagrid:{attrs:{disabled:["","disabled"],multiple:["","multiple"]}},datalist:{attrs:{data:null}},dd:j,del:{attrs:{cite:null,datetime:null}},details:{attrs:{open:["","open"]}},dfn:j,dir:j,div:j,dl:j,dt:j,em:j,embed:{attrs:{src:null,type:null,width:null,height:null}},eventsource:{attrs:{src:null}},fieldset:{attrs:{disabled:["","disabled"],form:null,name:null}},figcaption:j,figure:j,font:j,footer:j,form:{attrs:{action:null,name:null,"accept-charset":f,autocomplete:["on","off"],enctype:h,method:g,novalidate:["","novalidate"],target:e}},frame:j,frameset:j,h1:j,h2:j,h3:j,h4:j,h5:j,h6:j,head:{attrs:{},children:["title","base","link","style","meta","script","noscript","command"]},header:j,hgroup:j,hr:j,html:{attrs:{manifest:null},children:["head","body"]},i:j,iframe:{attrs:{src:null,srcdoc:null,name:null,width:null,height:null,sandbox:["allow-top-navigation","allow-same-origin","allow-forms","allow-scripts"],seamless:["","seamless"]}},img:{attrs:{alt:null,src:null,ismap:null,usemap:null,width:null,height:null,crossorigin:["anonymous","use-credentials"]}},input:{attrs:{alt:null,dirname:null,form:null,formaction:null,height:null,list:null,max:null,maxlength:null,min:null,name:null,pattern:null,placeholder:null,size:null,src:null,step:null,value:null,width:null,accept:["audio/*","video/*","image/*"],autocomplete:["on","off"],autofocus:["","autofocus"],checked:["","checked"],disabled:["","disabled"],formenctype:h,formmethod:g,formnovalidate:["","novalidate"],formtarget:e,multiple:["","multiple"],readonly:["","readonly"],required:["","required"],type:["hidden","text","search","tel","url","email","password","datetime","date","month","week","time","datetime-local","number","range","color","checkbox","radio","file","submit","image","reset","button"]}},ins:{attrs:{cite:null,datetime:null}},kbd:j,keygen:{attrs:{challenge:null,form:null,name:null,autofocus:["","autofocus"],disabled:["","disabled"],keytype:["RSA"]}},label:{attrs:{"for":null,form:null}},legend:j,li:{attrs:{value:null}},link:{attrs:{href:null,type:null,hreflang:d,media:i,sizes:["all","16x16","16x16 32x32","16x16 32x32 64x64"]}},map:{attrs:{name:null}},mark:j,menu:{attrs:{label:null,type:["list","context","toolbar"]}},meta:{attrs:{content:null,charset:f,name:["viewport","application-name","author","description","generator","keywords"],"http-equiv":["content-language","content-type","default-style","refresh"]}},meter:{attrs:{value:null,min:null,low:null,high:null,max:null,optimum:null}},nav:j,noframes:j,noscript:j,object:{attrs:{data:null,type:null,name:null,usemap:null,form:null,width:null,height:null,typemustmatch:["","typemustmatch"]}},ol:{attrs:{reversed:["","reversed"],start:null,type:["1","a","A","i","I"]}},optgroup:{attrs:{disabled:["","disabled"],label:null}},option:{attrs:{disabled:["","disabled"],label:null,selected:["","selected"],value:null}},output:{attrs:{"for":null,form:null,name:null}},p:j,param:{attrs:{name:null,value:null}},pre:j,progress:{attrs:{value:null,max:null}},q:{attrs:{cite:null}},rp:j,rt:j,ruby:j,s:j,samp:j,script:{attrs:{type:["text/javascript"],src:null,async:["","async"],defer:["","defer"],charset:f}},section:j,select:{attrs:{form:null,name:null,size:null,autofocus:["","autofocus"],disabled:["","disabled"],multiple:["","multiple"]}},small:j,source:{attrs:{src:null,type:null,media:null}},span:j,strike:j,strong:j,style:{attrs:{type:["text/css"],media:i,scoped:null}},sub:j,summary:j,sup:j,table:j,tbody:j,td:{attrs:{colspan:null,rowspan:null,headers:null}},textarea:{attrs:{dirname:null,form:null,maxlength:null,name:null,placeholder:null,rows:null,cols:null,autofocus:["","autofocus"],disabled:["","disabled"],readonly:["","readonly"],required:["","required"],wrap:["soft","hard"]}},tfoot:j,th:{attrs:{colspan:null,rowspan:null,headers:null,scope:["row","col","rowgroup","colgroup"]}},thead:j,time:{attrs:{datetime:null}},title:j,tr:j,track:{attrs:{src:null,label:null,"default":null,kind:["subtitles","captions","descriptions","chapters","metadata"],srclang:d}},tt:j,u:j,ul:j,"var":j,video:{attrs:{src:null,poster:null,width:null,height:null,crossorigin:["anonymous","use-credentials"],preload:["auto","metadata","none"],autoplay:["","autoplay"],mediagroup:["movie"],muted:["","muted"],controls:["","controls"]}},wbr:j},l={accesskey:["a","b","c","d","e","f","g","h","i","j","k","l","m","n","o","p","q","r","s","t","u","v","w","x","y","z","0","1","2","3","4","5","6","7","8","9"],"class":null,contenteditable:["true","false"],contextmenu:null,dir:["ltr","rtl","auto"],draggable:["true","false","auto"],dropzone:["copy","move","link","string:","file:"],hidden:["hidden"],id:null,inert:["inert"],itemid:null,itemprop:null,itemref:null,itemscope:["itemscope"],itemtype:null,lang:["en","es"],spellcheck:["true","false"],style:null,tabindex:["1","2","3","4","5","6","7","8","9"],title:null,translate:["yes","no"],onclick:null,rel:["stylesheet","alternate","author","bookmark","help","license","next","nofollow","noreferrer","prefetch","prev","search","tag"]};b(j);for(var m in k)k.hasOwnProperty(m)&&k[m]!=j&&b(k[m]);a.htmlSchema=k,a.registerHelper("hint","html",c)})},{"../../lib/codemirror":59,"./xml-hint":28}],25:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){function b(a,b){for(var c=0,d=a.length;cf.ch&&(g.end=f.ch,g.string=g.string.slice(0,f.ch-g.start)):g={start:f.ch,end:f.ch,string:"",state:g.state,type:"."==g.string?"property":null};for(var h=g;"property"==h.type;){if(h=d(b,j(f.line,h.start)),"."!=h.string)return;if(h=d(b,j(f.line,h.start)),!k)var k=[];k.push(h)}return{list:i(g,k,c,e),from:j(f.line,g.start),to:j(f.line,g.end)}}}function e(a,b){return d(a,n,function(a,b){return a.getTokenAt(b)},b)}function f(a,b){var c=a.getTokenAt(b);return b.ch==c.start+1&&"."==c.string.charAt(0)?(c.end=c.start,c.string=".",c.type="property"):/^\.[\w$_]*$/.test(c.string)&&(c.type="property",c.start++,c.string=c.string.replace(/\./,"")),c}function g(a,b){return d(a,o,f,b)}function h(a,b){if(Object.getOwnPropertyNames&&Object.getPrototypeOf)for(var c=a;c;c=Object.getPrototypeOf(c))Object.getOwnPropertyNames(c).forEach(b);else for(var d in a)b(d)}function i(a,d,e,f){function g(a){0!=a.lastIndexOf(n,0)||c(j,a)||j.push(a)}function i(a){"string"==typeof a?b(k,g):a instanceof Array?b(l,g):a instanceof Function&&b(m,g),h(a,g)}var j=[],n=a.string,o=f&&f.globalScope||window;if(d&&d.length){var p,q=d.pop();for(q.type&&0===q.type.indexOf("variable")?(f&&f.additionalContext&&(p=f.additionalContext[q.string]),f&&f.useGlobalScope===!1||(p=p||o[q.string])):"string"==q.type?p="":"atom"==q.type?p=1:"function"==q.type&&(null==o.jQuery||"$"!=q.string&&"jQuery"!=q.string||"function"!=typeof o.jQuery?null!=o._&&"_"==q.string&&"function"==typeof o._&&(p=o._()):p=o.jQuery());null!=p&&d.length;)p=p[d.pop().string];null!=p&&i(p)}else{for(var r=a.state.localVars;r;r=r.next)g(r.name);for(var r=a.state.globalVars;r;r=r.next)g(r.name);f&&f.useGlobalScope===!1||i(o),b(e,g)}return j}var j=a.Pos;a.registerHelper("hint","javascript",e),a.registerHelper("hint","coffeescript",g);var k="charAt charCodeAt indexOf lastIndexOf substring substr slice trim trimLeft trimRight toUpperCase toLowerCase split concat match replace search".split(" "),l="length concat join splice push pop shift unshift slice reverse sort indexOf lastIndexOf every some filter forEach map reduce reduceRight ".split(" "),m="prototype apply call bind".split(" "),n="break case catch continue debugger default delete do else false finally for function if in instanceof new null return switch throw true try typeof var void while with".split(" "),o="and break catch class continue delete do else extends false finally for if in instanceof isnt new no not null of off on or return switch then throw true try typeof until void while with yes".split(" ")})},{"../../lib/codemirror":59}],26:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b){this.cm=a,this.options=b,this.widget=null,this.debounce=0,this.tick=0,this.startPos=this.cm.getCursor("start"),this.startLen=this.cm.getLine(this.startPos.line).length-this.cm.getSelection().length;var c=this;a.on("cursorActivity",this.activityFunc=function(){c.cursorActivity()})}function c(b,c){var d=a.cmpPos(c.from,b.from);return d>0&&b.to.ch-b.from.ch!=c.to.ch-c.from.ch}function d(a,b,c){var d=a.options.hintOptions,e={};for(var f in p)e[f]=p[f];if(d)for(var f in d)void 0!==d[f]&&(e[f]=d[f]);if(c)for(var f in c)void 0!==c[f]&&(e[f]=c[f]);return e.hint.resolve&&(e.hint=e.hint.resolve(a,b)),e}function e(a){return"string"==typeof a?a:a.text}function f(a,b){function c(a,c){var e;e="string"!=typeof c?function(a){return c(a,b)}:d.hasOwnProperty(c)?d[c]:c,f[a]=e}var d={Up:function(){b.moveFocus(-1)},Down:function(){b.moveFocus(1)},PageUp:function(){b.moveFocus(-b.menuSize()+1,!0)},PageDown:function(){b.moveFocus(b.menuSize()-1,!0)},Home:function(){b.setFocus(0)},End:function(){b.setFocus(b.length-1)},Enter:b.pick,Tab:b.pick,Esc:b.close},e=a.options.customKeys,f=e?{}:d;if(e)for(var g in e)e.hasOwnProperty(g)&&c(g,e[g]);var h=a.options.extraKeys;if(h)for(var g in h)h.hasOwnProperty(g)&&c(g,h[g]);return f}function g(a,b){for(;b&&b!=a;){if("LI"===b.nodeName.toUpperCase()&&b.parentNode==a)return b;b=b.parentNode}}function h(b,c){this.completion=b,this.data=c,this.picked=!1;var d=this,h=b.cm,i=this.hints=document.createElement("ul");i.className="CodeMirror-hints",this.selectedHint=c.selectedHint||0;for(var j=c.list,k=0;ki.clientHeight+1,z=h.getScrollInfo();if(x>0){var A=w.bottom-w.top,B=q.top-(q.bottom-w.top);if(B-A>0)i.style.top=(s=q.top-A)+"px",t=!1;else if(A>v){i.style.height=v-5+"px",i.style.top=(s=q.bottom-w.top)+"px";var C=h.getCursor();c.from.ch!=C.ch&&(q=h.cursorCoords(C),i.style.left=(r=q.left)+"px",w=i.getBoundingClientRect())}}var D=w.right-u;if(D>0&&(w.right-w.left>u&&(i.style.width=u-5+"px",D-=w.right-w.left-u),i.style.left=(r=q.left-D)+"px"),y)for(var E=i.firstChild;E;E=E.nextSibling)E.style.paddingRight=h.display.nativeBarWidth+"px";if(h.addKeyMap(this.keyMap=f(b,{moveFocus:function(a,b){d.changeActive(d.selectedHint+a,b)},setFocus:function(a){d.changeActive(a)},menuSize:function(){return d.screenAmount()},length:j.length,close:function(){b.close()},pick:function(){d.pick()},data:c})),b.options.closeOnUnfocus){var F;h.on("blur",this.onBlur=function(){F=setTimeout(function(){b.close()},100)}),h.on("focus",this.onFocus=function(){clearTimeout(F)})}return h.on("scroll",this.onScroll=function(){var a=h.getScrollInfo(),c=h.getWrapperElement().getBoundingClientRect(),d=s+z.top-a.top,e=d-(window.pageYOffset||(document.documentElement||document.body).scrollTop);return t||(e+=i.offsetHeight),e<=c.top||e>=c.bottom?b.close():(i.style.top=d+"px",void(i.style.left=r+z.left-a.left+"px"))}),a.on(i,"dblclick",function(a){var b=g(i,a.target||a.srcElement);b&&null!=b.hintId&&(d.changeActive(b.hintId),d.pick())}),a.on(i,"click",function(a){var c=g(i,a.target||a.srcElement);c&&null!=c.hintId&&(d.changeActive(c.hintId),b.options.completeOnSingleClick&&d.pick())}),a.on(i,"mousedown",function(){setTimeout(function(){h.focus()},20)}),a.signal(c,"select",j[this.selectedHint],i.childNodes[this.selectedHint]),!0}function i(a,b){if(!a.somethingSelected())return b;for(var c=[],d=0;d0?b(a):d(e+1)})}var f=i(a,e);d(0)};return f.async=!0,f.supportsSelection=!0,f}return(d=b.getHelper(b.getCursor(),"hintWords"))?function(b){return a.hint.fromList(b,{words:d})}:a.hint.anyword?function(b,c){return a.hint.anyword(b,c)}:function(){}}var l="CodeMirror-hint",m="CodeMirror-hint-active";a.showHint=function(a,b,c){if(!b)return a.showHint(c);c&&c.async&&(b.async=!0);var d={hint:b};if(c)for(var e in c)d[e]=c[e];return a.showHint(d)},a.defineExtension("showHint",function(c){c=d(this,this.getCursor("start"),c);var e=this.listSelections();if(!(e.length>1)){if(this.somethingSelected()){if(!c.hint.supportsSelection)return;for(var f=0;f=this.data.list.length?b=c?this.data.list.length-1:0:b<0&&(b=c?0:this.data.list.length-1),this.selectedHint!=b){var d=this.hints.childNodes[this.selectedHint];d.className=d.className.replace(" "+m,""),d=this.hints.childNodes[this.selectedHint=b],d.className+=" "+m,d.offsetTopthis.hints.scrollTop+this.hints.clientHeight&&(this.hints.scrollTop=d.offsetTop+d.offsetHeight-this.hints.clientHeight+3),a.signal(this.data,"select",this.data.list[this.selectedHint],d)}},screenAmount:function(){return Math.floor(this.hints.clientHeight/this.hints.firstChild.offsetHeight)||1}},a.registerHelper("hint","auto",{resolve:k}),a.registerHelper("hint","fromList",function(b,c){var d=b.getCursor(),e=b.getTokenAt(d),f=a.Pos(d.line,e.end);if(e.string&&/\w/.test(e.string[e.string.length-1]))var g=e.string,h=a.Pos(d.line,e.start);else var g="",h=f;for(var i=[],j=0;j,]/,closeOnUnfocus:!0,completeOnSingleClick:!0,container:null,customKeys:null,extraKeys:null};a.defineOption("hintOptions",null)})},{"../../lib/codemirror":59}],27:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../../mode/sql/sql")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../../mode/sql/sql"],d):d(CodeMirror)}(function(a){"use strict";function b(a){return"[object Array]"==Object.prototype.toString.call(a)}function c(b){var c=b.doc.modeOption;return"sql"===c&&(c="text/x-sql"),a.resolveMode(c).keywords}function d(b){var c=b.doc.modeOption;return"sql"===c&&(c="text/x-sql"),a.resolveMode(c).identifierQuote||"`"}function e(a){return"string"==typeof a?a:a.text}function f(a,c){return b(c)&&(c={columns:c}),c.text||(c.text=a),c}function g(a){var c={};if(b(a))for(var d=a.length-1;d>=0;d--){var g=a[d];c[e(g).toUpperCase()]=f(e(g),g)}else if(a)for(var h in a)c[h.toUpperCase()]=f(h,a[h]);return c}function h(a){return q[a.toUpperCase()]}function i(a){var b={};for(var c in a)a.hasOwnProperty(c)&&(b[c]=a[c]);return b}function j(a,b){var c=a.length,d=e(b).substr(0,c);return a.toUpperCase()===d.toUpperCase()}function k(a,c,d,e){if(b(d))for(var f=0;f0)&&w(m,i[n])<=0){j={start:l,end:i[n]};break}l=i[n]}for(var p=c.getRange(j.start,j.end,!1),n=0;nm.ch&&(u.end=m.ch,u.string=u.string.slice(0,m.ch-u.start)),u.string.match(/^[.`"\w@]\w*$/)?(l=u.string,i=u.start,j=u.end):(i=j=m.ch,l=""),"."==l.charAt(0)||l.charAt(0)==t?i=n(m,u,o,a):(k(o,l,q,function(a){return a}),k(o,l,r,function(a){return a}),f||k(o,l,s,function(a){return a.toUpperCase()})),{list:o,from:v(m.line,i),to:v(m.line,j)}})})},{"../../lib/codemirror":59,"../../mode/sql/sql":74}],28:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(b,d){var e=d&&d.schemaInfo,f=d&&d.quoteChar||'"';if(e){var g=b.getCursor(),h=b.getTokenAt(g);h.end>g.ch&&(h.end=g.ch,h.string=h.string.slice(0,g.ch-h.start));var i=a.innerMode(b.getMode(),h.state);if("xml"==i.mode.name){var j,k,l=[],m=!1,n=/\btag\b/.test(h.type)&&!/>$/.test(h.string),o=n&&/^\w/.test(h.string);if(o){var p=b.getLine(g.line).slice(Math.max(0,h.start-2),h.start),q=/<\/$/.test(p)?"close":/<$/.test(p)?"open":null;q&&(k=h.start-("close"==q?2:1))}else n&&"<"==h.string?q="open":n&&"")}else{var s=e[i.state.tagName],w=s&&s.attrs,x=e["!attrs"];if(!w&&!x)return;if(w){if(x){var y={};for(var z in x)x.hasOwnProperty(z)&&(y[z]=x[z]);for(var z in w)w.hasOwnProperty(z)&&(y[z]=w[z]);w=y}}else w=x;if("string"==h.type||"="==h.string){var A,p=b.getRange(c(g.line,Math.max(0,g.ch-60)),c(g.line,"string"==h.type?h.start:h.end)),B=p.match(/([^\s\u00a0=<>\"\']+)=$/);if(!B||!w.hasOwnProperty(B[1])||!(A=w[B[1]]))return;if("function"==typeof A&&(A=A.call(this,b)),"string"==h.type){j=h.string;var C=0;/['"]/.test(h.string.charAt(0))&&(f=h.string.charAt(0),j=h.string.slice(1),C++);var D=h.string.length;/['"]/.test(h.string.charAt(D-1))&&(f=h.string.charAt(D-1),j=h.string.substr(C,D-2)),m=!0}for(var u=0;u0){var k=f.character;i.forEach(function(a){k>a&&(k-=1)}),f.character=k}}var l=f.character-1,m=l+1;f.evidence&&(h=f.evidence.substring(l).search(/.\b/),h>-1&&(m+=h)),f.description=f.reason,f.start=f.character,f.end=m,f=c(f),f&&d.push({message:f.description,severity:f.severity,from:a.Pos(f.line-1,l),to:a.Pos(f.line-1,m)})}}}var g=["Dangerous comment"],h=[["Expected '{'","Statement body should be inside '{ }' braces."]],i=["Missing semicolon","Extra comma","Missing property name","Unmatched "," and instead saw"," is not defined","Unclosed string","Stopping, unable to continue"];a.registerHelper("lint","javascript",b)})},{"../../lib/codemirror":59}],32:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.registerHelper("lint","json",function(b){var c=[];if(!window.jsonlint)return window.console&&window.console.error("Error: window.jsonlint not defined, CodeMirror JSON linting cannot run."),c;jsonlint.parseError=function(b,d){var e=d.loc;c.push({from:a.Pos(e.first_line-1,e.first_column),to:a.Pos(e.last_line-1,e.last_column),message:b})};try{jsonlint.parse(b)}catch(d){}return c})})},{"../../lib/codemirror":59}],33:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(b,c){function d(b){return e.parentNode?(e.style.top=Math.max(0,b.clientY-e.offsetHeight-5)+"px",void(e.style.left=b.clientX+5+"px")):a.off(document,"mousemove",d)}var e=document.createElement("div");return e.className="CodeMirror-lint-tooltip",e.appendChild(c.cloneNode(!0)),document.body.appendChild(e),a.on(document,"mousemove",d),d(b),null!=e.style.opacity&&(e.style.opacity=1),e}function c(a){a.parentNode&&a.parentNode.removeChild(a)}function d(a){a.parentNode&&(null==a.style.opacity&&c(a),a.style.opacity=0,setTimeout(function(){c(a)},600))}function e(c,e,f){function g(){a.off(f,"mouseout",g),h&&(d(h),h=null)}var h=b(c,e),i=setInterval(function(){if(h)for(var a=f;;a=a.parentNode){if(a&&11==a.nodeType&&(a=a.host),a==document.body)return;if(!a){g();break}}if(!h)return clearInterval(i)},400);a.on(f,"mouseout",g)}function f(a,b,c){this.marked=[],this.options=b,this.timeout=null,this.hasGutter=c,this.onMouseOver=function(b){r(a,b)},this.waitingFor=0}function g(a,b){return b instanceof Function?{getAnnotations:b}:(b&&b!==!0||(b={}),b)}function h(a){var b=a.state.lint;b.hasGutter&&a.clearGutter(s);for(var c=0;c1,c.options.tooltips))}}d.onUpdateLinting&&d.onUpdateLinting(b,e,a)}function p(a){var b=a.state.lint;b&&(clearTimeout(b.timeout),b.timeout=setTimeout(function(){n(a)},b.options.delay||500))}function q(a,b){for(var c=b.target||b.srcElement,d=document.createDocumentFragment(),f=0;fe)return!1;var f=c.getScrollInfo();if("align"==a.mv.options.connect)q=f.top;else{var h,i,j=.5*f.clientHeight,k=f.top+j,l=c.lineAtHeight(k,"local"),m=D(a.chunks,l,b),n=g(c,b?m.edit:m.orig),o=g(d,b?m.orig:m.edit),p=(k-n.top)/(n.bot-n.top),q=o.top-j+p*(o.bot-o.top);if(q>f.top&&(i=f.top/j)<1)q=q*i+f.top*(1-i);else if((h=f.height-f.clientHeight-f.top)h&&(i=h/j)<1&&(q=q*i+(r.height-r.clientHeight-h)*(1-i))}}return d.scrollTo(f.left,q),d.state.scrollSetAt=e,d.state.scrollSetBy=a,!0}function g(a,b){var c=b.after;return null==c&&(c=a.lastLine()+1),{top:a.heightAtLine(b.before||0,"local"),bot:a.heightAtLine(c,"local")}}function h(a,b,c){a.lockScroll=b,b&&0!=c&&f(a,DIFF_INSERT)&&n(a),a.lockButton.innerHTML=b?"\u21db\u21da":"\u21db  \u21da"}function i(a,b,c){for(var d=c.classLocation,e=0;e20||c.from-f.to>20?(j(a,c.marked,e),m(a,b,d,c.marked,f.from,f.to,e),c.from=f.from,c.to=f.to):(f.fromc.to&&(m(a,b,d,c.marked,c.to,f.to,e),c.to=f.to))})}function l(a,b,c,d,e,f){for(var g=c.classLocation,h=a.getLineHandle(b),i=0;it&&(o&&(h(n,t),o=!1),n=u)}else if(o=!0,r==c){var v=M(i,s,!0),w=P(j,i),x=O(k,v);Q(w,x)||d.push(a.markText(w,x,{className:m})),i=v}}o&&h(n,i.line+1)}function n(a){if(a.showDifferences){if(a.svg){J(a.svg);var b=a.gap.offsetWidth;K(a.svg,"width",b,"height",a.gap.offsetHeight)}a.copyButtons&&J(a.copyButtons);for(var c=a.edit.getViewport(),d=a.orig.getViewport(),e=a.mv.wrap.getBoundingClientRect().top,f=e-a.edit.getScrollerElement().getBoundingClientRect().top+a.edit.getScrollInfo().top,g=e-a.orig.getScrollerElement().getBoundingClientRect().top+a.orig.getScrollInfo().top,h=0;h=c.from&&i.origFrom<=d.to&&i.origTo>=d.from&&v(a,i,g,f,b)}}}function o(a,b){for(var c=0,d=0,e=0;ea&&f.editFrom<=a)return null;if(f.editFrom>a)break;c=f.editTo,d=f.origTo}return d+(a-c)}function p(a,b,c){for(var d=a.state.trackAlignable,e=a.firstLine(),f=0,g=[],h=0;;h++){for(var i=b[h],j=i?c?i.origFrom:i.editFrom:1e9;fl){f++,e--;continue a}if(m.editTo>k){if(m.editFrom<=k)continue a;break}h+=m.origTo-m.origFrom-(m.editTo-m.editFrom),g++}if(k==l-h)i[d]=l,f++;else if(k1&&c.push(u(a[f],b[f],h))}}function u(a,b,c){var d=!0;b>a.lastLine()&&(b--,d=!1);var e=document.createElement("div");return e.className="CodeMirror-merge-spacer",e.style.height=c+"px",e.style.minWidth="1px",a.addLineWidget(b,e,{height:c,above:d,mergeSpacer:!0,handleMouseEvents:!0})}function v(a,b,c,d,e){var f="left"==a.type,g=a.orig.heightAtLine(b.origFrom,"local",!0)-c;if(a.svg){var h=g,i=a.edit.heightAtLine(b.editFrom,"local",!0)-d;if(f){var j=h;h=i,i=j}var k=a.orig.heightAtLine(b.origTo,"local",!0)-c,l=a.edit.heightAtLine(b.editTo,"local",!0)-d;if(f){var j=k;k=l,l=j}var m=" C "+e/2+" "+i+" "+e/2+" "+h+" "+(e+2)+" "+h,n=" C "+e/2+" "+k+" "+e/2+" "+l+" -1 "+l;K(a.svg.appendChild(document.createElementNS(V,"path")),"d","M -1 "+i+m+" L "+(e+2)+" "+k+n+" z","class",a.classes.connect)}if(a.copyButtons){var o=a.copyButtons.appendChild(I("div","left"==a.type?"\u21dd":"\u21dc","CodeMirror-merge-copy")),p=a.mv.options.allowEditingOriginals;if(o.title=p?"Push to left":"Revert chunk",o.chunk=b,o.style.top=(b.origTo>b.origFrom?g:a.edit.heightAtLine(b.editFrom,"local")-d)+"px",p){var q=a.edit.heightAtLine(b.editFrom,"local")-d,r=a.copyButtons.appendChild(I("div","right"==a.type?"\u21dd":"\u21dc","CodeMirror-merge-copy-reverse"));r.title="Push to right",r.chunk={editFrom:b.origFrom,editTo:b.origTo,origFrom:b.editFrom,origTo:b.editTo},r.style.top=q+"px","right"==a.type?r.style.left="2px":r.style.right="2px"}}}function w(a,b,c,d){if(!a.diffOutOfDate){var e=d.origTo>c.lastLine()?U(d.origFrom-1):U(d.origFrom,0),f=U(d.origTo,0),g=d.editTo>b.lastLine()?U(d.editFrom-1):U(d.editFrom,0),h=U(d.editTo,0),i=a.mv.options.revertChunk;i?i(a.mv,c,e,f,b,g,h):b.replaceRange(c.getRange(e,f),g,h)}}function x(b){var c=b.lockButton=I("div",null,"CodeMirror-merge-scrolllock");c.title="Toggle locked scrolling";var d=I("div",[c],"CodeMirror-merge-scrolllock-wrap");a.on(c,"click",function(){h(b,!b.lockScroll)});var e=[d];if(b.mv.options.revertButtons!==!1&&(b.copyButtons=I("div",null,"CodeMirror-merge-copybuttons-"+b.type),a.on(b.copyButtons,"click",function(a){var c=a.target||a.srcElement;if(c.chunk)return"CodeMirror-merge-copy-reverse"==c.className?void w(b,b.orig,b.edit,c.chunk):void w(b,b.edit,b.orig,c.chunk)}),e.unshift(b.copyButtons)),"align"!=b.mv.options.connect){var f=document.createElementNS&&document.createElementNS(V,"svg");f&&!f.createSVGRect&&(f=null),b.svg=f,f&&e.push(f)}return b.gap=I("div",e,"CodeMirror-merge-gap")}function y(a){return"string"==typeof a?a:a.getValue()}function z(a,b,c){Y||(Y=new diff_match_patch);for(var d=Y.diff_main(a,b),e=0;ek&&(g&&b.push({origFrom:d,origTo:l,editFrom:c,editTo:k}),c=n,d=o)}else M(i==DIFF_INSERT?e:f,h[1])}return(c<=e.line||d<=f.line)&&b.push({origFrom:d,origTo:f.line+1,editFrom:c,editTo:e.line+1}),b}function B(a,b){if(b==a.length-1)return!0;var c=a[b+1][1];return!(1==c.length&&b1||b==a.length-3)&&10==c.charCodeAt(0)))}function C(a,b){if(0==b)return!0;var c=a[b-1][1];return 10==c.charCodeAt(c.length-1)&&(1==b||(c=a[b-2][1],10==c.charCodeAt(c.length-1)))}function D(a,b,c){for(var d,e,f,g,h=0;hb?(e=i.editFrom,g=i.origFrom):k>b&&(e=i.editTo,g=i.origTo)),k<=b?(d=i.editTo,f=i.origTo):j<=b&&(d=i.editFrom,f=i.origFrom)}return{edit:{before:d,after:e},orig:{before:f,after:g}}}function E(b,c,d){function e(){g.clear(),b.removeLineClass(c,"wrap","CodeMirror-merge-collapsed-line")}b.addLineClass(c,"wrap","CodeMirror-merge-collapsed-line");var f=document.createElement("span");f.className="CodeMirror-merge-collapsed-widget",f.title="Identical text collapsed. Click to expand.";var g=b.markText(U(c,0),U(d-1),{inclusiveLeft:!0,inclusiveRight:!0,replacedWith:f,clearOnEnter:!0});return a.on(f,"click",e),{mark:g,clear:e}}function F(a,b){function c(){for(var a=0;a=0&&hb){var k=[{line:i,cm:d}];a.left&&k.push({line:o(i,a.left.chunks),cm:a.left.orig}),a.right&&k.push({line:o(i,a.right.chunks),cm:a.right.orig});var l=F(j,k);a.options.onCollapse&&a.options.onCollapse(a,i,j,l)}}}function I(a,b,c,d){var e=document.createElement(a);if(c&&(e.className=c),d&&(e.style.cssText=d),"string"==typeof b)e.appendChild(document.createTextNode(b));else if(b)for(var f=0;f0;--b)a.removeChild(a.firstChild)}function K(a){for(var b=1;b0?a:b}function Q(a,b){return a.line==b.line&&a.ch==b.ch}function R(a,b,c){for(var d=a.length-1;d>=0;d--){var e=a[d],f=(c?e.origTo:e.editTo)-1;if(fb)return f}}function T(b,d){var e=null,f=b.state.diffViews,g=b.getCursor().line;if(f)for(var h=0;he:k0)break}this.signal(),this.alignable.splice(c,0,a,b)},find:function(a){for(var b=0;b-1){var j=this.alignable[f+1];j==$?this.alignable.splice(f,2):this.alignable[f+1]=j&~$}g>-1&&c&&this.set(a+c,$)}},a.commands.goNextDiff=function(a){return T(a,1)},a.commands.goPrevDiff=function(a){return T(a,-1)}})},{"../../lib/codemirror":59}],35:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),"cjs"):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],function(a){d(a,"amd")}):d(CodeMirror,"plain")}(function(b,c){function d(a,b){var c=b;return function(){0==--c&&a()}}function e(a,c){var e=b.modes[a].dependencies;if(!e)return c();for(var f=[],g=0;g-1?e+b.length:e}var f=b.exec(c?a.slice(c):a);return f?f.index+c+(d?f[0].length:0):-1}var d=Array.prototype.slice.call(arguments,1);return{startState:function(){return{outer:a.startState(b),innerActive:null,inner:null}},copyState:function(c){return{outer:a.copyState(b,c.outer),innerActive:c.innerActive,inner:c.innerActive&&a.copyState(c.innerActive.mode,c.inner)}},token:function(e,f){if(f.innerActive){var g=f.innerActive,h=e.string;if(!g.close&&e.sol())return f.innerActive=f.inner=null,this.token(e,f);var i=g.close?c(h,g.close,e.pos,g.parseDelimiters):-1;if(i==e.pos&&!g.parseDelimiters)return e.match(g.close),f.innerActive=f.inner=null,g.delimStyle&&g.delimStyle+" "+g.delimStyle+"-close";i>-1&&(e.string=h.slice(0,i));var j=g.mode.token(e,f.inner);return i>-1&&(e.string=h),i==e.pos&&g.parseDelimiters&&(f.innerActive=f.inner=null),g.innerStyle&&(j=j?j+" "+g.innerStyle:g.innerStyle),j}for(var k=1/0,h=e.string,l=0;l2){d.pending=[];for(var n=2;n-1)return a.Pass;var g=d.indent.length-1,h=b[d.state];a:for(;;){for(var j=0;j=this.string.length},sol:function(){return 0==this.pos},peek:function(){return this.string.charAt(this.pos)||null},next:function(){if(this.posb},eatSpace:function(){for(var a=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++this.pos;return this.pos>a},skipToEnd:function(){this.pos=this.string.length},skipTo:function(a){var b=this.string.indexOf(a,this.pos);if(b>-1)return this.pos=b,!0},backUp:function(a){this.pos-=a},column:function(){return this.start-this.lineStart},indentation:function(){return 0},match:function(a,b,c){if("string"!=typeof a){var d=this.string.slice(this.pos).match(a);return d&&d.index>0?null:(d&&b!==!1&&(this.pos+=d[0].length),d)}var e=function(a){return c?a.toLowerCase():a},f=this.string.substr(this.pos,a.length);if(e(f)==e(a))return b!==!1&&(this.pos+=a.length),!0},current:function(){return this.string.slice(this.start,this.pos)},hideFirstChars:function(a,b){this.lineStart+=a;try{return b()}finally{this.lineStart-=a}},lookAhead:function(){return null}},CodeMirror.StringStream=b,CodeMirror.startState=function(a,b,c){return!a.startState||a.startState(b,c)};var c=CodeMirror.modes={},d=CodeMirror.mimeModes={};CodeMirror.defineMode=function(a,b){arguments.length>2&&(b.dependencies=Array.prototype.slice.call(arguments,2)),c[a]=b},CodeMirror.defineMIME=function(a,b){d[a]=b},CodeMirror.resolveMode=function(a){return"string"==typeof a&&d.hasOwnProperty(a)?a=d[a]:a&&"string"==typeof a.name&&d.hasOwnProperty(a.name)&&(a=d[a.name]),"string"==typeof a?{name:a}:a||{name:"null"}},CodeMirror.getMode=function(a,b){b=CodeMirror.resolveMode(b);var d=c[b.name];if(!d)throw new Error("Unknown mode: "+b);return d(a,b)},CodeMirror.registerHelper=CodeMirror.registerGlobalHelper=Math.min,CodeMirror.defineMode("null",function(){return{token:function(a){a.skipToEnd()}}}),CodeMirror.defineMIME("text/plain","null"),CodeMirror.runMode=function(b,c,d,e){var f=CodeMirror.getMode({indentUnit:2},c);if(1==d.nodeType){var g=e&&e.tabSize||4,h=d,i=0;h.innerHTML="",d=function(a,b){if("\n"==a)return h.appendChild(document.createElement("br")),void(i=0);for(var c="",d=0;;){var e=a.indexOf("\t",d);if(e==-1){c+=a.slice(d),i+=a.length-d;break}i+=e-d,c+=a.slice(d,e);var f=g-i%g;i+=f;for(var j=0;jh)return c.charCoords(a,"local")[b?"top":"bottom"];var d=c.heightAtLine(j,"local");return d+(b?0:j.height)}a!==!1&&this.computeScale();var c=this.cm,d=this.hScale,e=document.createDocumentFragment(),f=this.annotations,g=c.getOption("lineWrapping"),h=g&&1.5*c.defaultTextHeight(),i=null,j=null,k=c.lastLine();if(c.display.barWidth)for(var l,m=0;mk)){for(var o=l||b(n.from,!0)*d,p=b(n.to,!1)*d;mk)&&(l=b(f[m+1].from,!0)*d,!(l>p+.9));)n=f[++m],p=b(n.to,!1)*d;if(p!=o){var q=Math.max(p-o,3),r=e.appendChild(document.createElement("div"));r.style.cssText="position: absolute; right: 0px; width: "+Math.max(c.display.barWidth-1,2)+"px; top: "+(o+this.buttonHeight)+"px; height: "+q+"px",r.className=this.options.className,n.id&&r.setAttribute("annotation-id",n.id)}}}this.div.textContent="",this.div.appendChild(e)},b.prototype.clear=function(){this.cm.off("refresh",this.resizeHandler),this.cm.off("markerAdded",this.resizeHandler),this.cm.off("markerCleared",this.resizeHandler),this.changeHandler&&this.cm.off("change",this.changeHandler),this.div.parentNode.removeChild(this.div)}})},{"../../lib/codemirror":59}],43:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(b,d){a.changeEnd(d).line==b.lastLine()&&c(b)}function c(a){var b="";if(a.lineCount()>1){var d=a.display.scroller.clientHeight-30,e=a.getLineHandle(a.lastLine()).height;b=d-e+"px"}a.state.scrollPastEndPadding!=b&&(a.state.scrollPastEndPadding=b,a.display.lineSpace.parentNode.style.paddingBottom=b,a.off("refresh",c),a.setSize(),a.on("refresh",c))}a.defineOption("scrollPastEnd",!1,function(d,e,f){f&&f!=a.Init&&(d.off("change",b),d.off("refresh",c),d.display.lineSpace.parentNode.style.paddingBottom="",d.state.scrollPastEndPadding=null),e&&(d.on("change",b),d.on("refresh",c),c(d))})})},{"../../lib/codemirror":59}],44:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(b,c,d){function e(b){var c=a.wheelEventPixels(b)["horizontal"==f.orientation?"x":"y"],d=f.pos;f.moveTo(f.pos+c),f.pos!=d&&a.e_preventDefault(b)}this.orientation=c,this.scroll=d,this.screen=this.total=this.size=1,this.pos=0,this.node=document.createElement("div"),this.node.className=b+"-"+c,this.inner=this.node.appendChild(document.createElement("div"));var f=this;a.on(this.inner,"mousedown",function(b){function c(){a.off(document,"mousemove",d),a.off(document,"mouseup",c)}function d(a){return 1!=a.which?c():void f.moveTo(h+(a[e]-g)*(f.total/f.size))}if(1==b.which){a.e_preventDefault(b);var e="horizontal"==f.orientation?"pageX":"pageY",g=b[e],h=f.pos;a.on(document,"mousemove",d),a.on(document,"mouseup",c)}}),a.on(this.node,"click",function(b){a.e_preventDefault(b);var c,d=f.inner.getBoundingClientRect();c="horizontal"==f.orientation?b.clientXd.right?1:0:b.clientYd.bottom?1:0,f.moveTo(f.pos+c*f.screen)}),a.on(this.node,"mousewheel",e),a.on(this.node,"DOMMouseScroll",e)}function c(a,c,d){this.addClass=a,this.horiz=new b(a,"horizontal",d),c(this.horiz.node),this.vert=new b(a,"vertical",d),c(this.vert.node),this.width=null}b.prototype.setPos=function(a,b){return a<0&&(a=0),a>this.total-this.screen&&(a=this.total-this.screen),!(!b&&a==this.pos)&&(this.pos=a,this.inner.style["horizontal"==this.orientation?"left":"top"]=a*(this.size/this.total)+"px",!0)},b.prototype.moveTo=function(a){this.setPos(a)&&this.scroll(a,this.orientation)};var d=10;b.prototype.update=function(a,b,c){var e=this.screen!=b||this.total!=a||this.size!=c;e&&(this.screen=b,this.total=a,this.size=c);var f=this.screen*(this.size/this.total);fa.clientWidth+1,e=a.scrollHeight>a.clientHeight+1;return this.vert.node.style.display=e?"block":"none",this.horiz.node.style.display=d?"block":"none",e&&(this.vert.update(a.scrollHeight,a.clientHeight,a.viewHeight-(d?c:0)),this.vert.node.style.bottom=d?c+"px":"0"),d&&(this.horiz.update(a.scrollWidth,a.clientWidth,a.viewWidth-(e?c:0)-a.barLeft),this.horiz.node.style.right=e?c+"px":"0",this.horiz.node.style.left=a.barLeft+"px"),{right:e?c:0,bottom:d?c:0}},c.prototype.setScrollTop=function(a){this.vert.setPos(a)},c.prototype.setScrollLeft=function(a){this.horiz.setPos(a)},c.prototype.clear=function(){var a=this.horiz.node.parentNode;a.removeChild(this.horiz.node),a.removeChild(this.vert.node)},a.scrollbarModel.simple=function(a,b){return new c("CodeMirror-simplescroll",a,b)},a.scrollbarModel.overlay=function(a,b){return new c("CodeMirror-overlayscroll",a,b)}})},{"../../lib/codemirror":59}],45:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../dialog/dialog")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../dialog/dialog"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c,d,e){a.openDialog?a.openDialog(b,e,{value:d,selectValueOnOpen:!0}):e(prompt(c,d))}function c(a,b){var c=Number(b);return/^[-+]/.test(b)?a.getCursor().line+c:c-1}var d='Jump to line: (Use line:column or scroll% syntax)';a.commands.jumpToLine=function(a){var e=a.getCursor();b(a,d,"Jump to line:",e.line+1+":"+e.ch,function(b){if(b){var d;if(d=/^\s*([\+\-]?\d+)\s*\:\s*(\d+)\s*$/.exec(b))a.setCursor(c(a,d[1]),Number(d[2]));else if(d=/^\s*([\+\-]?\d+(\.\d+)?)\%\s*/.exec(b)){var f=Math.round(a.lineCount()*Number(d[1])/100);/^[-+]/.test(d[1])&&(f=e.line+f+1),a.setCursor(f-1,e.ch)}else(d=/^\s*\:?\s*([\+\-]?\d+)\s*/.exec(b))&&a.setCursor(c(a,d[1]),e.ch)}})},a.keyMap["default"]["Alt-G"]="jumpToLine"})},{"../../lib/codemirror":59,"../dialog/dialog":3}],46:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("./matchesonscrollbar")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","./matchesonscrollbar"],d):d(CodeMirror)}(function(a){"use strict";function b(a){this.options={};for(var b in l)this.options[b]=(a&&a.hasOwnProperty(b)?a:l)[b];this.overlay=this.timeout=null,this.matchesonscroll=null,this.active=!1}function c(a){var b=a.state.matchHighlighter;(b.active||a.hasFocus())&&e(a,b)}function d(a){var b=a.state.matchHighlighter;b.active||(b.active=!0,e(a,b))}function e(a,b){clearTimeout(b.timeout),b.timeout=setTimeout(function(){h(a)},b.options.delay)}function f(a,b,c,d){var e=a.state.matchHighlighter;if(a.addOverlay(e.overlay=k(b,c,d)),e.options.annotateScrollbar&&a.showMatchesOnScrollbar){var f=c?new RegExp("\\b"+b+"\\b"):b;e.matchesonscroll=a.showMatchesOnScrollbar(f,!1,{className:"CodeMirror-selection-highlight-scrollbar"})}}function g(a){var b=a.state.matchHighlighter;b.overlay&&(a.removeOverlay(b.overlay),b.overlay=null,b.matchesonscroll&&(b.matchesonscroll.clear(),b.matchesonscroll=null))}function h(a){a.operation(function(){var b=a.state.matchHighlighter;if(g(a),!a.somethingSelected()&&b.options.showToken){for(var c=b.options.showToken===!0?/[\w$]/:b.options.showToken,d=a.getCursor(),e=a.getLine(d.line),h=d.ch,j=h;h&&c.test(e.charAt(h-1));)--h;for(;j=b.options.minChars&&f(a,m,!1,b.options.style)}})}function i(a,b,c){var d=a.getRange(b,c);if(null!==d.match(/^\w+$/)){if(b.ch>0){var e={line:b.line,ch:b.ch-1},f=a.getRange(e,b);if(null===f.match(/\W/))return!1}if(c.ch=this.gap.to)break;c.to.line>=this.gap.from&&this.matches.splice(b--,1)}for(var e=this.cm.getSearchCursor(this.query,a.Pos(this.gap.from,0),this.caseFold),f=this.options&&this.options.maxMatches||d;e.findNext();){var c={from:e.from(),to:e.to()};if(c.from.line>=this.gap.to)break;if(this.matches.splice(b++,0,c),this.matches.length>f)break}this.gap=null}},b.prototype.onChange=function(b){var d=b.from.line,e=a.changeEnd(b).line,f=e-b.to.line;if(this.gap?(this.gap.from=Math.min(c(this.gap.from,d,f),b.from.line),this.gap.to=Math.max(c(this.gap.to,d,f),b.from.line)):this.gap={from:b.from.line,to:e+1},f)for(var g=0;gb.cursorCoords(c,"window").top&&((k=d).style.opacity=.4)}))};g(b,r,j,m,function(c,e){var f=a.keyName(c),g=b.getOption("extraKeys"),h=g&&g[f]||a.keyMap[b.getOption("keyMap")][f];"findNext"==h||"findPrev"==h||"findPersistentNext"==h||"findPersistentPrev"==h?(a.e_stop(c),l(b,d(b),e),b.execCommand(h)):"find"!=h&&"findPersistent"!=h||(a.e_stop(c),m(e,c))}),f&&j&&(l(b,i,j),n(b,c))}else h(b,r,"Search for:",j,function(a){a&&!i.query&&b.operation(function(){l(b,i,a),i.posFrom=i.posTo=b.getCursor(),n(b,c)})})}function n(b,c,e){b.operation(function(){var g=d(b),h=f(b,g.query,c?g.posFrom:g.posTo);(h.find(c)||(h=f(b,g.query,c?a.Pos(b.lastLine()):a.Pos(b.firstLine(),0)),h.find(c)))&&(b.setSelection(h.from(),h.to()),b.scrollIntoView({from:h.from(),to:h.to()},20),g.posFrom=h.from(),g.posTo=h.to(),e&&e(h.from(),h.to()))})}function o(a){a.operation(function(){var b=d(a);b.lastQuery=b.query,b.query&&(b.query=b.queryText=null,a.removeOverlay(b.overlay),b.annotate&&(b.annotate.clear(),b.annotate=null))})}function p(a,b,c){a.operation(function(){for(var d=f(a,b);d.findNext();)if("string"!=typeof b){var e=a.getRange(d.from(),d.to()).match(b);d.replace(c.replace(/\$(\d)/g,function(a,b){return e[b]}))}else d.replace(c)})}function q(a,b){if(!a.getOption("readOnly")){var c=a.getSelection()||d(a).lastQuery,e=''+(b?"Replace all:":"Replace:")+"";h(a,e+s,e,c,function(c){c&&(c=k(c),h(a,t,"Replace with:","",function(d){if(d=j(d),b)p(a,c,d);else{o(a);var e=f(a,c,a.getCursor("from")),g=function(){var b,j=e.from();!(b=e.findNext())&&(e=f(a,c),!(b=e.findNext())||j&&e.from().line==j.line&&e.from().ch==j.ch)||(a.setSelection(e.from(),e.to()),a.scrollIntoView({from:e.from(),to:e.to()}),i(a,u,"Replace?",[function(){h(b)},g,function(){p(a,c,d)}]))},h=function(a){e.replace("string"==typeof c?d:d.replace(/\$(\d)/g,function(b,c){return a[c]})),g()};g()}}))})}}var r='Search: (Use /re/ syntax for regexp search)',s=' (Use /re/ syntax for regexp search)',t='With: ',u='Replace? ';a.commands.find=function(a){o(a),m(a)},a.commands.findPersistent=function(a){o(a),m(a,!1,!0)},a.commands.findPersistentNext=function(a){m(a,!1,!0,!0)},a.commands.findPersistentPrev=function(a){m(a,!0,!0,!0)},a.commands.findNext=m,a.commands.findPrev=function(a){m(a,!0)},a.commands.clearSearch=o,a.commands.replace=q,a.commands.replaceAll=function(a){q(a,!0)}})},{"../../lib/codemirror":59,"../dialog/dialog":3,"./searchcursor":49}],49:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a){var b=a.flags;return null!=b?b:(a.ignoreCase?"i":"")+(a.global?"g":"")+(a.multiline?"m":"")}function c(a){return a.global?a:new RegExp(a.source,b(a)+"g")}function d(a){return/\\s|\\n|\n|\\W|\\D|\[\^/.test(a.source)}function e(a,b,d){b=c(b);for(var e=d.line,f=d.ch,g=a.lastLine();e<=g;e++,f=0){b.lastIndex=f;var h=a.getLine(e),i=b.exec(h);if(i)return{from:p(e,i.index),to:p(e,i.index+i[0].length),match:i}}}function f(a,b,f){if(!d(b))return e(a,b,f);b=c(b);for(var g,h=1,i=f.line,j=a.lastLine();i<=j;){for(var k=0;k=h;e--,f=-1){var i=a.getLine(e);f>-1&&(i=i.slice(0,f));var j=g(i,b);if(j)return{from:p(e,j.index),to:p(e,j.index+j[0].length),match:j}}}function i(a,b,d){b=c(b);for(var e,f=1,h=d.line,i=a.firstLine();h>=i;){for(var j=0;j>1,h=d(a.slice(0,g)).length;if(h==c)return g;h>c?f=g:e=g+1}}function k(a,b,c,d){if(!b.length)return null;var e=d?n:o,f=e(b).split(/\r|\n\r?/);a:for(var g=c.line,h=c.ch,i=a.lastLine()+1-f.length;g<=i;g++,h=0){var k=a.getLine(g).slice(h),l=e(k);if(1==f.length){var m=l.indexOf(f[0]);if(m==-1)continue a;var c=j(k,l,m,e)+h;return{from:p(g,j(k,l,m,e)+h),to:p(g,j(k,l,m+f[0].length,e)+h)}}var q=l.length-f[0].length;if(l.slice(q)==f[0]){for(var r=1;r=i;g--,h=-1){var k=a.getLine(g);h>-1&&(k=k.slice(0,h));var l=e(k);if(1==f.length){var m=l.lastIndexOf(f[0]);if(m==-1)continue a;return{from:p(g,j(k,l,m,e)),to:p(g,j(k,l,m+f[0].length,e))}}var q=f[f.length-1];if(l.slice(0,q.length)==q){for(var r=1,c=g-f.length+1;r0);)d.push({anchor:e.from(),head:e.to()});d.length&&this.setSelections(d,0)})})},{"../../lib/codemirror":59}],50:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a){for(var b=0;b=c.line,n=m?c:i(l,0),o=a.markText(k,n,{className:f});if(null==d?e.push(o):e.splice(d++,0,o),m)break;g=l}}function e(a){for(var b=a.state.markedSelection,c=0;c1)return f(a);var b=a.getCursor("start"),c=a.getCursor("end"),g=a.state.markedSelection;if(!g.length)return d(a,b,c);var i=g[0].find(),k=g[g.length-1].find();if(!i||!k||c.line-b.line<=h||j(b,k.to)>=0||j(c,i.from)<=0)return f(a);for(;j(b,i.from)>0;)g.shift().clear(),i=g[0].find();for(j(b,i.from)<0&&(i.to.line-b.line0&&(c.line-k.from.line=b.mouseX&&f.top<=b.mouseY&&f.bottom>=b.mouseY&&(d=!0)}var g=d?b.value:"";a.display.lineDiv.style.cursor!=g&&(a.display.lineDiv.style.cursor=g)}}a.defineOption("selectionPointer",!1,function(e,f){var g=e.state.selectionPointer;g&&(a.off(e.getWrapperElement(),"mousemove",g.mousemove),a.off(e.getWrapperElement(),"mouseout",g.mouseout),a.off(window,"scroll",g.windowScroll),e.off("cursorActivity",d),e.off("scroll",d),e.state.selectionPointer=null,e.display.lineDiv.style.cursor=""),f&&(g=e.state.selectionPointer={value:"string"==typeof f?f:"default",mousemove:function(a){b(e,a)},mouseout:function(a){c(e,a)},windowScroll:function(){d(e)},rects:null,mouseX:null,mouseY:null,willUpdate:!1},a.on(e.getWrapperElement(),"mousemove",g.mousemove),a.on(e.getWrapperElement(),"mouseout",g.mouseout),a.on(window,"scroll",g.windowScroll),e.on("cursorActivity",d),e.on("scroll",d))})})},{"../../lib/codemirror":59}],53:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c){var d=a.docs[b];d?c(F(a,d)):a.options.getFile?a.options.getFile(b,c):c(null)}function c(a,b,c){for(var d in a.docs){var e=a.docs[d];if(e.doc==b)return e}if(!c)for(var f=0;;++f)if(d="[doc"+(f||"")+"]",!a.docs[d]){c=d;break}return a.addDoc(c,b)}function d(b,d){return"string"==typeof d?b.docs[d]:(d instanceof a&&(d=d.getDoc()),d instanceof a.Doc?c(b,d):void 0)}function e(a,b,d){var e=c(a,b),g=a.cachedArgHints;g&&g.doc==b&&L(g.start,d.to)>=0&&(a.cachedArgHints=null);var h=e.changed;null==h&&(e.changed=h={from:d.from.line,to:d.from.line});var i=d.from.line+(d.text.length-1);d.from.line=h.to&&(h.to=i+1),h.from>d.from.line&&(h.from=d.from.line),b.lineCount()>J&&d.to-h.from>100&&setTimeout(function(){e.changed&&e.changed.to-e.changed.from>100&&f(a,e)},200)}function f(a,b){a.server.request({files:[{type:"full",name:b.name,text:F(a,b)}]},function(a){a?window.console.error(a):b.changed=null})}function g(b,c,d){b.request(c,{type:"completions",types:!0,docs:!0,urls:!0},function(e,f){if(e)return D(b,c,e);var g=[],i="",j=f.start,k=f.end;'["'==c.getRange(H(j.line,j.ch-2),j)&&'"]'!=c.getRange(k,H(k.line,k.ch+2))&&(i='"]');for(var l=0;l=m;--j){for(var o=c.getLine(j),p=0,q=0;;){var r=o.indexOf("\t",q);if(r==-1)break;p+=i-(r+p)%i-1,q=r+1}if(g=f.column-p,"("==o.charAt(g)){n=!0;break}}if(n){var s=H(j,g),t=b.cachedArgHints;return t&&t.doc==c.getDoc()&&0==L(s,t.start)?k(b,c,h):void b.request(c,{type:"type",preferFunction:!0,end:s},function(a,d){!a&&d.type&&/^fn\(/.test(d.type)&&(b.cachedArgHints={start:s,type:l(d.type),name:d.exprName||d.name||"fn",guess:d.guess,doc:c.getDoc()},k(b,c,h))})}}}}}function k(a,b,c){E(a);for(var d=a.cachedArgHints,e=d.type,f=w("span",d.guess?I+"fhint-guess":null,w("span",I+"fname",d.name),"("),g=0;g\xa0":")")),e.rettype&&f.appendChild(w("span",I+"type",e.rettype));var i=b.cursorCoords(null,"page"),j=a.activeArgHints=A(i.right+1,i.bottom,f);setTimeout(function(){j.clear=z(b,function(){a.activeArgHints==j&&E(a)})},20)}function l(a){function b(b){for(var c=0,e=d;;){var f=a.charAt(d);if(b.test(f)&&!c)return a.slice(e,d);/[{\[\(]/.test(f)?++c:/[}\]\)]/.test(f)&&--c,++d}}var c=[],d=3;if(")"!=a.charAt(d))for(;;){var e=a.slice(d).match(/^([^, \(\[\{]+): /);if(e&&(d+=e[0].length,e=e[1]),c.push({name:e,type:b(/[\),]/)}),")"==a.charAt(d))break;d+=2}var f=a.slice(d).match(/^\) -> (.*)$/);return{args:c,rettype:f&&f[1]}}function m(a,b){function d(d){var e={type:"definition",variable:d||null},f=c(a,b.getDoc());a.server.request(u(a,f,e),function(c,d){if(c)return D(a,b,c);if(!d.file&&d.url)return void window.open(d.url);if(d.file){var e,g=a.docs[d.file];if(g&&(e=p(g.doc,d)))return a.jumpStack.push({file:f.name,start:b.getCursor("from"),end:b.getCursor("to")}),void o(a,f,g,e.start,e.end)}D(a,b,"Could not find a definition.")})}q(b)?d():x(b,"Jump to variable",function(a){a&&d(a)})}function n(a,b){var d=a.jumpStack.pop(),e=d&&a.docs[d.file];e&&o(a,c(a,b.getDoc()),e,d.start,d.end)}function o(a,b,c,d,e){c.doc.setSelection(d,e),b!=c&&a.options.switchToDoc&&(E(a),a.options.switchToDoc(c.name,c.doc))}function p(a,b){for(var c=b.context.slice(0,b.contextOffset).split("\n"),d=b.start.line-(c.length-1),e=H(d,(1==c.length?b.start.ch:a.getLine(d).length)-c[0].length),f=a.getLine(d).slice(e.ch),g=d+1;g=0&&L(h,j.end)<=0&&(g=f.length-1))}b.setSelections(f,g)})}function t(a,b){for(var c=Object.create(null),d=0;dJ&&g!==!1&&b.changed.to-b.changed.from<100&&b.changed.from<=h.line&&b.changed.to>c.end.line){e.push(v(b,h,c.end)),c.file="#0";var f=e[0].offsetLines;null!=c.start&&(c.start=H(c.start.line- -f,c.start.ch)),c.end=H(c.end.line-f,c.end.ch)}else e.push({type:"full",name:b.name,text:F(a,b)}),c.file=b.name,b.changed=null;else c.file=b.name;for(var i in a.docs){var j=a.docs[i];j.changed&&j!=b&&(e.push({type:"full",name:j.name,text:F(a,j)}),j.changed=null)}return{query:c,files:e}}function v(b,c,d){for(var e,f=b.doc,g=null,h=null,i=4,j=c.line-1,k=Math.max(0,j-50);j>=k;--j){var l=f.getLine(j),m=l.search(/\bfunction\b/);if(!(m<0)){var n=a.countColumn(l,null,i);null!=g&&g<=n||(g=n,h=j)}}null==h&&(h=k);var o=Math.min(f.lastLine(),d.line+20);if(null==g||g==a.countColumn(f.getLine(c.line),null,i))e=o;else for(e=d.line+1;e",c):c(prompt(b,""))}function y(b,c,d){function e(){j=!0,i||f()}function f(){b.state.ternTooltip=null,h.parentNode&&C(h),k()}b.state.ternTooltip&&B(b.state.ternTooltip);var g=b.cursorCoords(),h=b.state.ternTooltip=A(g.right+1,g.bottom,c),i=!1,j=!1;a.on(h,"mousemove",function(){i=!0}),a.on(h,"mouseout",function(b){a.contains(h,b.relatedTarget||b.toElement)||(j?f():i=!1)}),setTimeout(e,d.options.hintDelay?d.options.hintDelay:1700);var k=z(b,f)}function z(a,b){return a.on("cursorActivity",b),a.on("blur",b),a.on("scroll",b),a.on("setDoc",b),function(){a.off("cursorActivity",b),a.off("blur",b),a.off("scroll",b),a.off("setDoc",b)}}function A(a,b,c){var d=w("div",I+"tooltip",c);return d.style.left=a+"px",d.style.top=b+"px",document.body.appendChild(d),d}function B(a){var b=a&&a.parentNode;b&&b.removeChild(a)}function C(a){a.style.opacity="0",setTimeout(function(){B(a)},1100)}function D(a,b,c){a.options.showError?a.options.showError(b,c):y(b,String(c),a)}function E(a){a.activeArgHints&&(a.activeArgHints.clear&&a.activeArgHints.clear(),B(a.activeArgHints),a.activeArgHints=null)}function F(a,b){var c=b.doc.getValue();return a.options.fileFilter&&(c=a.options.fileFilter(c,b.name,b.doc)),c}function G(a){function c(a,b){b&&(a.id=++e,f[e]=b),d.postMessage(a)}var d=a.worker=new Worker(a.options.workerScript);d.postMessage({type:"init",defs:a.options.defs,plugins:a.options.plugins,scripts:a.options.workerDeps});var e=0,f={};d.onmessage=function(d){var e=d.data;"getFile"==e.type?b(a,e.name,function(a,b){c({type:"getFile",err:String(a),text:b,id:e.id})}):"debug"==e.type?window.console.log(e.message):e.id&&f[e.id]&&(f[e.id](e.err,e.body),delete f[e.id])},d.onerror=function(a){for(var b in f)f[b](a);f={}},this.addFile=function(a,b){c({type:"add",name:a,text:b})},this.delFile=function(a){c({type:"del",name:a})},this.request=function(a,b){c({type:"req",body:a},b)}}a.TernServer=function(a){var c=this;this.options=a||{};var d=this.options.plugins||(this.options.plugins={});d.doc_comment||(d.doc_comment=!0),this.docs=Object.create(null),this.options.useWorker?this.server=new G(this):this.server=new tern.Server({getFile:function(a,d){return b(c,a,d)},async:!0,defs:this.options.defs||[],plugins:d}),this.trackChange=function(a,b){e(c,a,b)},this.cachedArgHints=null,this.activeArgHints=null,this.jumpStack=[],this.getHint=function(a,b){return g(c,a,b)},this.getHint.async=!0},a.TernServer.prototype={addDoc:function(b,c){var d={doc:c,name:b,changed:null};return this.server.addFile(b,F(this,d)),a.on(c,"change",this.trackChange),this.docs[b]=d},delDoc:function(b){var c=d(this,b);c&&(a.off(c.doc,"change",this.trackChange),delete this.docs[c.name],this.server.delFile(c.name))},hideDoc:function(a){E(this);var b=d(this,a);b&&b.changed&&f(this,b)},complete:function(a){a.showHint({hint:this.getHint})},showType:function(a,b,c){i(this,a,b,"type",c)},showDocs:function(a,b,c){i(this,a,b,"documentation",c)},updateArgHints:function(a){j(this,a)},jumpToDef:function(a){m(this,a)},jumpBack:function(a){n(this,a)},rename:function(a){r(this,a)},selectName:function(a){s(this,a)},request:function(a,b,d,e){var f=this,g=c(this,a.getDoc()),h=u(this,g,b,e),i=h.query&&this.options.queryOptions&&this.options.queryOptions[h.query.type];if(i)for(var j in i)h.query[j]=i[j];this.server.request(h,function(a,c){!a&&f.options.responseFilter&&(c=f.options.responseFilter(g,b,h,a,c)),d(a,c)})},destroy:function(){E(this),this.worker&&(this.worker.terminate(),this.worker=null)}};var H=a.Pos,I="CodeMirror-Tern-",J=250,K=0,L=a.cmpPos})},{"../../lib/codemirror":59}],54:[function(a,b,c){function d(a,b){postMessage({type:"getFile",name:a,id:++g}),h[g]=b}function e(a,b,c){c&&importScripts.apply(null,c),f=new tern.Server({getFile:d,async:!0,defs:a,plugins:b})}var f;this.onmessage=function(a){var b=a.data;switch(b.type){case"init":return e(b.defs,b.plugins,b.scripts);case"add":return f.addFile(b.name,b.text);case"del":return f.delFile(b.name);case"req":return f.request(b.body,function(a,c){postMessage({id:b.id,body:c,err:a&&String(a)})});case"getFile":var c=h[b.id];return delete h[b.id],c(b.err,b.text);default:throw new Error("Unknown message type: "+b.type)}};var g=0,h={};this.console={log:function(a){postMessage({type:"debug",message:a})}}},{}],55:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c){for(var d=c.paragraphStart||a.getHelper(b,"paragraphStart"),e=b.line,f=a.firstLine();e>f;--e){var g=a.getLine(e);if(d&&d.test(g))break;if(!/\S/.test(g)){++e;break}}for(var h=c.paragraphEnd||a.getHelper(b,"paragraphEnd"),i=b.line+1,j=a.lastLine();i<=j;++i){var g=a.getLine(i);if(h&&h.test(g)){++i;break}if(!/\S/.test(g))break}return{from:e,to:i}}function c(a,b,c,d){for(var e=b;e0&&!c.test(a.slice(e-1,e+1));--e);for(var f=!0;;f=!1){var g=e;if(d)for(;" "==a.charAt(g-1);)--g;if(0!=g||!f)return{from:g,to:e};e=b}}function d(b,d,f,g){d=b.clipPos(d),f=b.clipPos(f);var h=g.column||80,i=g.wrapOn||/\s\S|-[^\.\d]/,j=g.killTrailingSpace!==!1,k=[],l="",m=d.line,n=b.getRange(d,f,!1);if(!n.length)return null;for(var o=n[0].match(/^[ \t]*/)[0],p=0;ph&&o==t&&c(l,h,i,j);u&&u.from==r&&u.to==r+s?(l=o+q,++m):k.push({text:[s?" ":""],from:e(m,r),to:e(m+1,t.length)})}for(;l.length>h;){var v=c(l,h,i,j);k.push({text:["",o],from:e(m,v.from),to:e(m,v.to)}),l=o+l.slice(v.to),++m}}return k.length&&b.operation(function(){for(var c=0;c=0;g--){var h,i=c[g];if(i.empty()){var j=b(a,i.head,{});h={from:e(j.from,0),to:e(j.to-1)}}else h={from:i.from(),to:i.to()};h.to.line>=f||(f=h.from.line,d(a,h.from,h.to,{}))}})},a.defineExtension("wrapRange",function(a,b,c){return d(this,a,b,c||{})}),a.defineExtension("wrapParagraphsInRange",function(a,c,f){f=f||{};for(var g=this,h=[],i=a.line;i<=c.line;){var j=b(g,e(i,0),f);h.push(j),i=j.to}var k=!1;return h.length&&g.operation(function(){for(var a=h.length-1;a>=0;--a)k=k||d(g,e(h[a].from,0),e(h[a].to-1),f)}),k})})},{"../../lib/codemirror":59}],56:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../lib/codemirror")):"function"==typeof define&&define.amd?define(["../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b){return a.line==b.line&&a.ch==b.ch}function c(a){I.push(a),I.length>50&&I.shift()}function d(a){return I.length?void(I[I.length-1]+=a):c(a)}function e(a){return I[I.length-(a?Math.min(a,1):1)]||""}function f(){return I.length>1&&I.pop(),e()}function g(a,e,f,g,h){null==h&&(h=a.getRange(e,f)),g&&J&&J.cm==a&&b(e,J.pos)&&a.isClean(J.gen)?d(h):c(h),a.replaceRange("",e,f,"+delete"),J=g?{cm:a,pos:e,gen:a.changeGeneration()}:null}function h(a,b,c){return a.findPosH(b,c,"char",!0)}function i(a,b,c){return a.findPosH(b,c,"word",!0)}function j(a,b,c){return a.findPosV(b,c,"line",a.doc.sel.goalColumn)}function k(a,b,c){return a.findPosV(b,c,"page",a.doc.sel.goalColumn)}function l(a,b,c){for(var d=b.line,e=a.getLine(d),f=/\S/.test(c<0?e.slice(0,b.ch):e.slice(b.ch)),g=a.firstLine(),h=a.lastLine();;){if(d+=c,dh)return a.clipPos(H(d-c,c<0?0:null));e=a.getLine(d);var i=/\S/.test(e);if(i)f=!0;else if(f)return H(d,0)}}function m(a,b,c){for(var d=b.line,e=b.ch,f=a.getLine(b.line),g=!1;;){var h=f.charAt(e+(c<0?-1:0));if(h){if(g&&/[!?.]/.test(h))return H(d,e+(c>0?1:0));g||(g=/\w/.test(h)),e+=c}else{if(d==(c<0?a.firstLine():a.lastLine()))return H(d,e);if(f=a.getLine(d+c),!/\S/.test(f))return H(d,e);d+=c,e=c<0?f.length:0}}}function n(a,c,d){var e;if(a.findMatchingBracket&&(e=a.findMatchingBracket(c,{strict:!0}))&&e.match&&(e.forward?1:-1)==d)return d>0?H(e.to.line,e.to.ch+1):e.to;for(var f=!0;;f=!1){var g=a.getTokenAt(c),h=H(c.line,d<0?g.start:g.end);if(!(f&&d>0&&g.end==c.ch)&&/\w/.test(g.string))return h;var i=a.findPosH(h,d,"char");if(b(h,i))return c;c=i}}function o(a,b){var c=a.state.emacsPrefix;return c?(w(a),"-"==c?-1:Number(c)):b?null:1}function p(a){var b="string"==typeof a?function(b){b.execCommand(a)}:a;return function(a){var c=o(a);b(a);for(var d=1;d1&&"+input"==b.origin){for(var d=b.text.join("\n"),e="",f=1;f',c,{bottom:!0}):c(prompt(b,""))}function D(a,b){var c=a.getCursor(),d=a.findPosH(c,1,"word");a.replaceRange(b(a.getRange(c,d)),c,d),a.setCursor(d)}function E(a){for(var b=a.getCursor(),c=b.line,d=b.ch,e=[];c>=a.firstLine();){for(var f=a.getLine(c),g=null==d?f.length:d;g>0;){var d=f.charAt(--g);if(")"==d)e.push("(");else if("]"==d)e.push("[");else if("}"==d)e.push("{");else if(/[\(\{\[]/.test(d)&&(!e.length||e.pop()!=d))return a.extendSelection(H(c,g))}--c,d=null}}function F(a){a.execCommand("clearSearch"),B(a)}function G(a){M[a]=function(b){u(b,a)},L["Ctrl-"+a]=function(b){u(b,a)},K["Ctrl-"+a]=!0}var H=a.Pos,I=[],J=null,K={"Alt-G":!0,"Ctrl-X":!0,"Ctrl-Q":!0,"Ctrl-U":!0};a.emacs={kill:g,killRegion:t,repeated:p};for(var L=a.keyMap.emacs=a.normalizeKeyMap({"Ctrl-W":function(a){g(a,a.getCursor("start"),a.getCursor("end"))},"Ctrl-K":p(function(a){var b=a.getCursor(),c=a.clipPos(H(b.line)),d=a.getRange(b,c);/\S/.test(d)||(d+="\n",c=H(b.line+1,0)),g(a,b,c,!0,d)}),"Alt-W":function(a){c(a.getSelection()),B(a)},"Ctrl-Y":function(a){var b=a.getCursor();a.replaceRange(e(o(a)),b,b,"paste"),a.setSelection(b,a.getCursor())},"Alt-Y":function(a){a.replaceSelection(f(),"around","paste")},"Ctrl-Space":A,"Ctrl-Shift-2":A,"Ctrl-F":r(h,1),"Ctrl-B":r(h,-1),Right:r(h,1),Left:r(h,-1),"Ctrl-D":function(a){s(a,h,1)},Delete:function(a){t(a)||s(a,h,1)},"Ctrl-H":function(a){s(a,h,-1)},Backspace:function(a){t(a)||s(a,h,-1)},"Alt-F":r(i,1),"Alt-B":r(i,-1),"Alt-D":function(a){s(a,i,1)},"Alt-Backspace":function(a){s(a,i,-1)},"Ctrl-N":r(j,1),"Ctrl-P":r(j,-1),Down:r(j,1),Up:r(j,-1),"Ctrl-A":"goLineStart","Ctrl-E":"goLineEnd",End:"goLineEnd",Home:"goLineStart","Alt-V":r(k,-1),"Ctrl-V":r(k,1),PageUp:r(k,-1),PageDown:r(k,1),"Ctrl-Up":r(l,-1),"Ctrl-Down":r(l,1),"Alt-A":r(m,-1),"Alt-E":r(m,1),"Alt-K":function(a){s(a,m,1)},"Ctrl-Alt-K":function(a){s(a,n,1)},"Ctrl-Alt-Backspace":function(a){s(a,n,-1)},"Ctrl-Alt-F":r(n,1),"Ctrl-Alt-B":r(n,-1),"Shift-Ctrl-Alt-2":function(a){var b=a.getCursor();a.setSelection(q(a,b,n,1),b)},"Ctrl-Alt-T":function(a){var b=n(a,a.getCursor(),-1),c=n(a,b,1),d=n(a,c,1),e=n(a,d,-1);a.replaceRange(a.getRange(e,d)+a.getRange(c,e)+a.getRange(b,c),b,d)},"Ctrl-Alt-U":p(E),"Alt-Space":function(a){for(var b=a.getCursor(),c=b.ch,d=b.ch,e=a.getLine(b.line);c&&/\s/.test(e.charAt(c-1));)--c;for(;d0?a.setCursor(b-1):void C(a,"Goto line",function(b){var c;b&&!isNaN(c=Number(b))&&c==(0|c)&&c>0&&a.setCursor(c-1)})},"Ctrl-X Tab":function(a){a.indentSelection(o(a,!0)||a.getOption("indentUnit"))},"Ctrl-X Ctrl-X":function(a){a.setSelection(a.getCursor("head"),a.getCursor("anchor"))},"Ctrl-X Ctrl-S":"save","Ctrl-X Ctrl-W":"save","Ctrl-X S":"saveAll","Ctrl-X F":"open","Ctrl-X U":p("undo"),"Ctrl-X K":"close","Ctrl-X Delete":function(a){g(a,a.getCursor(),m(a,a.getCursor(),1),!0)},"Ctrl-X H":"selectAll","Ctrl-Q Tab":p("insertTab"),"Ctrl-U":y}),M={"Ctrl-G":w},N=0;N<10;++N)G(String(N));G("-")})},{"../lib/codemirror":59}],57:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../lib/codemirror"),a("../addon/search/searchcursor"),a("../addon/edit/matchbrackets")):"function"==typeof define&&define.amd?define(["../lib/codemirror","../addon/search/searchcursor","../addon/edit/matchbrackets"],d):d(CodeMirror)}(function(a){"use strict";function b(b,c,d){if(d<0&&0==c.ch)return b.clipPos(o(c.line-1));var e=b.getLine(c.line);if(d>0&&c.ch>=e.length)return b.clipPos(o(c.line+1,0));for(var f,g="start",h=c.ch,i=d<0?0:e.length,j=0;h!=i;h+=d,j++){var k=e.charAt(d<0?h-1:h),l="_"!=k&&a.isWordChar(k)?"w":"o";if("w"==l&&k.toUpperCase()==k&&(l="W"),"start"==g)"o"!=l&&(g="in",f=l);else if("in"==g&&f!=l){if("w"==f&&"W"==l&&d<0&&h--,"W"==f&&"w"==l&&d>0){f="w";continue}break}}return o(c.line,h)}function c(a,c){a.extendSelectionsBy(function(d){return a.display.shift||a.doc.extend||d.empty()?b(a.doc,d.head,c):c<0?d.from():d.to()})}function d(b,c){return b.isReadOnly()?a.Pass:(b.operation(function(){for(var a=b.listSelections().length,d=[],e=-1,f=0;f=0;h--){var i=d[f[h]];if(!(j&&a.cmpPos(i.head,j)>0)){var k=e(b,i.head);j=k.from,b.replaceRange(c(k.word),k.from,k.to)}}})}function k(b){var c=b.getCursor("from"),d=b.getCursor("to");if(0==a.cmpPos(c,d)){var f=e(b,c);if(!f.word)return;c=f.from,d=f.to}return{from:c,to:d,query:b.getRange(c,d),word:f}}function l(a,b){var c=k(a);if(c){var d=c.query,e=a.getSearchCursor(d,b?c.to:c.from);(b?e.findNext():e.findPrevious())?a.setSelection(e.from(),e.to()):(e=a.getSearchCursor(d,b?o(a.firstLine(),0):a.clipPos(o(a.lastLine()))),(b?e.findNext():e.findPrevious())?a.setSelection(e.from(),e.to()):c.word&&a.setSelection(c.from,c.to))}}var m=a.keyMap.sublime={fallthrough:"default"},n=a.commands,o=a.Pos,p=a.keyMap["default"]==a.keyMap.macDefault,q=p?"Cmd-":"Ctrl-",r=p?"Ctrl-":"Alt-";n[m[r+"Left"]="goSubwordLeft"]=function(a){c(a,-1)},n[m[r+"Right"]="goSubwordRight"]=function(a){c(a,1)},p&&(m["Cmd-Left"]="goLineStartSmart");var s=p?"Ctrl-Alt-":"Ctrl-";n[m[s+"Up"]="scrollLineUp"]=function(a){var b=a.getScrollInfo();if(!a.somethingSelected()){var c=a.lineAtHeight(b.top+b.clientHeight,"local");a.getCursor().line>=c&&a.execCommand("goLineUp")}a.scrollTo(null,b.top-a.defaultTextHeight())},n[m[s+"Down"]="scrollLineDown"]=function(a){var b=a.getScrollInfo();if(!a.somethingSelected()){var c=a.lineAtHeight(b.top,"local")+1;a.getCursor().line<=c&&a.execCommand("goLineDown")}a.scrollTo(null,b.top+a.defaultTextHeight())},n[m["Shift-"+q+"L"]="splitSelectionByLine"]=function(a){for(var b=a.listSelections(),c=[],d=0;de.line&&g==f.line&&0==f.ch||c.push({anchor:g==e.line?e:o(g,0),head:g==f.line?f:o(g)});a.setSelections(c,0)},m["Shift-Tab"]="indentLess",n[m.Esc="singleSelectionTop"]=function(a){var b=a.listSelections()[0];a.setSelection(b.anchor,b.head,{scroll:!1})},n[m[q+"L"]="selectLine"]=function(a){for(var b=a.listSelections(),c=[],d=0;de?d.push(i,j):d.length&&(d[d.length-1]=j),e=j}b.operation(function(){for(var a=0;ab.lastLine()?b.replaceRange("\n"+g,o(b.lastLine()),null,"+swapLine"):b.replaceRange(g+"\n",o(e,0),null,"+swapLine")}b.setSelections(f),b.scrollIntoView()})},n[m[v+"Down"]="swapLineDown"]=function(b){if(b.isReadOnly())return a.Pass;for(var c=b.listSelections(),d=[],e=b.lastLine()+1,f=c.length-1;f>=0;f--){var g=c[f],h=g.to().line+1,i=g.from().line;0!=g.to().ch||g.empty()||h--,h=0;a-=2){var c=d[a],e=d[a+1],f=b.getLine(c);c==b.lastLine()?b.replaceRange("",o(c-1),o(c),"+swapLine"):b.replaceRange("",o(c,0),o(c+1,0),"+swapLine"),b.replaceRange(f+"\n",o(e,0),null,"+swapLine")}b.scrollIntoView()})},n[m[q+"/"]="toggleCommentIndented"]=function(a){a.toggleComment({indent:!0})},n[m[q+"J"]="joinLines"]=function(a){for(var b=a.listSelections(),c=[],d=0;d=0;e--){var f=c[e].head,g=b.getRange({line:f.line,ch:0},f),h=a.countColumn(g,null,b.getOption("tabSize")),i=b.findPosH(f,-1,"char",!1);if(g&&!/\S/.test(g)&&h%d==0){var j=new o(f.line,a.findColumn(g,h-d,d));j.ch!=f.ch&&(i=j)}b.replaceRange("",i,f,"+delete")}})},n[m[w+q+"K"]="delLineRight"]=function(a){a.operation(function(){for(var b=a.listSelections(),c=b.length-1;c>=0;c--)a.replaceRange("",b[c].anchor,o(b[c].to().line),"+delete");a.scrollIntoView()})},n[m[w+q+"U"]="upcaseAtCursor"]=function(a){j(a,function(a){return a.toUpperCase()})},n[m[w+q+"L"]="downcaseAtCursor"]=function(a){j(a,function(a){return a.toLowerCase()})},n[m[w+q+"Space"]="setSublimeMark"]=function(a){a.state.sublimeMark&&a.state.sublimeMark.clear(),a.state.sublimeMark=a.setBookmark(a.getCursor())},n[m[w+q+"A"]="selectToSublimeMark"]=function(a){var b=a.state.sublimeMark&&a.state.sublimeMark.find();b&&a.setSelection(a.getCursor(),b)},n[m[w+q+"W"]="deleteToSublimeMark"]=function(b){var c=b.state.sublimeMark&&b.state.sublimeMark.find();if(c){var d=b.getCursor(),e=c;if(a.cmpPos(d,e)>0){var f=e;e=d,d=f}b.state.sublimeKilled=b.getRange(d,e),b.replaceRange("",d,e)}},n[m[w+q+"X"]="swapWithSublimeMark"]=function(a){var b=a.state.sublimeMark&&a.state.sublimeMark.find();b&&(a.state.sublimeMark.clear(),a.state.sublimeMark=a.setBookmark(a.getCursor()),a.setCursor(b))},n[m[w+q+"Y"]="sublimeYank"]=function(a){null!=a.state.sublimeKilled&&a.replaceSelection(a.state.sublimeKilled,null,"paste")},m[w+q+"G"]="clearBookmarks",n[m[w+q+"C"]="showInCenter"]=function(a){var b=a.cursorCoords(null,"local");a.scrollTo(null,(b.top+b.bottom)/2-a.getScrollInfo().clientHeight/2)};var x=p?"Ctrl-Shift-":"Ctrl-Alt-";n[m[x+"Up"]="selectLinesUpward"]=function(a){a.operation(function(){for(var b=a.listSelections(),c=0;ca.firstLine()&&a.addSelection(o(d.head.line-1,d.head.ch))}})},n[m[x+"Down"]="selectLinesDownward"]=function(a){a.operation(function(){for(var b=a.listSelections(),c=0;c",type:"keyToKey",toKeys:"h"},{keys:"",type:"keyToKey",toKeys:"l"},{keys:"",type:"keyToKey",toKeys:"k"},{keys:"",type:"keyToKey",toKeys:"j"},{keys:"",type:"keyToKey",toKeys:"l"},{keys:"",type:"keyToKey",toKeys:"h",context:"normal"},{keys:"",type:"keyToKey",toKeys:"W"},{keys:"",type:"keyToKey",toKeys:"B",context:"normal"},{keys:"",type:"keyToKey",toKeys:"w"},{keys:"",type:"keyToKey",toKeys:"b",context:"normal"},{keys:"",type:"keyToKey",toKeys:"j"},{keys:"",type:"keyToKey",toKeys:"k"},{keys:"",type:"keyToKey",toKeys:""},{keys:"",type:"keyToKey",toKeys:""},{keys:"",type:"keyToKey",toKeys:"",context:"insert"},{keys:"",type:"keyToKey",toKeys:"",context:"insert"},{keys:"s",type:"keyToKey",toKeys:"cl",context:"normal"},{keys:"s",type:"keyToKey",toKeys:"c",context:"visual"},{keys:"S",type:"keyToKey",toKeys:"cc",context:"normal"},{keys:"S",type:"keyToKey",toKeys:"VdO",context:"visual"},{keys:"",type:"keyToKey",toKeys:"0"},{keys:"",type:"keyToKey",toKeys:"$"},{keys:"",type:"keyToKey",toKeys:""},{keys:"",type:"keyToKey",toKeys:""},{keys:"",type:"keyToKey",toKeys:"j^",context:"normal"},{keys:"",type:"action",action:"toggleOverwrite",context:"insert"},{keys:"H",type:"motion",motion:"moveToTopLine",motionArgs:{linewise:!0,toJumplist:!0}},{keys:"M",type:"motion",motion:"moveToMiddleLine",motionArgs:{linewise:!0,toJumplist:!0}},{keys:"L",type:"motion",motion:"moveToBottomLine",motionArgs:{linewise:!0,toJumplist:!0}},{keys:"h",type:"motion",motion:"moveByCharacters",motionArgs:{forward:!1}},{keys:"l",type:"motion",motion:"moveByCharacters",motionArgs:{forward:!0}},{keys:"j",type:"motion",motion:"moveByLines",motionArgs:{forward:!0,linewise:!0}},{keys:"k",type:"motion",motion:"moveByLines",motionArgs:{forward:!1,linewise:!0}},{keys:"gj",type:"motion",motion:"moveByDisplayLines",motionArgs:{forward:!0}},{keys:"gk",type:"motion",motion:"moveByDisplayLines",motionArgs:{forward:!1}},{keys:"w",type:"motion",motion:"moveByWords",motionArgs:{forward:!0,wordEnd:!1}},{keys:"W",type:"motion",motion:"moveByWords",motionArgs:{forward:!0,wordEnd:!1,bigWord:!0}},{keys:"e",type:"motion",motion:"moveByWords",motionArgs:{forward:!0,wordEnd:!0,inclusive:!0}},{keys:"E",type:"motion",motion:"moveByWords",motionArgs:{forward:!0,wordEnd:!0,bigWord:!0,inclusive:!0}},{keys:"b",type:"motion",motion:"moveByWords",motionArgs:{forward:!1,wordEnd:!1}},{keys:"B",type:"motion",motion:"moveByWords",motionArgs:{forward:!1,wordEnd:!1,bigWord:!0}},{keys:"ge",type:"motion",motion:"moveByWords",motionArgs:{forward:!1,wordEnd:!0,inclusive:!0}},{keys:"gE",type:"motion",motion:"moveByWords",motionArgs:{forward:!1,wordEnd:!0,bigWord:!0,inclusive:!0}},{keys:"{",type:"motion",motion:"moveByParagraph",motionArgs:{forward:!1,toJumplist:!0}},{keys:"}",type:"motion",motion:"moveByParagraph",motionArgs:{forward:!0,toJumplist:!0}},{keys:"",type:"motion",motion:"moveByPage",motionArgs:{forward:!0}},{keys:"",type:"motion",motion:"moveByPage",motionArgs:{forward:!1}},{keys:"",type:"motion",motion:"moveByScroll",motionArgs:{forward:!0,explicitRepeat:!0}},{keys:"",type:"motion",motion:"moveByScroll",motionArgs:{forward:!1,explicitRepeat:!0}},{keys:"gg",type:"motion",motion:"moveToLineOrEdgeOfDocument",motionArgs:{forward:!1,explicitRepeat:!0,linewise:!0,toJumplist:!0}},{keys:"G",type:"motion",motion:"moveToLineOrEdgeOfDocument",motionArgs:{forward:!0,explicitRepeat:!0,linewise:!0,toJumplist:!0}},{keys:"0",type:"motion",motion:"moveToStartOfLine"},{keys:"^",type:"motion",motion:"moveToFirstNonWhiteSpaceCharacter"},{keys:"+",type:"motion",motion:"moveByLines",motionArgs:{forward:!0,toFirstChar:!0}},{keys:"-",type:"motion",motion:"moveByLines",motionArgs:{forward:!1,toFirstChar:!0}},{keys:"_",type:"motion",motion:"moveByLines",motionArgs:{forward:!0,toFirstChar:!0,repeatOffset:-1}},{keys:"$",type:"motion",motion:"moveToEol",motionArgs:{inclusive:!0}},{keys:"%",type:"motion",motion:"moveToMatchedSymbol",motionArgs:{inclusive:!0,toJumplist:!0}},{keys:"f",type:"motion",motion:"moveToCharacter",motionArgs:{forward:!0,inclusive:!0}},{keys:"F",type:"motion",motion:"moveToCharacter",motionArgs:{forward:!1}},{keys:"t",type:"motion",motion:"moveTillCharacter",motionArgs:{forward:!0,inclusive:!0}},{keys:"T",type:"motion",motion:"moveTillCharacter",motionArgs:{forward:!1}},{keys:";",type:"motion",motion:"repeatLastCharacterSearch",motionArgs:{forward:!0}},{keys:",",type:"motion",motion:"repeatLastCharacterSearch",motionArgs:{forward:!1}},{keys:"'",type:"motion",motion:"goToMark",motionArgs:{toJumplist:!0,linewise:!0}},{keys:"`",type:"motion",motion:"goToMark",motionArgs:{toJumplist:!0}},{keys:"]`",type:"motion",motion:"jumpToMark",motionArgs:{forward:!0}},{keys:"[`",type:"motion",motion:"jumpToMark",motionArgs:{forward:!1}},{keys:"]'",type:"motion",motion:"jumpToMark",motionArgs:{forward:!0,linewise:!0}},{keys:"['",type:"motion",motion:"jumpToMark",motionArgs:{forward:!1,linewise:!0}},{keys:"]p",type:"action",action:"paste",isEdit:!0,actionArgs:{after:!0,isEdit:!0,matchIndent:!0}},{keys:"[p",type:"action",action:"paste",isEdit:!0,actionArgs:{after:!1,isEdit:!0,matchIndent:!0}},{keys:"]",type:"motion",motion:"moveToSymbol",motionArgs:{forward:!0,toJumplist:!0}},{keys:"[",type:"motion",motion:"moveToSymbol",motionArgs:{forward:!1,toJumplist:!0}},{keys:"|",type:"motion",motion:"moveToColumn"},{keys:"o",type:"motion",motion:"moveToOtherHighlightedEnd",context:"visual"},{keys:"O",type:"motion",motion:"moveToOtherHighlightedEnd",motionArgs:{sameLine:!0},context:"visual"},{keys:"d",type:"operator",operator:"delete"},{keys:"y",type:"operator",operator:"yank"},{keys:"c",type:"operator",operator:"change"},{keys:">",type:"operator",operator:"indent",operatorArgs:{indentRight:!0}},{keys:"<",type:"operator",operator:"indent",operatorArgs:{indentRight:!1}},{keys:"g~",type:"operator",operator:"changeCase"},{keys:"gu",type:"operator",operator:"changeCase",operatorArgs:{toLower:!0},isEdit:!0},{keys:"gU",type:"operator",operator:"changeCase",operatorArgs:{toLower:!1},isEdit:!0},{keys:"n",type:"motion",motion:"findNext",motionArgs:{forward:!0,toJumplist:!0}},{keys:"N",type:"motion",motion:"findNext",motionArgs:{forward:!1,toJumplist:!0}},{keys:"x",type:"operatorMotion",operator:"delete",motion:"moveByCharacters",motionArgs:{forward:!0},operatorMotionArgs:{visualLine:!1}},{keys:"X",type:"operatorMotion",operator:"delete",motion:"moveByCharacters",motionArgs:{forward:!1},operatorMotionArgs:{visualLine:!0}},{keys:"D",type:"operatorMotion",operator:"delete",motion:"moveToEol",motionArgs:{inclusive:!0},context:"normal"},{keys:"D",type:"operator",operator:"delete",operatorArgs:{linewise:!0},context:"visual"},{keys:"Y",type:"operatorMotion",operator:"yank",motion:"expandToLine",motionArgs:{linewise:!0},context:"normal"},{keys:"Y",type:"operator",operator:"yank",operatorArgs:{linewise:!0},context:"visual"},{keys:"C",type:"operatorMotion",operator:"change",motion:"moveToEol",motionArgs:{inclusive:!0},context:"normal"},{keys:"C",type:"operator",operator:"change",operatorArgs:{linewise:!0},context:"visual"},{keys:"~",type:"operatorMotion",operator:"changeCase",motion:"moveByCharacters",motionArgs:{forward:!0},operatorArgs:{shouldMoveCursor:!0},context:"normal"},{keys:"~",type:"operator",operator:"changeCase",context:"visual"},{keys:"",type:"operatorMotion",operator:"delete",motion:"moveByWords",motionArgs:{forward:!1,wordEnd:!1},context:"insert"},{keys:"",type:"action",action:"jumpListWalk",actionArgs:{forward:!0}},{keys:"",type:"action",action:"jumpListWalk",actionArgs:{forward:!1}},{keys:"",type:"action",action:"scroll",actionArgs:{forward:!0,linewise:!0}},{keys:"",type:"action",action:"scroll",actionArgs:{forward:!1,linewise:!0}},{keys:"a",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"charAfter"},context:"normal"},{keys:"A",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"eol"},context:"normal"},{keys:"A",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"endOfSelectedArea"},context:"visual"},{keys:"i",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"inplace"},context:"normal"},{keys:"I",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"firstNonBlank"},context:"normal"},{keys:"I",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{insertAt:"startOfSelectedArea"},context:"visual"},{keys:"o",type:"action",action:"newLineAndEnterInsertMode",isEdit:!0,interlaceInsertRepeat:!0,actionArgs:{after:!0},context:"normal"},{keys:"O",type:"action",action:"newLineAndEnterInsertMode",isEdit:!0,interlaceInsertRepeat:!0,actionArgs:{after:!1},context:"normal"},{keys:"v",type:"action",action:"toggleVisualMode"},{keys:"V",type:"action",action:"toggleVisualMode",actionArgs:{linewise:!0}},{keys:"",type:"action",action:"toggleVisualMode",actionArgs:{blockwise:!0}},{keys:"",type:"action",action:"toggleVisualMode",actionArgs:{blockwise:!0}},{keys:"gv",type:"action",action:"reselectLastSelection"},{keys:"J",type:"action",action:"joinLines",isEdit:!0},{keys:"p",type:"action",action:"paste",isEdit:!0,actionArgs:{after:!0,isEdit:!0}},{keys:"P",type:"action",action:"paste",isEdit:!0,actionArgs:{after:!1,isEdit:!0}},{keys:"r",type:"action",action:"replace",isEdit:!0},{keys:"@",type:"action",action:"replayMacro"},{keys:"q",type:"action",action:"enterMacroRecordMode"},{keys:"R",type:"action",action:"enterInsertMode",isEdit:!0,actionArgs:{replace:!0}},{keys:"u",type:"action",action:"undo",context:"normal"},{keys:"u",type:"operator",operator:"changeCase",operatorArgs:{toLower:!0},context:"visual",isEdit:!0},{keys:"U",type:"operator",operator:"changeCase",operatorArgs:{toLower:!1},context:"visual",isEdit:!0},{keys:"",type:"action",action:"redo"},{keys:"m",type:"action",action:"setMark"},{keys:'"',type:"action",action:"setRegister"},{keys:"zz",type:"action",action:"scrollToCursor",actionArgs:{position:"center"}},{keys:"z.",type:"action",action:"scrollToCursor",actionArgs:{position:"center"},motion:"moveToFirstNonWhiteSpaceCharacter"},{keys:"zt",type:"action",action:"scrollToCursor",actionArgs:{position:"top"}},{keys:"z",type:"action",action:"scrollToCursor",actionArgs:{position:"top"},motion:"moveToFirstNonWhiteSpaceCharacter"},{keys:"z-",type:"action",action:"scrollToCursor",actionArgs:{position:"bottom"}},{keys:"zb",type:"action",action:"scrollToCursor",actionArgs:{position:"bottom"},motion:"moveToFirstNonWhiteSpaceCharacter"},{keys:".",type:"action",action:"repeatLastEdit"},{keys:"",type:"action",action:"incrementNumberToken",isEdit:!0,actionArgs:{increase:!0,backtrack:!1}},{keys:"",type:"action",action:"incrementNumberToken",isEdit:!0,actionArgs:{increase:!1,backtrack:!1}},{keys:"",type:"action",action:"indent",actionArgs:{indentRight:!0},context:"insert"},{keys:"",type:"action",action:"indent",actionArgs:{indentRight:!1},context:"insert"},{keys:"a",type:"motion",motion:"textObjectManipulation"},{keys:"i",type:"motion",motion:"textObjectManipulation",motionArgs:{textObjectInner:!0}},{keys:"/",type:"search",searchArgs:{forward:!0,querySrc:"prompt",toJumplist:!0}},{keys:"?",type:"search",searchArgs:{forward:!1,querySrc:"prompt",toJumplist:!0}},{keys:"*",type:"search",searchArgs:{forward:!0,querySrc:"wordUnderCursor",wholeWordOnly:!0,toJumplist:!0}},{keys:"#",type:"search",searchArgs:{forward:!1,querySrc:"wordUnderCursor",wholeWordOnly:!0,toJumplist:!0}},{keys:"g*",type:"search",searchArgs:{forward:!0,querySrc:"wordUnderCursor",toJumplist:!0}},{keys:"g#",type:"search",searchArgs:{forward:!1,querySrc:"wordUnderCursor",toJumplist:!0}},{keys:":",type:"ex"}],c=[{name:"colorscheme",shortName:"colo"},{name:"map"},{name:"imap",shortName:"im"},{name:"nmap",shortName:"nm"},{name:"vmap",shortName:"vm"},{name:"unmap"},{name:"write",shortName:"w"},{name:"undo",shortName:"u"},{name:"redo",shortName:"red"},{name:"set",shortName:"se"},{name:"set",shortName:"se"},{name:"setlocal",shortName:"setl"},{name:"setglobal",shortName:"setg"},{name:"sort",shortName:"sor"},{name:"substitute",shortName:"s",possiblyAsync:!0},{name:"nohlsearch",shortName:"noh"},{name:"yank",shortName:"y"},{name:"delmarks",shortName:"delm"},{name:"registers",shortName:"reg",excludeFromCommandHistory:!0},{name:"global",shortName:"g"}],d=a.Pos,e=function(){function e(b){b.setOption("disableInput",!0),b.setOption("showCursorWhenSelecting",!1),a.signal(b,"vim-mode-change",{mode:"normal"}),b.on("cursorActivity",bb),x(b),a.on(b.getInputField(),"paste",k(b))}function f(b){b.setOption("disableInput",!1),b.off("cursorActivity",bb),a.off(b.getInputField(),"paste",k(b)),b.state.vim=null}function g(b,c){this==a.keyMap.vim&&a.rmClass(b.getWrapperElement(),"cm-fat-cursor"),c&&c.attach==h||f(b)}function h(b,c){this==a.keyMap.vim&&a.addClass(b.getWrapperElement(),"cm-fat-cursor"),c&&c.attach==h||e(b)}function i(b,c){if(c){if(this[b])return this[b];var d=j(b);if(!d)return!1;var e=a.Vim.findKey(c,d);return"function"==typeof e&&a.signal(c,"vim-keypress",d),e}}function j(a){if("'"==a.charAt(0))return a.charAt(1);var b=a.split(/-(?!$)/),c=b[b.length-1];if(1==b.length&&1==b[0].length)return!1;if(2==b.length&&"Shift"==b[0]&&1==c.length)return!1;for(var d=!1,e=0;e")}function k(a){var b=a.state.vim;return b.onPasteFn||(b.onPasteFn=function(){b.insertMode||(a.setCursor(L(a.getCursor(),0,1)),Bb.enterInsertMode(a,{},b))}),b.onPasteFn}function l(a,b){for(var c=[],d=a;d=a.firstLine()&&b<=a.lastLine()}function n(a){return/^[a-z]$/.test(a)}function o(a){return"()[]{}".indexOf(a)!=-1}function p(a){return kb.test(a)}function q(a){return/^[A-Z]$/.test(a)}function r(a){return/^\s*$/.test(a)}function s(a,b){for(var c=0;c"==b.slice(-11)){var c=b.length-11,d=a.slice(0,c),e=b.slice(0,c);return d==e&&a.length>c?"full":0==e.indexOf(d)&&"partial"}return a==b?"full":0==b.indexOf(a)&&"partial"}function P(a){var b=/^.*(<[^>]+>)$/.exec(a),c=b?b[1]:a.slice(-1);if(c.length>1)switch(c){case"":c="\n";break;case"":c=" ";break;default:c=""}return c}function Q(a,b,c){return function(){for(var d=0;d2&&(b=U.apply(void 0,Array.prototype.slice.call(arguments,1))),T(a,b)?a:b}function V(a,b){return arguments.length>2&&(b=V.apply(void 0,Array.prototype.slice.call(arguments,1))),T(a,b)?b:a}function W(a,b,c){var d=T(a,b),e=T(b,c);return d&&e}function X(a,b){return a.getLine(b).length}function Y(a){return a.trim?a.trim():a.replace(/^\s+|\s+$/g,"")}function Z(a){return a.replace(/([.?*+$\[\]\/\\(){}|\-])/g,"\\$1")}function $(a,b,c){var e=X(a,b),f=new Array(c-e+1).join(" ");a.setCursor(d(b,e)),a.replaceRange(f,a.getCursor())}function _(a,b){var c=[],e=a.listSelections(),f=R(a.clipPos(b)),g=!S(b,f),h=a.getCursor("head"),i=ba(e,h),j=S(e[i].head,e[i].anchor),k=e.length-1,l=k-i>i?k:0,m=e[l].anchor,n=Math.min(m.line,f.line),o=Math.max(m.line,f.line),p=m.ch,q=f.ch,r=e[l].head.ch-p,s=q-p;r>0&&s<=0?(p++,g||q--):r<0&&s>=0?(p--,j||q++):r<0&&s==-1&&(p--,q++);for(var t=n;t<=o;t++){var u={anchor:new d(t,p),head:new d(t,q)};c.push(u)}return a.setSelections(c),b.ch=q,m.ch=p,m}function aa(a,b,c){for(var d=[],e=0;ej&&(f.line=j),f.ch=X(a,f.line)}return{ranges:[{anchor:g,head:f}],primary:0}}if("block"==c){for(var k=Math.min(g.line,f.line),l=Math.min(g.ch,f.ch),m=Math.max(g.line,f.line),n=Math.max(g.ch,f.ch)+1,o=m-k+1,p=f.line==k?0:o-1,q=[],r=0;r0&&f&&r(f);f=e.pop())c.line--,c.ch=0;f?(c.line--,c.ch=X(a,c.line)):c.ch=0}}function ka(a,b,c){b.ch=0,c.ch=0,c.line++}function la(a){if(!a)return 0;var b=a.search(/\S/);return b==-1?a.length:b}function ma(a,b,c,e,f){for(var g=ha(a),h=a.getLine(g.line),i=g.ch,j=f?lb[0]:mb[0];!j(h.charAt(i));)if(i++,i>=h.length)return null;e?j=mb[0]:(j=lb[0],j(h.charAt(i))||(j=lb[1]));for(var k=i,l=i;j(h.charAt(k))&&k=0;)l--;if(l++,b){for(var m=k;/\s/.test(h.charAt(k))&&k0;)l--;l||(l=n)}}return{start:d(g.line,l),end:d(g.line,k)}}function na(a,b,c){S(b,c)||vb.jumpList.add(a,b,c)}function oa(a,b){vb.lastCharacterSearch.increment=a,vb.lastCharacterSearch.forward=b.forward,vb.lastCharacterSearch.selectedCharacter=b.selectedCharacter}function pa(a,b,c,e){var f=R(a.getCursor()),g=c?1:-1,h=c?a.lineCount():-1,i=f.ch,j=f.line,k=a.getLine(j),l={lineText:k,nextCh:k.charAt(i),lastCh:null,index:i,symb:e,reverseSymb:(c?{")":"(","}":"{"}:{"(":")","{":"}"})[e],forward:c,depth:0,curMoveThrough:!1},m=Cb[e];if(!m)return f;var n=Db[m].init,o=Db[m].isComplete;for(n&&n(l);j!==h&&b;){if(l.index+=g,l.nextCh=l.lineText.charAt(l.index),!l.nextCh){if(j+=g,l.lineText=a.getLine(j)||"",g>0)l.index=0;else{var p=l.lineText.length;l.index=p>0?p-1:0}l.nextCh=l.lineText.charAt(l.index)}o(l)&&(f.line=j,f.ch=l.index,b--)}return l.nextCh||l.curMoveThrough?d(j,l.index):f}function qa(a,b,c,d,e){var f=b.line,g=b.ch,h=a.getLine(f),i=c?1:-1,j=d?mb:lb;if(e&&""==h){if(f+=i,h=a.getLine(f),!m(a,f))return null;g=c?0:h.length}for(;;){if(e&&""==h)return{from:0,to:0,line:f};for(var k=i>0?h.length:-1,l=k,n=k;g!=k;){for(var o=!1,p=0;p0?0:h.length}}function ra(a,b,c,e,f,g){var h=R(b),i=[];(e&&!f||!e&&f)&&c++;for(var j=!(e&&f),k=0;k0;)h(n,e)&&c--,n+=e;return new d(n,0)}var o=a.state.vim;if(o.visualLine&&h(k,1,!0)){var p=o.sel.anchor;h(p.line,-1,!0)&&(f&&p.line==k||(k+=1))}var q=g(k);for(n=k;n<=m&&c;n++)h(n,1,!0)&&(f&&g(n)==q||c--);for(j=new d(n,0),n>m&&!q?q=!0:f=!1,n=k;n>l&&(f&&g(n)!=q&&n!=k||!h(n,-1,!0));n--);return i=new d(n,0),{start:i,end:j}}function xa(a,b,c,e){var f,g,h=b,i={"(":/[()]/,")":/[()]/,"[":/[[\]]/,"]":/[[\]]/,"{":/[{}]/,"}":/[{}]/}[c],j={"(":"(",")":"(","[":"[","]":"[","{":"{","}":"{"}[c],k=a.getLine(h.line).charAt(h.ch),l=k===j?1:0;if(f=a.scanForBracket(d(h.line,h.ch+l),-1,null,{bracketRegex:i}),g=a.scanForBracket(d(h.line,h.ch+l),1,null,{bracketRegex:i}),!f||!g)return{start:h,end:h};if(f=f.pos,g=g.pos,f.line==g.line&&f.ch>g.ch||f.line>g.line){var m=f;f=g,g=m}return e?g.ch+=1:f.ch+=1,{start:f,end:g}}function ya(a,b,c,e){var f,g,h,i,j=R(b),k=a.getLine(j.line),l=k.split(""),m=l.indexOf(c);if(j.ch-1&&!f;h--)l[h]==c&&(f=h+1);else f=j.ch+1;if(f&&!g)for(h=f,i=l.length;h'+b+"",{bottom:!0,duration:5e3}):alert(b)}function Ja(a,b){var c=''+(a||"")+'';return b&&(c+=' '+b+""),c}function Ka(a,b){var c=(b.prefix||"")+" "+(b.desc||""),d=Ja(b.prefix,b.desc);Ba(a,d,c,b.onClose,b)}function La(a,b){if(a instanceof RegExp&&b instanceof RegExp){for(var c=["global","multiline","ignoreCase","source"],d=0;d=b&&a<=c:a==b}function Sa(a){var b=a.getScrollInfo(),c=6,d=10,e=a.coordsChar({left:0,top:c+b.top},"local"),f=b.clientHeight-d+b.top,g=a.coordsChar({left:0,top:f},"local");return{top:e.line,bottom:g.line}}function Ta(a,b,c){if("'"==c){var d=a.doc.history.done,e=d[d.length-2];return e&&e.ranges&&e.ranges[0].head}var f=b.marks[c];return f&&f.find()}function Ua(b,c,d,e,f,g,h,i,j){function k(){b.operation(function(){for(;!p;)l(),m();n()})}function l(){var a=b.getRange(g.from(),g.to()),c=a.replace(h,i);g.replace(c)}function m(){for(;g.findNext()&&Ra(g.from(),e,f);)if(d||!q||g.from().line!=q.line)return b.scrollIntoView(g.from(),30),b.setSelection(g.from(),g.to()),q=g.from(),void(p=!1);p=!0}function n(a){if(a&&a(),b.focus(),q){b.setCursor(q);var c=b.state.vim;c.exMode=!1,c.lastHPos=c.lastHSPos=q.ch}j&&j()}function o(c,d,e){a.e_stop(c);var f=a.keyName(c);switch(f){case"Y":l(),m();break;case"N":m();break;case"A":var g=j;j=void 0,b.operation(k),j=g;break;case"L":l();case"Q":case"Esc":case"Ctrl-C":case"Ctrl-[":n(e)}return p&&n(e),!0}b.state.vim.exMode=!0;var p=!1,q=g.from();return m(),p?void Ia(b,"No matches for "+h.source):c?void Ka(b,{prefix:"replace with "+i+" (y/n/a/q/l)",onKeyDown:o}):(k(),void(j&&j()))}function Va(b){var c=b.state.vim,d=vb.macroModeState,e=vb.registerController.getRegister("."),f=d.isPlaying,g=d.lastInsertModeChanges,h=[];if(!f){for(var i=g.inVisualBlock?c.lastSelection.visualBlock.height:1,j=g.changes,h=[],k=0;k1&&(gb(b,c,c.insertModeRepeat-1,!0),c.lastEditInputState.repeatOverride=c.insertModeRepeat),delete c.insertModeRepeat,c.insertMode=!1,b.setCursor(b.getCursor().line,b.getCursor().ch-1),b.setOption("keyMap","vim"),b.setOption("disableInput",!0),b.toggleOverwrite(!1),e.setText(g.changes.join("")),a.signal(b,"vim-mode-change",{mode:"normal"}),d.isRecording&&$a(d)}function Wa(a){b.unshift(a)}function Xa(a,b,c,d,e){var f={keys:a,type:b};f[b]=c,f[b+"Args"]=d;for(var g in e)f[g]=e[g];Wa(f)}function Ya(b,c,d,e){var f=vb.registerController.getRegister(e);if(":"==e)return f.keyBuffer[0]&&Jb.processCommand(b,f.keyBuffer[0]),void(d.isPlaying=!1);var g=f.keyBuffer,h=0;d.isPlaying=!0,d.replaySearchQueries=f.searchQueries.slice(0);for(var i=0;i|<\w+>|./.exec(l),k=j[0],l=l.substring(j.index+k.length),a.Vim.handleKey(b,k,"macro"),c.insertMode){var m=f.insertModeChanges[h++].changes;vb.macroModeState.lastInsertModeChanges.changes=m,hb(b,m,1),Va(b)}d.isPlaying=!1}function Za(a,b){if(!a.isPlaying){var c=a.latestRegister,d=vb.registerController.getRegister(c);d&&d.pushText(b)}}function $a(a){if(!a.isPlaying){var b=a.latestRegister,c=vb.registerController.getRegister(b);c&&c.pushInsertModeChanges&&c.pushInsertModeChanges(a.lastInsertModeChanges)}}function _a(a,b){if(!a.isPlaying){var c=a.latestRegister,d=vb.registerController.getRegister(c);d&&d.pushSearchQuery&&d.pushSearchQuery(b)}}function ab(a,b){var c=vb.macroModeState,d=c.lastInsertModeChanges;if(!c.isPlaying)for(;b;){if(d.expectCursorActivityForChange=!0,"+input"==b.origin||"paste"==b.origin||void 0===b.origin){var e=b.text.join("\n");d.maybeReset&&(d.changes=[],d.maybeReset=!1),a.state.overwrite&&!/\n/.test(e)?d.changes.push([e]):d.changes.push(e)}b=b.next}}function bb(a){var b=a.state.vim;if(b.insertMode){var c=vb.macroModeState;if(c.isPlaying)return;var d=c.lastInsertModeChanges;d.expectCursorActivityForChange?d.expectCursorActivityForChange=!1:d.maybeReset=!0}else a.curOp.isVimOp||db(a,b);b.visualMode&&cb(a)}function cb(a){var b=a.state.vim,c=J(a,R(b.sel.head)),d=L(c,0,1);b.fakeCursor&&b.fakeCursor.clear(),b.fakeCursor=a.markText(c,d,{className:"cm-animate-fat-cursor"})}function db(b,c){var d=b.getCursor("anchor"),e=b.getCursor("head");if(c.visualMode&&!b.somethingSelected()?ia(b,!1):c.visualMode||c.insertMode||!b.somethingSelected()||(c.visualMode=!0,c.visualLine=!1,a.signal(b,"vim-mode-change",{mode:"visual"})),c.visualMode){var f=T(e,d)?0:-1,g=T(e,d)?-1:0;e=L(e,0,f),d=L(d,0,g),c.sel={anchor:d,head:e},ua(b,c,"<",U(e,d)),ua(b,c,">",V(e,d))}else c.insertMode||(c.lastHPos=b.getCursor().ch)}function eb(a){this.keyName=a}function fb(b){function c(){return e.maybeReset&&(e.changes=[],e.maybeReset=!1),e.changes.push(new eb(f)),!0}var d=vb.macroModeState,e=d.lastInsertModeChanges,f=a.keyName(b);f&&(f.indexOf("Delete")==-1&&f.indexOf("Backspace")==-1||a.lookupKey(f,"vim-insert",c))}function gb(a,b,c,d){function e(){h?yb.processAction(a,b,b.lastEditActionCommand):yb.evalInput(a,b)}function f(c){if(g.lastInsertModeChanges.changes.length>0){c=b.lastEditActionCommand?c:1;var d=g.lastInsertModeChanges;hb(a,d.changes,c)}}var g=vb.macroModeState;g.isPlaying=!0;var h=!!b.lastEditActionCommand,i=b.inputState;if(b.inputState=b.lastEditInputState,h&&b.lastEditActionCommand.interlaceInsertRepeat)for(var j=0;j"]),rb=[].concat(nb,ob,pb,["-",'"',".",":","/"]),sb={};t("filetype",void 0,"string",["ft"],function(a,b){if(void 0!==b){if(void 0===a){var c=b.getOption("mode");return"null"==c?"":c}var c=""==a?"null":a;b.setOption("mode",c)}});var tb=function(){function a(a,b,h){function i(b){var e=++d%c,f=g[e];f&&f.clear(),g[e]=a.setBookmark(b)}var j=d%c,k=g[j];if(k){var l=k.find();l&&!S(l,b)&&i(b)}else i(b);i(h),e=d,f=d-c+1,f<0&&(f=0)}function b(a,b){d+=b,d>e?d=e:d0?1:-1,k=a.getCursor();do if(d+=j,h=g[(c+d)%c],h&&(i=h.find())&&!S(k,i))break;while(df)}return h}var c=100,d=-1,e=0,f=0,g=new Array(c);return{cachedCursor:void 0,add:a,move:b}},ub=function(a){return a?{changes:a.changes,expectCursorActivityForChange:a.expectCursorActivityForChange}:{changes:[],expectCursorActivityForChange:!1}};w.prototype={exitMacroRecordMode:function(){var a=vb.macroModeState;a.onRecordingDone&&a.onRecordingDone(),a.onRecordingDone=void 0,a.isRecording=!1},enterMacroRecordMode:function(a,b){var c=vb.registerController.getRegister(b);c&&(c.clear(),this.latestRegister=b,a.openDialog&&(this.onRecordingDone=a.openDialog("(recording)["+b+"]",null,{bottom:!0})),this.isRecording=!0)}};var vb,wb,xb={buildKeyMap:function(){},getRegisterController:function(){return vb.registerController},resetVimGlobalState_:y,getVimGlobalState_:function(){return vb},maybeInitVimState_:x,suppressErrorLogging:!1,InsertModeKey:eb,map:function(a,b,c){Jb.map(a,b,c)},unmap:function(a,b){Jb.unmap(a,b)},setOption:u,getOption:v,defineOption:t,defineEx:function(a,b,c){if(b){if(0!==a.indexOf(b))throw new Error('(Vim.defineEx) "'+b+'" is not a prefix of "'+a+'", command not registered')}else b=a;Ib[a]=c,Jb.commandMap_[b]={name:a,shortName:b,type:"api"}},handleKey:function(a,b,c){var d=this.findKey(a,b,c);if("function"==typeof d)return d()},findKey:function(c,d,e){function f(){var a=vb.macroModeState;if(a.isRecording){if("q"==d)return a.exitMacroRecordMode(),A(c),!0;"mapping"!=e&&Za(a,d)}}function g(){if(""==d)return A(c),l.visualMode?ia(c):l.insertMode&&Va(c),!0}function h(b){for(var e;b;)e=/<\w+-.+?>|<\w+>|./.exec(b),d=e[0],b=b.substring(e.index+d.length),a.Vim.handleKey(c,d,"mapping")}function i(){if(g())return!0;for(var a=l.inputState.keyBuffer=l.inputState.keyBuffer+d,e=1==d.length,f=yb.matchCommand(a,b,l.inputState,"insert");a.length>1&&"full"!=f.type;){var a=l.inputState.keyBuffer=a.slice(1),h=yb.matchCommand(a,b,l.inputState,"insert");"none"!=h.type&&(f=h)}if("none"==f.type)return A(c),!1;if("partial"==f.type)return wb&&window.clearTimeout(wb),wb=window.setTimeout(function(){l.insertMode&&l.inputState.keyBuffer&&A(c)},v("insertModeEscKeysTimeout")),!e;if(wb&&window.clearTimeout(wb),e){for(var i=c.listSelections(),j=0;j0||this.motionRepeat.length>0)&&(a=1,this.prefixRepeat.length>0&&(a*=parseInt(this.prefixRepeat.join(""),10)),this.motionRepeat.length>0&&(a*=parseInt(this.motionRepeat.join(""),10))),a},B.prototype={setText:function(a,b,c){this.keyBuffer=[a||""],this.linewise=!!b,this.blockwise=!!c},pushText:function(a,b){b&&(this.linewise||this.keyBuffer.push("\n"),this.linewise=!0),this.keyBuffer.push(a)},pushInsertModeChanges:function(a){this.insertModeChanges.push(ub(a))},pushSearchQuery:function(a){this.searchQueries.push(a)},clear:function(){this.keyBuffer=[],this.insertModeChanges=[],this.searchQueries=[],this.linewise=!1},toString:function(){return this.keyBuffer.join("")}},D.prototype={pushText:function(a,b,c,d,e){d&&"\n"!==c.charAt(c.length-1)&&(c+="\n");var f=this.isValidRegister(a)?this.getRegister(a):null;if(!f){switch(b){case"yank":this.registers[0]=new B(c,d,e);break;case"delete":case"change":c.indexOf("\n")==-1?this.registers["-"]=new B(c,d):(this.shiftNumericRegisters_(),this.registers[1]=new B(c,d))}return void this.unnamedRegister.setText(c,d,e)}var g=q(a);g?f.pushText(c,d):f.setText(c,d,e),this.unnamedRegister.setText(f.toString(),d)},getRegister:function(a){return this.isValidRegister(a)?(a=a.toLowerCase(),this.registers[a]||(this.registers[a]=new B),this.registers[a]):this.unnamedRegister},isValidRegister:function(a){return a&&s(a,rb)},shiftNumericRegisters_:function(){for(var a=9;a>=2;a--)this.registers[a]=this.getRegister(""+(a-1))}},E.prototype={nextMatch:function(a,b){var c=this.historyBuffer,d=b?-1:1;null===this.initialPrefix&&(this.initialPrefix=a);for(var e=this.iterator+d;b?e>=0:e=c.length?(this.iterator=c.length,this.initialPrefix):e<0?a:void 0},pushInput:function(a){var b=this.historyBuffer.indexOf(a);b>-1&&this.historyBuffer.splice(b,1),a.length&&this.historyBuffer.push(a)},reset:function(){this.initialPrefix=null,this.iterator=this.historyBuffer.length}};var yb={matchCommand:function(a,b,c,d){var e=N(a,b,d,c);if(!e.full&&!e.partial)return{type:"none"};if(!e.full&&e.partial)return{type:"partial"};for(var f,g=0;g"==f.keys.slice(-11)){var i=P(a);if(!i)return{type:"none"};c.selectedCharacter=i}return{type:"full",command:f}},processCommand:function(a,b,c){switch(b.inputState.repeatOverride=c.repeatOverride,c.type){case"motion":this.processMotion(a,b,c);break;case"operator":this.processOperator(a,b,c);break;case"operatorMotion":this.processOperatorMotion(a,b,c);break;case"action":this.processAction(a,b,c);break;case"search":this.processSearch(a,b,c);break;case"ex":case"keyToEx":this.processEx(a,b,c)}},processMotion:function(a,b,c){b.inputState.motion=c.motion,b.inputState.motionArgs=K(c.motionArgs),this.evalInput(a,b)},processOperator:function(a,b,c){var d=b.inputState;if(d.operator){if(d.operator==c.operator)return d.motion="expandToLine",d.motionArgs={linewise:!0},void this.evalInput(a,b);A(a)}d.operator=c.operator,d.operatorArgs=K(c.operatorArgs),b.visualMode&&this.evalInput(a,b)},processOperatorMotion:function(a,b,c){var d=b.visualMode,e=K(c.operatorMotionArgs);e&&d&&e.visualLine&&(b.visualLine=!0),this.processOperator(a,b,c),d||this.processMotion(a,b,c)},processAction:function(a,b,c){var d=b.inputState,e=d.getRepeat(),f=!!e,g=K(c.actionArgs)||{};d.selectedCharacter&&(g.selectedCharacter=d.selectedCharacter),c.operator&&this.processOperator(a,b,c),c.motion&&this.processMotion(a,b,c),(c.motion||c.operator)&&this.evalInput(a,b),g.repeat=e||1,g.repeatIsExplicit=f,g.registerName=d.registerName,A(a),b.lastMotion=null,c.isEdit&&this.recordLastEdit(b,d,c),Bb[c.action](a,g,b)},processSearch:function(b,c,d){function e(a,e,f){vb.searchHistoryController.pushInput(a),vb.searchHistoryController.reset();try{Ma(b,a,e,f)}catch(g){return Ia(b,"Invalid regex: "+a),void A(b)}yb.processMotion(b,c,{type:"motion",motion:"findNext",motionArgs:{forward:!0,toJumplist:d.searchArgs.toJumplist}})}function f(a){b.scrollTo(m.left,m.top),e(a,!0,!0);var c=vb.macroModeState;c.isRecording&&_a(c,a)}function g(c,d,e){var f,g,h=a.keyName(c);"Up"==h||"Down"==h?(f="Up"==h,g=c.target?c.target.selectionEnd:0,d=vb.searchHistoryController.nextMatch(d,f)||"",e(d),g&&c.target&&(c.target.selectionEnd=c.target.selectionStart=Math.min(g,c.target.value.length))):"Left"!=h&&"Right"!=h&&"Ctrl"!=h&&"Alt"!=h&&"Shift"!=h&&vb.searchHistoryController.reset();var j;try{j=Ma(b,d,!0,!0)}catch(c){}j?b.scrollIntoView(Pa(b,!i,j),30):(Qa(b),b.scrollTo(m.left,m.top))}function h(c,d,e){var f=a.keyName(c);"Esc"==f||"Ctrl-C"==f||"Ctrl-["==f||"Backspace"==f&&""==d?(vb.searchHistoryController.pushInput(d),vb.searchHistoryController.reset(),Ma(b,l),Qa(b),b.scrollTo(m.left,m.top),a.e_stop(c),A(b),e(),b.focus()):"Up"==f||"Down"==f?a.e_stop(c):"Ctrl-U"==f&&(a.e_stop(c),e(""))}if(b.getSearchCursor){var i=d.searchArgs.forward,j=d.searchArgs.wholeWordOnly;Aa(b).setReversed(!i);var k=i?"/":"?",l=Aa(b).getQuery(),m=b.getScrollInfo();switch(d.searchArgs.querySrc){case"prompt":var n=vb.macroModeState;if(n.isPlaying){var o=n.replaySearchQueries.shift();e(o,!0,!1)}else Ka(b,{onClose:f,prefix:k,desc:Gb,onKeyUp:g,onKeyDown:h});break;case"wordUnderCursor":var p=ma(b,!1,!0,!1,!0),q=!0;if(p||(p=ma(b,!1,!0,!1,!1),q=!1),!p)return;var o=b.getLine(p.start.line).substring(p.start.ch,p.end.ch);o=q&&j?"\\b"+o+"\\b":Z(o),vb.jumpList.cachedCursor=b.getCursor(),b.setCursor(p.start),e(o,!0,!1)}}},processEx:function(b,c,d){function e(a){vb.exCommandHistoryController.pushInput(a),vb.exCommandHistoryController.reset(),Jb.processCommand(b,a)}function f(c,d,e){var f,g,h=a.keyName(c);("Esc"==h||"Ctrl-C"==h||"Ctrl-["==h||"Backspace"==h&&""==d)&&(vb.exCommandHistoryController.pushInput(d),vb.exCommandHistoryController.reset(),a.e_stop(c),A(b),e(),b.focus()),"Up"==h||"Down"==h?(a.e_stop(c),f="Up"==h,g=c.target?c.target.selectionEnd:0,d=vb.exCommandHistoryController.nextMatch(d,f)||"",e(d),g&&c.target&&(c.target.selectionEnd=c.target.selectionStart=Math.min(g,c.target.value.length))):"Ctrl-U"==h?(a.e_stop(c),e("")):"Left"!=h&&"Right"!=h&&"Ctrl"!=h&&"Alt"!=h&&"Shift"!=h&&vb.exCommandHistoryController.reset()}"keyToEx"==d.type?Jb.processCommand(b,d.exArgs.input):c.visualMode?Ka(b,{onClose:e,prefix:":",value:"'<,'>",onKeyDown:f}):Ka(b,{onClose:e,prefix:":",onKeyDown:f})},evalInput:function(a,b){var c,e,f,g=b.inputState,h=g.motion,i=g.motionArgs||{},j=g.operator,k=g.operatorArgs||{},l=g.registerName,m=b.sel,n=R(b.visualMode?J(a,m.head):a.getCursor("head")),o=R(b.visualMode?J(a,m.anchor):a.getCursor("anchor")),p=R(n),q=R(o);if(j&&this.recordLastEdit(b,g),f=void 0!==g.repeatOverride?g.repeatOverride:g.getRepeat(),f>0&&i.explicitRepeat?i.repeatIsExplicit=!0:(i.noRepeat||!i.explicitRepeat&&0===f)&&(f=1,i.repeatIsExplicit=!1),g.selectedCharacter&&(i.selectedCharacter=k.selectedCharacter=g.selectedCharacter),i.repeat=f,A(a),h){var r=zb[h](a,n,i,b);if(b.lastMotion=zb[h],!r)return;if(i.toJumplist){var s=vb.jumpList,t=s.cachedCursor;t?(na(a,t,r),delete s.cachedCursor):na(a,n,r)}r instanceof Array?(e=r[0],c=r[1]):c=r,c||(c=R(n)),b.visualMode?(b.visualBlock&&c.ch===1/0||(c=J(a,c,b.visualBlock)),e&&(e=J(a,e,!0)),e=e||q,m.anchor=e,m.head=c,fa(a),ua(a,b,"<",T(e,c)?e:c),ua(a,b,">",T(e,c)?c:e)):j||(c=J(a,c),a.setCursor(c.line,c.ch))}if(j){if(k.lastSel){e=q;var u=k.lastSel,v=Math.abs(u.head.line-u.anchor.line),w=Math.abs(u.head.ch-u.anchor.ch);c=u.visualLine?d(q.line+v,q.ch):u.visualBlock?d(q.line+v,q.ch+w):u.head.line==u.anchor.line?d(q.line,q.ch+w):d(q.line+v,q.ch),b.visualMode=!0,b.visualLine=u.visualLine,b.visualBlock=u.visualBlock,m=b.sel={anchor:e,head:c},fa(a)}else b.visualMode&&(k.lastSel={anchor:R(m.anchor),head:R(m.head),visualBlock:b.visualBlock,visualLine:b.visualLine});var x,y,z,B,C;if(b.visualMode){if(x=U(m.head,m.anchor),y=V(m.head,m.anchor),z=b.visualLine||k.linewise,B=b.visualBlock?"block":z?"line":"char",C=ga(a,{anchor:x,head:y},B),z){var D=C.ranges;if("block"==B)for(var E=0;Ek&&f.line==k?this.moveToEol(a,b,c,e):(c.toFirstChar&&(g=la(a.getLine(i)),e.lastHPos=g),e.lastHSPos=a.charCoords(d(i,g),"div").left,d(i,g))},moveByDisplayLines:function(a,b,c,e){var f=b;switch(e.lastMotion){case this.moveByDisplayLines:case this.moveByScroll:case this.moveByLines:case this.moveToColumn:case this.moveToEol:break;default:e.lastHSPos=a.charCoords(f,"div").left}var g=c.repeat,h=a.findPosV(f,c.forward?g:-g,"line",e.lastHSPos);if(h.hitSide)if(c.forward)var i=a.charCoords(h,"div"),j={top:i.top+8,left:e.lastHSPos},h=a.coordsChar(j,"div");else{var k=a.charCoords(d(a.firstLine(),0),"div");k.left=e.lastHSPos,h=a.coordsChar(k,"div")}return e.lastHPos=h.ch,h},moveByPage:function(a,b,c){var d=b,e=c.repeat;return a.findPosV(d,c.forward?e:-e,"page")},moveByParagraph:function(a,b,c){var d=c.forward?1:-1;return wa(a,b,c.repeat,d)},moveByScroll:function(a,b,c,d){var e=a.getScrollInfo(),f=null,g=c.repeat;g||(g=e.clientHeight/(2*a.defaultTextHeight()));var h=a.charCoords(b,"local");c.repeat=g;var f=zb.moveByDisplayLines(a,b,c,d);if(!f)return null;var i=a.charCoords(f,"local");return a.scrollTo(null,e.top+i.top-h.top),f},moveByWords:function(a,b,c){return ra(a,b,c.repeat,!!c.forward,!!c.wordEnd,!!c.bigWord)},moveTillCharacter:function(a,b,c){var d=c.repeat,e=sa(a,d,c.forward,c.selectedCharacter),f=c.forward?-1:1;return oa(f,c),e?(e.ch+=f,e):null},moveToCharacter:function(a,b,c){var d=c.repeat;return oa(0,c),sa(a,d,c.forward,c.selectedCharacter)||b},moveToSymbol:function(a,b,c){var d=c.repeat;return pa(a,d,c.forward,c.selectedCharacter)||b},moveToColumn:function(a,b,c,d){var e=c.repeat;return d.lastHPos=e-1,d.lastHSPos=a.charCoords(b,"div").left,ta(a,e)},moveToEol:function(a,b,c,e){var f=b;e.lastHPos=1/0;var g=d(f.line+c.repeat-1,1/0),h=a.clipPos(g);return h.ch--,e.lastHSPos=a.charCoords(h,"div").left,g},moveToFirstNonWhiteSpaceCharacter:function(a,b){var c=b;return d(c.line,la(a.getLine(c.line)))},moveToMatchedSymbol:function(a,b){for(var c,e=b,f=e.line,g=e.ch,h=a.getLine(f);gb.lastLine()&&c.linewise&&!o?b.replaceRange("",n,k):b.replaceRange("",j,k),c.linewise&&(o||(b.setCursor(n),a.commands.newlineAndIndent(b)),j.ch=Number.MAX_VALUE),f=j}vb.registerController.pushText(c.registerName,"change",g,c.linewise,e.length>1),Bb.enterInsertMode(b,{head:f},b.state.vim)},"delete":function(a,b,c){var e,f,g=a.state.vim;if(g.visualBlock){f=a.getSelection();var h=G("",c.length);a.replaceSelections(h),e=c[0].anchor}else{var i=c[0].anchor,j=c[0].head;b.linewise&&j.line!=a.firstLine()&&i.line==a.lastLine()&&i.line==j.line-1&&(i.line==a.firstLine()?i.ch=0:i=d(i.line-1,X(a,i.line-1))),f=a.getRange(i,j),a.replaceRange("",i,j),e=i,b.linewise&&(e=zb.moveToFirstNonWhiteSpaceCharacter(a,i))}return vb.registerController.pushText(b.registerName,"delete",f,b.linewise,g.visualBlock),J(a,e)},indent:function(a,b,c){var d=a.state.vim,e=c[0].anchor.line,f=d.visualBlock?c[c.length-1].anchor.line:c[0].head.line,g=d.visualMode?b.repeat:1;b.linewise&&f--;for(var h=e;h<=f;h++)for(var i=0;ij.top?(i.line+=(h-j.top)/e,i.line=Math.ceil(i.line),a.setCursor(i),j=a.charCoords(i,"local"),a.scrollTo(null,j.top)):a.scrollTo(null,h);else{var k=h+a.getScrollInfo().clientHeight;k=g.anchor.line?L(g.head,0,1):d(g.anchor.line,0);else if("inplace"==f&&e.visualMode)return;b.setOption("disableInput",!1),c&&c.replace?(b.toggleOverwrite(!0),b.setOption("keyMap","vim-replace"),a.signal(b,"vim-mode-change",{mode:"replace"})):(b.toggleOverwrite(!1),b.setOption("keyMap","vim-insert"),a.signal(b,"vim-mode-change",{mode:"insert"})),vb.macroModeState.isPlaying||(b.on("change",ab),a.on(b.getInputField(),"keydown",fb)),e.visualMode&&ia(b),aa(b,h,i)}},toggleVisualMode:function(b,c,e){var f,g=c.repeat,h=b.getCursor();e.visualMode?e.visualLine^c.linewise||e.visualBlock^c.blockwise?(e.visualLine=!!c.linewise,e.visualBlock=!!c.blockwise,a.signal(b,"vim-mode-change",{mode:"visual",subMode:e.visualLine?"linewise":e.visualBlock?"blockwise":""}),fa(b)):ia(b):(e.visualMode=!0,e.visualLine=!!c.linewise,e.visualBlock=!!c.blockwise,f=J(b,d(h.line,h.ch+g-1),!0),e.sel={anchor:h,head:f},a.signal(b,"vim-mode-change",{mode:"visual",subMode:e.visualLine?"linewise":e.visualBlock?"blockwise":""}),fa(b),ua(b,e,"<",U(h,f)),ua(b,e,">",V(h,f)))},reselectLastSelection:function(b,c,d){var e=d.lastSelection;if(d.visualMode&&da(b,d),e){var f=e.anchorMark.find(),g=e.headMark.find();if(!f||!g)return;d.sel={anchor:f,head:g},d.visualMode=!0,d.visualLine=e.visualLine,d.visualBlock=e.visualBlock,fa(b),ua(b,d,"<",U(f,g)),ua(b,d,">",V(f,g)),a.signal(b,"vim-mode-change",{mode:"visual",subMode:d.visualLine?"linewise":d.visualBlock?"blockwise":""})}},joinLines:function(a,b,c){var e,f;if(c.visualMode){if(e=a.getCursor("anchor"),f=a.getCursor("head"),T(f,e)){var g=f;f=e,e=g}f.ch=X(a,f.line)-1}else{var h=Math.max(b.repeat,2);e=a.getCursor(),f=J(a,d(e.line+h-1,1/0))}for(var i=0,j=e.line;j1)var g=Array(b.repeat+1).join(g);var o=f.linewise,p=f.blockwise;if(o)c.visualMode?g=c.visualLine?g.slice(0,-1):"\n"+g.slice(0,g.length-1)+"\n":b.after?(g="\n"+g.slice(0,g.length-1),e.ch=X(a,e.line)):e.ch=0;else{if(p){g=g.split("\n");for(var q=0;qa.lastLine()&&a.replaceRange("\n",d(A,0));var B=X(a,A);Bk.length&&(f=k.length),g=d(i.line,f)}if("\n"==h)e.visualMode||b.replaceRange("",i,g),(a.commands.newlineAndIndentContinueComment||a.commands.newlineAndIndent)(b);else{var l=b.getRange(i,g);if(l=l.replace(/[^\n]/g,h),e.visualBlock){var m=new Array(b.getOption("tabSize")+1).join(" ");l=b.getSelection(),l=l.replace(/\t/g,m).replace(/[^\n]/g,h).split("\n"),b.replaceSelections(l)}else b.replaceRange(l,i,g);e.visualMode?(i=T(j[0].anchor,j[0].head)?j[0].anchor:j[0].head,b.setCursor(i),ia(b,!1)):b.setCursor(L(g,0,-1))}},incrementNumberToken:function(a,b){for(var c,e,f,g,h,i=a.getCursor(),j=a.getLine(i.line),k=/-?\d+/g;null!==(c=k.exec(j))&&(h=c[0],e=c.index,f=e+h.length,!(i.ch=1)return!0}else a.nextCh===a.reverseSymb&&a.depth--;return!1}},section:{init:function(a){a.curMoveThrough=!0,a.symb=(a.forward?"]":"[")===a.symb?"{":"}"},isComplete:function(a){return 0===a.index&&a.nextCh===a.symb}},comment:{isComplete:function(a){var b="*"===a.lastCh&&"/"===a.nextCh;return a.lastCh=a.nextCh,b}},method:{init:function(a){a.symb="m"===a.symb?"{":"}",a.reverseSymb="{"===a.symb?"}":"{"},isComplete:function(a){return a.nextCh===a.symb}},preprocess:{init:function(a){a.index=0},isComplete:function(a){if("#"===a.nextCh){var b=a.lineText.match(/#(\w+)/)[1];if("endif"===b){if(a.forward&&0===a.depth)return!0;a.depth++}else if("if"===b){if(!a.forward&&0===a.depth)return!0;a.depth--}if("else"===b&&0===a.depth)return!0}return!1}}};t("pcre",!0,"boolean"),za.prototype={getQuery:function(){return vb.query},setQuery:function(a){vb.query=a},getOverlay:function(){return this.searchOverlay},setOverlay:function(a){this.searchOverlay=a},isReversed:function(){return vb.isReversed},setReversed:function(a){vb.isReversed=a},getScrollbarAnnotate:function(){return this.annotate},setScrollbarAnnotate:function(a){this.annotate=a}};var Eb={"\\n":"\n","\\r":"\r","\\t":"\t"},Fb={"\\/":"/","\\\\":"\\","\\n":"\n","\\r":"\r","\\t":"\t"},Gb="(Javascript regexp)",Hb=function(){this.buildCommandMap_()};Hb.prototype={processCommand:function(a,b,c){var d=this;a.operation(function(){a.curOp.isVimOp=!0,d._processCommand(a,b,c)})},_processCommand:function(b,c,d){var e=b.state.vim,f=vb.registerController.getRegister(":"),g=f.toString();e.visualMode&&ia(b);var h=new a.StringStream(c);f.setText(c);var i=d||{};i.input=c;try{this.parseInput_(b,h,i)}catch(j){throw Ia(b,j),j}var k,l;if(i.commandName){if(k=this.matchCommand_(i.commandName)){if(l=k.name,k.excludeFromCommandHistory&&f.setText(g),this.parseCommandArgs_(h,i,k),"exToKey"==k.type){for(var m=0;m0;b--){var c=a.substring(0,b);if(this.commandMap_[c]){var d=this.commandMap_[c];if(0===d.name.indexOf(a))return d}}return null},buildCommandMap_:function(){this.commandMap_={};for(var a=0;a
    ";if(c){var f;c=c.join("");for(var g=0;g"}}else for(var f in d){var i=d[f].toString();i.length&&(e+='"'+f+" "+i+"
    ")}Ia(a,e)},sort:function(b,c){function e(){if(c.argString){var b=new a.StringStream(c.argString);if(b.eat("!")&&(h=!0),b.eol())return;if(!b.eatSpace())return"Invalid arguments";var d=b.match(/([dinuox]+)?\s*(\/.+\/)?\s*/);if(!d&&!b.eol())return"Invalid arguments";if(d[1]){i=d[1].indexOf("i")!=-1,j=d[1].indexOf("u")!=-1;var e=d[1].indexOf("d")!=-1||d[1].indexOf("n")!=-1&&1,f=d[1].indexOf("x")!=-1&&1,g=d[1].indexOf("o")!=-1&&1;if(e+f+g>1)return"Invalid arguments";k=e&&"decimal"||f&&"hex"||g&&"octal"}d[2]&&(l=new RegExp(d[2].substr(1,d[2].length-2),i?"i":""))}}function f(a,b){if(h){var c;c=a,a=b,b=c}i&&(a=a.toLowerCase(),b=b.toLowerCase());var d=k&&s.exec(a),e=k&&s.exec(b);return d?(d=parseInt((d[1]+d[2]).toLowerCase(),t),e=parseInt((e[1]+e[2]).toLowerCase(),t),d-e):a")}if(!d)return void Ia(a,l);var o=0,p=function(){if(o=k)return void Ia(b,"Invalid argument: "+c.argString.substring(f));for(var l=0;l<=k-j;l++){var m=String.fromCharCode(j+l);delete d.marks[m]}}else delete d.marks[g]}}},Jb=new Hb;return a.keyMap.vim={attach:h,detach:g,call:i},t("insertModeEscKeysTimeout",200,"number"),a.keyMap["vim-insert"]={fallthrough:["default"],attach:h,detach:g,call:i},a.keyMap["vim-replace"]={Backspace:"goCharLeft",fallthrough:["vim-insert"],attach:h,detach:g,call:i},y(),xb};a.Vim=e()})},{"../addon/dialog/dialog":3,"../addon/edit/matchbrackets.js":12,"../addon/search/searchcursor":49,"../lib/codemirror":59}],59:[function(a,b,c){!function(a,d){"object"==typeof c&&"undefined"!=typeof b?b.exports=d():"function"==typeof define&&define.amd?define(d):a.CodeMirror=d()}(this,function(){"use strict";function a(a){return new RegExp("(^|\\s)"+a+"(?:$|\\s)\\s*")}function b(a){for(var b=a.childNodes.length;b>0;--b)a.removeChild(a.firstChild);return a}function c(a,c){return b(a).appendChild(c)}function d(a,b,c,d){var e=document.createElement(a);if(c&&(e.className=c),d&&(e.style.cssText=d),"string"==typeof b)e.appendChild(document.createTextNode(b));else if(b)for(var f=0;f=b)return g+(b-f);g+=h-f,g+=c-g%c,f=h+1}}function m(a,b){for(var c=0;c=b)return d+Math.min(g,b-e);if(e+=f-d,e+=c-e%c,d=f+1,e>=b)return d}}function o(a){for(;Qg.length<=a;)Qg.push(p(Qg)+" ");return Qg[a]}function p(a){return a[a.length-1]}function q(a,b){for(var c=[],d=0;d"\x80"&&(a.toUpperCase()!=a.toLowerCase()||Rg.test(a))}function v(a,b){return b?!!(b.source.indexOf("\\w")>-1&&u(a))||b.test(a):u(a)}function w(a){for(var b in a)if(a.hasOwnProperty(b)&&a[b])return!1;return!0}function x(a){return a.charCodeAt(0)>=768&&Sg.test(a)}function y(a,b,c){for(;(c<0?b>0:b=a.size)throw new Error("There is no line "+(b+a.first)+" in the document.");for(var c=a;!c.lines;)for(var d=0;;++d){var e=c.children[d],f=e.chunkSize();if(b=a.first&&bc?J(c,B(a,c).text.length):R(b,B(a,b.line).text.length)}function R(a,b){var c=a.ch;return null==c||c>b?J(a.line,b):c<0?J(a.line,0):a}function S(a,b){for(var c=[],d=0;d=b:f.to>b);(d||(d=[])).push(new V(g,f.from,i?null:f.to))}}return d}function $(a,b,c){var d;if(a)for(var e=0;e=b:f.to>b);if(h||f.from==b&&"bookmark"==g.type&&(!c||f.marker.insertLeft)){var i=null==f.from||(g.inclusiveLeft?f.from<=b:f.from0&&h)for(var v=0;v0)){var k=[i,1],l=K(j.from,h.from),n=K(j.to,h.to);(l<0||!g.inclusiveLeft&&!l)&&k.push({from:j.from,to:h.from}),(n>0||!g.inclusiveRight&&!n)&&k.push({from:h.to,to:j.to}),e.splice.apply(e,k),i+=k.length-3}}return e}function ca(a){var b=a.markedSpans;if(b){for(var c=0;c=0&&l<=0||k<=0&&l>=0)&&(k<=0&&(i.marker.inclusiveRight&&e.inclusiveLeft?K(j.to,c)>=0:K(j.to,c)>0)||k>=0&&(i.marker.inclusiveRight&&e.inclusiveLeft?K(j.from,d)<=0:K(j.from,d)<0)))return!0}}}function la(a){for(var b;b=ia(a);)a=b.find(-1,!0).line;return a}function ma(a){for(var b;b=ja(a);)a=b.find(1,!0).line;return a}function na(a){for(var b,c;b=ja(a);)a=b.find(1,!0).line,(c||(c=[])).push(a);return c}function oa(a,b){var c=B(a,b),d=la(c);return c==d?b:F(d)}function pa(a,b){if(b>a.lastLine())return b;var c,d=B(a,b);if(!qa(a,d))return b;for(;c=ja(d);)d=c.find(1,!0).line;return F(d)+1}function qa(a,b){var c=Ug&&b.markedSpans;if(c)for(var d=void 0,e=0;eb.maxLineLength&&(b.maxLineLength=c,b.maxLine=a)})}function va(a,b,c,d){if(!a)return d(b,c,"ltr");for(var e=!1,f=0;fb||b==c&&g.to==b)&&(d(Math.max(g.from,b),Math.min(g.to,c),1==g.level?"rtl":"ltr"),e=!0)}e||d(b,c,"ltr")}function wa(a,b,c){var d;Vg=null;for(var e=0;eb)return e;f.to==b&&(f.from!=f.to&&"before"==c?d=e:Vg=e),f.from==b&&(f.from!=f.to&&"before"!=c?d=e:Vg=e)}return null!=d?d:Vg}function xa(a,b){var c=a.order;return null==c&&(c=a.order=Wg(a.text,b)),c}function ya(a,b,c){var d=y(a.text,b+c,c);return d<0||d>a.text.length?null:d}function za(a,b,c){var d=ya(a,b.ch,c);return null==d?null:new J(b.line,d,c<0?"after":"before")}function Aa(a,b,c,d,e){if(a){var f=xa(c,b.doc.direction);if(f){var g,h=e<0?p(f):f[0],i=e<0==(1==h.level),j=i?"after":"before";if(h.level>0){var k=Zb(b,c);g=e<0?c.text.length-1:0;var l=$b(b,k,g).top;g=z(function(a){return $b(b,k,a).top==l},e<0==(1==h.level)?h.from:h.to-1,g),"before"==j&&(g=ya(c,g,1))}else g=e<0?h.to:h.from;return new J(d,g,j)}}return new J(d,e<0?c.text.length:0,e<0?"before":"after")}function Ba(a,b,c,d){var e=xa(b,a.doc.direction);if(!e)return za(b,c,d);c.ch>=b.text.length?(c.ch=b.text.length,c.sticky="before"):c.ch<=0&&(c.ch=0,c.sticky="after");var f=wa(e,c.ch,c.sticky),g=e[f];if("ltr"==a.doc.direction&&g.level%2==0&&(d>0?g.to>c.ch:g.from=g.from&&m>=k.begin)){var n=l?"before":"after";return new J(c.line,m,n)}}var o=function(a,b,d){for(var f=function(a,b){return b?new J(c.line,i(a,1),"before"):new J(c.line,a,"after")};a>=0&&a0==(1!=g.level),j=h?d.begin:i(d.end,-1);if(g.from<=j&&j0?k.end:i(k.begin,-1);return null==q||d>0&&q==b.text.length||!(p=o(d>0?0:e.length-1,d,j(q)))?null:p}function Ca(a,b){return a._handlers&&a._handlers[b]||Xg}function Da(a,b,c){if(a.removeEventListener)a.removeEventListener(b,c,!1);else if(a.detachEvent)a.detachEvent("on"+b,c);else{var d=a._handlers,e=d&&d[b];if(e){var f=m(e,c);f>-1&&(d[b]=e.slice(0,f).concat(e.slice(f+1)))}}}function Ea(a,b){var c=Ca(a,b);if(c.length)for(var d=Array.prototype.slice.call(arguments,2),e=0;e0}function Ia(a){a.prototype.on=function(a,b){Yg(this,a,b)},a.prototype.off=function(a,b){Da(this,a,b)}}function Ja(a){a.preventDefault?a.preventDefault():a.returnValue=!1}function Ka(a){a.stopPropagation?a.stopPropagation():a.cancelBubble=!0}function La(a){return null!=a.defaultPrevented?a.defaultPrevented:0==a.returnValue}function Ma(a){Ja(a),Ka(a)}function Na(a){return a.target||a.srcElement}function Oa(a){var b=a.which;return null==b&&(1&a.button?b=1:2&a.button?b=3:4&a.button&&(b=2)),zg&&a.ctrlKey&&1==b&&(b=3),b}function Pa(a){if(null==Jg){var b=d("span","\u200b");c(a,d("span",[b,document.createTextNode("x")])),0!=a.firstChild.offsetHeight&&(Jg=b.offsetWidth<=1&&b.offsetHeight>2&&!(ng&&og<8))}var e=Jg?d("span","\u200b"):d("span","\xa0",null,"display: inline-block; width: 1px; margin-right: -1px");return e.setAttribute("cm-text",""),e}function Qa(a){if(null!=Kg)return Kg;var d=c(a,document.createTextNode("A\u062eA")),e=Dg(d,0,1).getBoundingClientRect(),f=Dg(d,1,2).getBoundingClientRect();return b(a),!(!e||e.left==e.right)&&(Kg=f.right-e.right<3)}function Ra(a){if(null!=bh)return bh;var b=c(a,d("span","x")),e=b.getBoundingClientRect(),f=Dg(b,0,1).getBoundingClientRect();return bh=Math.abs(e.left-f.left)>1}function Sa(a,b){arguments.length>2&&(b.dependencies=Array.prototype.slice.call(arguments,2)),ch[a]=b}function Ta(a,b){dh[a]=b}function Ua(a){if("string"==typeof a&&dh.hasOwnProperty(a))a=dh[a];else if(a&&"string"==typeof a.name&&dh.hasOwnProperty(a.name)){var b=dh[a.name];"string"==typeof b&&(b={name:b}),a=t(b,a),a.name=b.name}else{if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+xml$/.test(a))return Ua("application/xml");if("string"==typeof a&&/^[\w\-]+\/[\w\-]+\+json$/.test(a))return Ua("application/json")}return"string"==typeof a?{name:a}:a||{name:"null"}}function Va(a,b){b=Ua(b);var c=ch[b.name];if(!c)return Va(a,"text/plain");var d=c(a,b);if(eh.hasOwnProperty(b.name)){var e=eh[b.name];for(var f in e)e.hasOwnProperty(f)&&(d.hasOwnProperty(f)&&(d["_"+f]=d[f]),d[f]=e[f])}if(d.name=b.name,b.helperType&&(d.helperType=b.helperType),b.modeProps)for(var g in b.modeProps)d[g]=b.modeProps[g];return d}function Wa(a,b){var c=eh.hasOwnProperty(a)?eh[a]:eh[a]={};k(b,c)}function Xa(a,b){if(b===!0)return b;if(a.copyState)return a.copyState(b);var c={};for(var d in b){var e=b[d];e instanceof Array&&(e=e.concat([])),c[d]=e}return c}function Ya(a,b){for(var c;a.innerMode&&(c=a.innerMode(b),c&&c.mode!=a);)b=c.state,a=c.mode;return c||{mode:a,state:b}}function Za(a,b,c){return!a.startState||a.startState(b,c)}function $a(a,b,c,d){var e=[a.state.modeGen],f={};gb(a,b.text,a.doc.mode,c,function(a,b){return e.push(a,b)},f,d);for(var g=c.state,h=function(d){var g=a.state.overlays[d],h=1,i=0;c.state=!0,gb(a,b.text,g.mode,c,function(a,b){for(var c=h;ia&&e.splice(h,1,a,e[h+1],d),h+=2,i=Math.min(a,d)}if(b)if(g.opaque)e.splice(c,h-c,a,"overlay "+b),h=c+2;else for(;ca.options.maxHighlightLength&&Xa(a.doc.mode,d.state),f=$a(a,b,d);e&&(d.state=e),b.stateAfter=d.save(!e),b.styles=f.styles,f.classes?b.styleClasses=f.classes:b.styleClasses&&(b.styleClasses=null),c===a.doc.highlightFrontier&&(a.doc.modeFrontier=Math.max(a.doc.modeFrontier,++a.doc.highlightFrontier))}return b.styles}function ab(a,b,c){var d=a.doc,e=a.display;if(!d.mode.startState)return new hh(d,!0,b);var f=hb(a,b,c),g=f>d.first&&B(d,f-1).stateAfter,h=g?hh.fromSaved(d,g,f):new hh(d,Za(d.mode),f);return d.iter(f,b,function(c){bb(a,c.text,h);var d=h.line;c.stateAfter=d==b-1||d%5==0||d>=e.viewFrom&&db.start)return f}throw new Error("Mode "+a.name+" failed to advance stream.")}function eb(a,b,c,d){var e,f=a.doc,g=f.mode;b=Q(f,b);var h,i=B(f,b.line),j=ab(a,b.line,c),k=new fh(i.text,a.options.tabSize,j);for(d&&(h=[]);(d||k.posa.options.maxHighlightLength?(h=!1,g&&bb(a,b,d,l.pos),l.pos=b.length,i=null):i=fb(db(c,l,d.state,m),f),m){var n=m[0].name;n&&(i="m-"+(i?n+" "+i:n))}if(!h||k!=i){for(;jg;--h){if(h<=f.first)return f.first;var i=B(f,h-1),j=i.stateAfter;if(j&&(!c||h+(j instanceof gh?j.lookAhead:0)<=f.modeFrontier))return h;var k=l(i.text,null,a.options.tabSize);(null==e||d>k)&&(e=h-1,d=k)}return e}function ib(a,b){if(a.modeFrontier=Math.min(a.modeFrontier,b),!(a.highlightFrontierc;d--){var e=B(a,d).stateAfter;if(e&&(!(e instanceof gh)||d+e.lookAhead1&&!/ /.test(a))return a;for(var c=b,d="",e=0;ej&&l.from<=j));m++);if(l.to>=k)return a(c,d,e,f,g,h,i);a(c,d.slice(0,l.to-j),e,f,null,h,i),f=null,d=d.slice(l.to-j),j=l.to}}}function rb(a,b,c,d){var e=!d&&c.widgetNode;e&&a.map.push(a.pos,a.pos+b,e),!d&&a.cm.display.input.needsContentAttribute&&(e||(e=a.content.appendChild(document.createElement("span"))),e.setAttribute("cm-marker",c.id)),e&&(a.cm.display.input.setUneditable(e),a.content.appendChild(e)),a.pos+=b,a.trailingSpace=!1}function sb(a,b,c){var d=a.markedSpans,e=a.text,f=0;if(d)for(var g,h,i,j,k,l,m,n=e.length,o=0,p=1,q="",r=0;;){if(r==o){i=j=k=l=h="",m=null,r=1/0;for(var s=[],t=void 0,u=0;uo||w.collapsed&&v.to==o&&v.from==o)?(null!=v.to&&v.to!=o&&r>v.to&&(r=v.to,j=""),w.className&&(i+=" "+w.className),w.css&&(h=(h?h+";":"")+w.css),w.startStyle&&v.from==o&&(k+=" "+w.startStyle),w.endStyle&&v.to==r&&(t||(t=[])).push(w.endStyle,v.to),w.title&&!l&&(l=w.title),w.collapsed&&(!m||ga(m.marker,w)<0)&&(m=v)):v.from>o&&r>v.from&&(r=v.from)}if(t)for(var x=0;x=n)break;for(var z=Math.min(n,r);;){if(q){var A=o+q.length;if(!m){var B=A>z?q.slice(0,z-o):q;b.addToken(b,B,g?g+i:i,k,o+B.length==r?j:"",l,h)}if(A>=z){q=q.slice(z-o),o=z;break}o=A,k=""}q=e.slice(f,f=c[p++]),g=lb(c[p++],b.cm.options)}}else for(var C=1;C2&&f.push((i.bottom+j.top)/2-c.top)}}f.push(c.bottom-c.top)}}function Vb(a,b,c){if(a.line==b)return{map:a.measure.map,cache:a.measure.cache};for(var d=0;dc)return{map:a.measure.maps[e],cache:a.measure.caches[e],before:!0}}function Wb(a,b){b=la(b);var d=F(b),e=a.display.externalMeasured=new tb(a.doc,b,d);e.lineN=d;var f=e.built=mb(a,e);return e.text=f.pre,c(a.display.lineMeasure,f.pre),e}function Xb(a,b,c,d){return $b(a,Zb(a,b),c,d)}function Yb(a,b){if(b>=a.display.viewFrom&&b=c.lineN&&bb)&&(f=i-h,e=f-1,b>=i&&(g="right")),null!=e){if(d=a[j+2],h==i&&c==(d.insertLeft?"left":"right")&&(g=c),"left"==c&&0==e)for(;j&&a[j-2]==a[j-3]&&a[j-1].insertLeft;)d=a[(j-=3)+2],g="left";if("right"==c&&e==i-h)for(;j=0&&(c=a[e]).left==c.right;e--);return c}function bc(a,b,c,d){var e,f=_b(b.map,c,d),g=f.node,h=f.start,i=f.end,j=f.collapse;if(3==g.nodeType){for(var k=0;k<4;k++){for(;h&&x(b.line.text.charAt(f.coverStart+h));)--h;for(;f.coverStart+i0&&(j=d="right");var l;e=a.options.lineWrapping&&(l=g.getClientRects()).length>1?l["right"==d?l.length-1:0]:g.getBoundingClientRect()}if(ng&&og<9&&!h&&(!e||!e.left&&!e.right)){var m=g.parentNode.getClientRects()[0];e=m?{left:m.left,right:m.left+tc(a.display),top:m.top,bottom:m.bottom}:ph}for(var n=e.top-b.rect.top,o=e.bottom-b.rect.top,p=(n+o)/2,q=b.view.measure.heights,r=0;r=d.text.length?(j=d.text.length,k="before"):j<=0&&(j=0,k="after"),!i)return g("before"==k?j-1:j,"before"==k);var l=wa(i,j,k),m=Vg,n=h(j,l,"before"==k);return null!=m&&(n.other=h(j,m,"before"!=k)),n}function mc(a,b){var c=0;b=Q(a.doc,b),a.options.lineWrapping||(c=tc(a.display)*b.ch);var d=B(a.doc,b.line),e=sa(d)+Ob(a.display);return{left:c,right:c,top:e,bottom:e+d.height}}function nc(a,b,c,d,e){var f=J(a,b,c);return f.xRel=e,d&&(f.outside=!0),f}function oc(a,b,c){var d=a.doc;if(c+=a.display.viewOffset,c<0)return nc(d.first,0,null,!0,-1);var e=G(d,c),f=d.first+d.size-1;if(e>f)return nc(d.first+d.size-1,B(d,f).text.length,null,!0,1);b<0&&(b=0);for(var g=B(d,e);;){var h=rc(a,g,e,b,c),i=ja(g),j=i&&i.find(0,!0);if(!i||!(h.ch>j.from.ch||h.ch==j.from.ch&&h.xRel>0))return h;e=F(g=j.to.line)}}function pc(a,b,c,d){var e=function(d){return ic(a,b,$b(a,c,d),"line")},f=b.text.length,g=z(function(a){return e(a-1).bottom<=d},f,0);return f=z(function(a){return e(a).top>d},g,f),{begin:g,end:f}}function qc(a,b,c,d){var e=ic(a,b,$b(a,c,d),"line").top;return pc(a,b,c,e)}function rc(a,b,c,d,e){e-=sa(b);var f,g=0,h=b.text.length,i=Zb(a,b),j=xa(b,a.doc.direction);if(j){if(a.options.lineWrapping){var k;k=pc(a,b,i,e),g=k.begin,h=k.end}f=new J(c,Math.floor(g+(h-g)/2));var l,m,n=lc(a,f,"line",b,i).left,o=n1){var t=Math.abs(p-l)/q;q=Math.min(q,Math.ceil(Math.abs(p)/t)),o=p<0?1:-1}}while(0!=p&&(q>1||o<0!=p<0&&Math.abs(p)<=Math.abs(l)));if(Math.abs(p)>Math.abs(l)){if(p<0==l<0)throw new Error("Broke out of infinite loop in coordsCharInner");f=m}}else{var u=z(function(c){var f=ic(a,b,$b(a,i,c),"line");return f.top>e?(h=Math.min(c,h),!0):!(f.bottom<=e)&&(f.left>d||!(f.rightv.right?1:0,f}function sc(a){if(null!=a.cachedTextHeight)return a.cachedTextHeight;if(null==kh){kh=d("pre");for(var e=0;e<49;++e)kh.appendChild(document.createTextNode("x")),kh.appendChild(d("br"));kh.appendChild(document.createTextNode("x"))}c(a.measure,kh);var f=kh.offsetHeight/50;return f>3&&(a.cachedTextHeight=f),b(a.measure),f||1}function tc(a){if(null!=a.cachedCharWidth)return a.cachedCharWidth;var b=d("span","xxxxxxxxxx"),e=d("pre",[b]);c(a.measure,e);var f=b.getBoundingClientRect(),g=(f.right-f.left)/10;return g>2&&(a.cachedCharWidth=g),g||10}function uc(a){for(var b=a.display,c={},d={},e=b.gutters.clientLeft,f=b.gutters.firstChild,g=0;f;f=f.nextSibling,++g)c[a.options.gutters[g]]=f.offsetLeft+f.clientLeft+e,d[a.options.gutters[g]]=f.clientWidth;return{fixedPos:vc(b),gutterTotalWidth:b.gutters.offsetWidth,gutterLeft:c,gutterWidth:d,wrapperWidth:b.wrapper.clientWidth}}function vc(a){return a.scroller.getBoundingClientRect().left-a.sizer.getBoundingClientRect().left}function wc(a){var b=sc(a.display),c=a.options.lineWrapping,d=c&&Math.max(5,a.display.scroller.clientWidth/tc(a.display)-3);return function(e){if(qa(a.doc,e))return 0;var f=0;if(e.widgets)for(var g=0;g=a.display.viewTo)return null;if(b-=a.display.viewFrom,b<0)return null;for(var c=a.display.view,d=0;d=a.display.viewTo||h.to().line0?b.blinker=setInterval(function(){return b.cursorDiv.style.visibility=(c=!c)?"":"hidden"},a.options.cursorBlinkRate):a.options.cursorBlinkRate<0&&(b.cursorDiv.style.visibility="hidden")}}function Gc(a){a.state.focused||(a.display.input.focus(),Ic(a))}function Hc(a){a.state.delayingBlurEvent=!0,setTimeout(function(){a.state.delayingBlurEvent&&(a.state.delayingBlurEvent=!1,Jc(a))},100)}function Ic(a,b){a.state.delayingBlurEvent&&(a.state.delayingBlurEvent=!1),"nocursor"!=a.options.readOnly&&(a.state.focused||(Ea(a,"focus",a,b),a.state.focused=!0,h(a.display.wrapper,"CodeMirror-focused"),a.curOp||a.display.selForContextMenu==a.doc.sel||(a.display.input.reset(),pg&&setTimeout(function(){return a.display.input.reset(!0)},20)),a.display.input.receivedFocus()),Fc(a))}function Jc(a,b){a.state.delayingBlurEvent||(a.state.focused&&(Ea(a,"blur",a,b),a.state.focused=!1,Gg(a.display.wrapper,"CodeMirror-focused")),clearInterval(a.display.blinker),setTimeout(function(){a.state.focused||(a.display.shift=!1)},150))}function Kc(a){for(var b=a.display,c=b.lineDiv.offsetTop,d=0;d.005||i<-.005)&&(E(e.line,f),Lc(e.line),e.rest))for(var j=0;j=g&&(f=G(b,sa(B(b,i))-a.wrapper.clientHeight),g=i)}return{from:f,to:Math.max(g,f+1)}}function Nc(a){var b=a.display,c=b.view;if(b.alignWidgets||b.gutters.firstChild&&a.options.fixedGutter){for(var d=vc(b)-b.scroller.scrollLeft+a.doc.scrollLeft,e=b.gutters.offsetWidth,f=d+"px",g=0;g(window.innerHeight||document.documentElement.clientHeight)&&(f=!1),null!=f&&!vg){var g=d("div","\u200b",null,"position: absolute;\n top: "+(b.top-c.viewOffset-Ob(a.display))+"px;\n height: "+(b.bottom-b.top+Rb(a)+c.barHeight)+"px;\n left: "+b.left+"px; width: "+Math.max(2,b.right-b.left)+"px;");a.display.lineSpace.appendChild(g),g.scrollIntoView(f),a.display.lineSpace.removeChild(g)}}}function Qc(a,b,c,d){null==d&&(d=0);var e;a.options.lineWrapping||b!=c||(b=b.ch?J(b.line,"before"==b.sticky?b.ch-1:b.ch,"after"):b,c="before"==b.sticky?J(b.line,b.ch+1,"before"):b);for(var f=0;f<5;f++){var g=!1,h=lc(a,b),i=c&&c!=b?lc(a,c):h;e={left:Math.min(h.left,i.left),top:Math.min(h.top,i.top)-d,right:Math.max(h.left,i.left),bottom:Math.max(h.bottom,i.bottom)+d};var j=Sc(a,e),k=a.doc.scrollTop,l=a.doc.scrollLeft;if(null!=j.scrollTop&&(Zc(a,j.scrollTop),Math.abs(a.doc.scrollTop-k)>1&&(g=!0)),null!=j.scrollLeft&&(_c(a,j.scrollLeft),Math.abs(a.doc.scrollLeft-l)>1&&(g=!0)),!g)break}return e}function Rc(a,b){var c=Sc(a,b);null!=c.scrollTop&&Zc(a,c.scrollTop),null!=c.scrollLeft&&_c(a,c.scrollLeft)}function Sc(a,b){var c=a.display,d=sc(a.display);b.top<0&&(b.top=0);var e=a.curOp&&null!=a.curOp.scrollTop?a.curOp.scrollTop:c.scroller.scrollTop,f=Tb(a),g={};b.bottom-b.top>f&&(b.bottom=b.top+f);var h=a.doc.height+Pb(c),i=b.toph-d;if(b.tope+f){var k=Math.min(b.top,(j?h:b.bottom)-f);k!=e&&(g.scrollTop=k)}var l=a.curOp&&null!=a.curOp.scrollLeft?a.curOp.scrollLeft:c.scroller.scrollLeft,m=Sb(a)-(a.options.fixedGutter?c.gutters.offsetWidth:0),n=b.right-b.left>m;return n&&(b.right=b.left+m),b.left<10?g.scrollLeft=0:b.leftm+l-3&&(g.scrollLeft=b.right+(n?0:10)-m),g}function Tc(a,b){null!=b&&(Xc(a),a.curOp.scrollTop=(null==a.curOp.scrollTop?a.doc.scrollTop:a.curOp.scrollTop)+b)}function Uc(a){Xc(a);var b=a.getCursor();a.curOp.scrollToPos={from:b,to:b,margin:a.options.cursorScrollMargin}}function Vc(a,b,c){null==b&&null==c||Xc(a),null!=b&&(a.curOp.scrollLeft=b),null!=c&&(a.curOp.scrollTop=c)}function Wc(a,b){Xc(a),a.curOp.scrollToPos=b}function Xc(a){var b=a.curOp.scrollToPos;if(b){a.curOp.scrollToPos=null;var c=mc(a,b.from),d=mc(a,b.to);Yc(a,c,d,b.margin)}}function Yc(a,b,c,d){var e=Sc(a,{left:Math.min(b.left,c.left),top:Math.min(b.top,c.top)-d,right:Math.max(b.right,c.right),bottom:Math.max(b.bottom,c.bottom)+d});Vc(a,e.scrollLeft,e.scrollTop)}function Zc(a,b){Math.abs(a.doc.scrollTop-b)<2||(jg||Dd(a,{top:b}),$c(a,b,!0),jg&&Dd(a),wd(a,100))}function $c(a,b,c){b=Math.min(a.display.scroller.scrollHeight-a.display.scroller.clientHeight,b),(a.display.scroller.scrollTop!=b||c)&&(a.doc.scrollTop=b,a.display.scrollbars.setScrollTop(b),a.display.scroller.scrollTop!=b&&(a.display.scroller.scrollTop=b))}function _c(a,b,c,d){b=Math.min(b,a.display.scroller.scrollWidth-a.display.scroller.clientWidth),(c?b==a.doc.scrollLeft:Math.abs(a.doc.scrollLeft-b)<2)&&!d||(a.doc.scrollLeft=b,Nc(a),a.display.scroller.scrollLeft!=b&&(a.display.scroller.scrollLeft=b),a.display.scrollbars.setScrollLeft(b))}function ad(a){var b=a.display,c=b.gutters.offsetWidth,d=Math.round(a.doc.height+Pb(a.display));return{clientHeight:b.scroller.clientHeight,viewHeight:b.wrapper.clientHeight,scrollWidth:b.scroller.scrollWidth,clientWidth:b.scroller.clientWidth,viewWidth:b.wrapper.clientWidth,barLeft:a.options.fixedGutter?c:0,docHeight:d,scrollHeight:d+Rb(a)+b.barHeight,nativeBarWidth:b.nativeBarWidth,gutterWidth:c}}function bd(a,b){b||(b=ad(a));var c=a.display.barWidth,d=a.display.barHeight;cd(a,b);for(var e=0;e<4&&c!=a.display.barWidth||d!=a.display.barHeight;e++)c!=a.display.barWidth&&a.options.lineWrapping&&Kc(a),cd(a,ad(a)),c=a.display.barWidth,d=a.display.barHeight}function cd(a,b){var c=a.display,d=c.scrollbars.update(b);c.sizer.style.paddingRight=(c.barWidth=d.right)+"px",c.sizer.style.paddingBottom=(c.barHeight=d.bottom)+"px",c.heightForcer.style.borderBottom=d.bottom+"px solid transparent",d.right&&d.bottom?(c.scrollbarFiller.style.display="block",c.scrollbarFiller.style.height=d.bottom+"px",c.scrollbarFiller.style.width=d.right+"px"):c.scrollbarFiller.style.display="",d.bottom&&a.options.coverGutterNextToScrollbar&&a.options.fixedGutter?(c.gutterFiller.style.display="block",c.gutterFiller.style.height=d.bottom+"px",c.gutterFiller.style.width=b.gutterWidth+"px"):c.gutterFiller.style.display=""}function dd(a){a.display.scrollbars&&(a.display.scrollbars.clear(),a.display.scrollbars.addClass&&Gg(a.display.wrapper,a.display.scrollbars.addClass)),a.display.scrollbars=new sh[a.options.scrollbarStyle](function(b){a.display.wrapper.insertBefore(b,a.display.scrollbarFiller),Yg(b,"mousedown",function(){a.state.focused&&setTimeout(function(){return a.display.input.focus()},0)}),b.setAttribute("cm-not-content","true")},function(b,c){"horizontal"==c?_c(a,b):Zc(a,b)},a),a.display.scrollbars.addClass&&h(a.display.wrapper,a.display.scrollbars.addClass)}function ed(a){a.curOp={cm:a,viewChanged:!1,startHeight:a.doc.height,forceUpdate:!1,updateInput:null,typing:!1,changeObjs:null,cursorActivityHandlers:null,cursorActivityCalled:0,selectionChanged:!1,updateMaxLine:!1,scrollLeft:null,scrollTop:null,scrollToPos:null,focus:!1,id:++th},vb(a.curOp)}function fd(a){var b=a.curOp;xb(b,function(a){for(var b=0;b=c.viewTo)||c.maxLineChanged&&b.options.lineWrapping,a.update=a.mustUpdate&&new uh(b,a.mustUpdate&&{top:a.scrollTop,ensure:a.scrollToPos},a.forceUpdate)}function id(a){a.updatedDisplay=a.mustUpdate&&Bd(a.cm,a.update)}function jd(a){var b=a.cm,c=b.display;a.updatedDisplay&&Kc(b),a.barMeasure=ad(b),c.maxLineChanged&&!b.options.lineWrapping&&(a.adjustWidthTo=Xb(b,c.maxLine,c.maxLine.text.length).left+3,b.display.sizerWidth=a.adjustWidthTo,a.barMeasure.scrollWidth=Math.max(c.scroller.clientWidth,c.sizer.offsetLeft+a.adjustWidthTo+Rb(b)+b.display.barWidth),a.maxScrollLeft=Math.max(0,c.sizer.offsetLeft+a.adjustWidthTo-Sb(b))),(a.updatedDisplay||a.selectionChanged)&&(a.preparedSelection=c.input.prepareSelection(a.focus))}function kd(a){var b=a.cm;null!=a.adjustWidthTo&&(b.display.sizer.style.minWidth=a.adjustWidthTo+"px",a.maxScrollLeftb)&&(e.updateLineNumbers=b),a.curOp.viewChanged=!0,b>=e.viewTo)Ug&&oa(a.doc,b)e.viewFrom?sd(a):(e.viewFrom+=d,e.viewTo+=d);else if(b<=e.viewFrom&&c>=e.viewTo)sd(a);else if(b<=e.viewFrom){var f=td(a,c,c+d,1);f?(e.view=e.view.slice(f.index),e.viewFrom=f.lineN,e.viewTo+=d):sd(a)}else if(c>=e.viewTo){var g=td(a,b,b,-1);g?(e.view=e.view.slice(0,g.index),e.viewTo=g.lineN):sd(a)}else{var h=td(a,b,b,-1),i=td(a,c,c+d,1);h&&i?(e.view=e.view.slice(0,h.index).concat(ub(a,h.lineN,i.lineN)).concat(e.view.slice(i.index)),e.viewTo+=d):sd(a)}var j=e.externalMeasured;j&&(c=e.lineN&&b=d.viewTo)){var f=d.view[zc(a,b)];if(null!=f.node){var g=f.changes||(f.changes=[]);m(g,c)==-1&&g.push(c)}}}function sd(a){a.display.viewFrom=a.display.viewTo=a.doc.first,a.display.view=[],a.display.viewOffset=0}function td(a,b,c,d){var e,f=zc(a,b),g=a.display.view;if(!Ug||c==a.doc.first+a.doc.size)return{index:f,lineN:c};for(var h=a.display.viewFrom,i=0;i0){if(f==g.length-1)return null;e=h+g[f].size-b,f++}else e=h-b;b+=e,c+=e}for(;oa(a.doc,c)!=c;){if(f==(d<0?0:g.length-1))return null;c+=d*g[f-(d<0?1:0)].size,f+=d}return{index:f,lineN:c}}function ud(a,b,c){var d=a.display,e=d.view;0==e.length||b>=d.viewTo||c<=d.viewFrom?(d.view=ub(a,b,c),d.viewFrom=b):(d.viewFrom>b?d.view=ub(a,b,d.viewFrom).concat(d.view):d.viewFromc&&(d.view=d.view.slice(0,zc(a,c)))),d.viewTo=c}function vd(a){for(var b=a.display.view,c=0,d=0;d=a.display.viewTo)){var c=+new Date+a.options.workTime,d=ab(a,b.highlightFrontier),e=[];b.iter(d.line,Math.min(b.first+b.size,a.display.viewTo+500),function(f){if(d.line>=a.display.viewFrom){var g=f.styles,h=f.text.length>a.options.maxHighlightLength?Xa(b.mode,d.state):null,i=$a(a,f,d,!0);h&&(d.state=h),f.styles=i.styles;var j=f.styleClasses,k=i.classes;k?f.styleClasses=k:j&&(f.styleClasses=null);for(var l=!g||g.length!=f.styles.length||j!=k&&(!j||!k||j.bgClass!=k.bgClass||j.textClass!=k.textClass),m=0;!l&&mc)return wd(a,a.options.workDelay),!0}),b.highlightFrontier=d.line,b.modeFrontier=Math.max(b.modeFrontier,d.line),e.length&&md(a,function(){for(var b=0;b=d.viewFrom&&c.visible.to<=d.viewTo&&(null==d.updateLineNumbers||d.updateLineNumbers>=d.viewTo)&&d.renderedView==d.view&&0==vd(a))return!1;Oc(a)&&(sd(a),c.dims=uc(a));var f=e.first+e.size,g=Math.max(c.visible.from-a.options.viewportMargin,e.first),h=Math.min(f,c.visible.to+a.options.viewportMargin);d.viewFromh&&d.viewTo-h<20&&(h=Math.min(f,d.viewTo)),Ug&&(g=oa(a.doc,g),h=pa(a.doc,h));var i=g!=d.viewFrom||h!=d.viewTo||d.lastWrapHeight!=c.wrapperHeight||d.lastWrapWidth!=c.wrapperWidth;ud(a,g,h),d.viewOffset=sa(B(a.doc,d.viewFrom)),a.display.mover.style.top=d.viewOffset+"px";var j=vd(a);if(!i&&0==j&&!c.force&&d.renderedView==d.view&&(null==d.updateLineNumbers||d.updateLineNumbers>=d.viewTo))return!1;var k=zd(a);return j>4&&(d.lineDiv.style.display="none"),Ed(a,d.updateLineNumbers,c.dims),j>4&&(d.lineDiv.style.display=""),d.renderedView=d.view,Ad(k),b(d.cursorDiv),b(d.selectionDiv),d.gutters.style.height=d.sizer.style.minHeight=0,i&&(d.lastWrapHeight=c.wrapperHeight,d.lastWrapWidth=c.wrapperWidth,wd(a,400)),d.updateLineNumbers=null,!0}function Cd(a,b){for(var c=b.viewport,d=!0;(d&&a.options.lineWrapping&&b.oldDisplayWidth!=Sb(a)||(c&&null!=c.top&&(c={top:Math.min(a.doc.height+Pb(a.display)-Tb(a),c.top)}),b.visible=Mc(a.display,a.doc,c),!(b.visible.from>=a.display.viewFrom&&b.visible.to<=a.display.viewTo)))&&Bd(a,b);d=!1){Kc(a);var e=ad(a);Ac(a),bd(a,e),Gd(a,e),b.force=!1}b.signal(a,"update",a),a.display.viewFrom==a.display.reportedViewFrom&&a.display.viewTo==a.display.reportedViewTo||(b.signal(a,"viewportChange",a,a.display.viewFrom,a.display.viewTo),a.display.reportedViewFrom=a.display.viewFrom,a.display.reportedViewTo=a.display.viewTo)}function Dd(a,b){var c=new uh(a,b);if(Bd(a,c)){Kc(a),Cd(a,c);var d=ad(a);Ac(a),bd(a,d),Gd(a,d),c.finish()}}function Ed(a,c,d){function e(b){var c=b.nextSibling;return pg&&zg&&a.display.currentWheelTarget==b?b.style.display="none":b.parentNode.removeChild(b),c}for(var f=a.display,g=a.options.lineNumbers,h=f.lineDiv,i=h.firstChild,j=f.view,k=f.viewFrom,l=0;l-1&&(o=!1),Ab(a,n,k,d)),o&&(b(n.lineNumber),n.lineNumber.appendChild(document.createTextNode(I(a.options,k)))),i=n.node.nextSibling}else{var p=Ib(a,n,k,d);h.insertBefore(p,i)}k+=n.size}for(;i;)i=e(i)}function Fd(a){var b=a.display.gutters.offsetWidth;a.display.sizer.style.marginLeft=b+"px"}function Gd(a,b){a.display.sizer.style.minHeight=b.docHeight+"px",a.display.heightForcer.style.top=b.docHeight+"px",a.display.gutters.style.height=b.docHeight+a.display.barHeight+Rb(a)+"px"}function Hd(a){var c=a.display.gutters,e=a.options.gutters;b(c);for(var f=0;f-1&&!a.lineNumbers&&(a.gutters=a.gutters.slice(0),a.gutters.splice(b,1))}function Jd(a){var b=a.wheelDeltaX,c=a.wheelDeltaY;return null==b&&a.detail&&a.axis==a.HORIZONTAL_AXIS&&(b=a.detail),null==c&&a.detail&&a.axis==a.VERTICAL_AXIS?c=a.detail:null==c&&(c=a.wheelDelta),{x:b,y:c}}function Kd(a){var b=Jd(a);return b.x*=wh,b.y*=wh,b}function Ld(a,b){var c=Jd(b),d=c.x,e=c.y,f=a.display,g=f.scroller,h=g.scrollWidth>g.clientWidth,i=g.scrollHeight>g.clientHeight;if(d&&h||e&&i){if(e&&zg&&pg)a:for(var j=b.target,k=f.view;j!=g;j=j.parentNode)for(var l=0;l=0){var g=O(f.from(),e.from()),h=N(f.to(),e.to()),i=f.empty()?e.from()==e.head:f.from()==f.head;d<=b&&--b,a.splice(--d,2,new yh(i?h:g,i?g:h))}}return new xh(a,b)}function Nd(a,b){return new xh([new yh(a,b||a)],0)}function Od(a){return a.text?J(a.from.line+a.text.length-1,p(a.text).length+(1==a.text.length?a.from.ch:0)):a.to}function Pd(a,b){if(K(a,b.from)<0)return a;if(K(a,b.to)<=0)return Od(b);var c=a.line+b.text.length-(b.to.line-b.from.line)-1,d=a.ch;return a.line==b.to.line&&(d+=Od(b).ch-b.to.ch),J(c,d)}function Qd(a,b){for(var c=[],d=0;d1&&a.remove(h.line+1,o-1),a.insert(h.line+1,s)}yb(a,"change",a,b)}function Xd(a,b,c){function d(a,e,f){if(a.linked)for(var g=0;g1&&!a.done[a.done.length-2].ranges?(a.done.pop(),p(a.done)):void 0}function de(a,b,c,d){var e=a.history;e.undone.length=0;var f,g,h=+new Date;if((e.lastOp==d||e.lastOrigin==b.origin&&b.origin&&("+"==b.origin.charAt(0)&&a.cm&&e.lastModTime>h-a.cm.options.historyEventDelay||"*"==b.origin.charAt(0)))&&(f=ce(e,e.lastOp==d)))g=p(f.changes),0==K(b.from,b.to)&&0==K(b.from,g.to)?g.to=Od(b):f.changes.push(ae(a,b));else{var i=p(e.done);for(i&&i.ranges||ge(a.sel,e.done),f={changes:[ae(a,b)],generation:e.generation},e.done.push(f);e.done.length>e.undoDepth;)e.done.shift(),e.done[0].ranges||e.done.shift()}e.done.push(c),e.generation=++e.maxGeneration,e.lastModTime=e.lastSelTime=h,e.lastOp=e.lastSelOp=d,e.lastOrigin=e.lastSelOrigin=b.origin,g||Ea(a,"historyAdded")}function ee(a,b,c,d){var e=b.charAt(0);return"*"==e||"+"==e&&c.ranges.length==d.ranges.length&&c.somethingSelected()==d.somethingSelected()&&new Date-a.history.lastSelTime<=(a.cm?a.cm.options.historyEventDelay:500)}function fe(a,b,c,d){var e=a.history,f=d&&d.origin;c==e.lastSelOp||f&&e.lastSelOrigin==f&&(e.lastModTime==e.lastSelTime&&e.lastOrigin==f||ee(a,f,p(e.done),b))?e.done[e.done.length-1]=b:ge(b,e.done),e.lastSelTime=+new Date,e.lastSelOrigin=f,e.lastSelOp=c,d&&d.clearRedo!==!1&&be(e.undone)}function ge(a,b){var c=p(b);c&&c.ranges&&c.equals(a)||b.push(a)}function he(a,b,c,d){var e=b["spans_"+a.id],f=0;a.iter(Math.max(a.first,c),Math.min(a.first+a.size,d),function(c){c.markedSpans&&((e||(e=b["spans_"+a.id]={}))[f]=c.markedSpans),++f})}function ie(a){if(!a)return null;for(var b,c=0;c-1&&(p(h)[l]=j[l],delete j[l])}}}return d}function me(a,b,c,d){if(d){var e=a.anchor;if(c){var f=K(b,e)<0;f!=K(c,e)<0?(e=b,b=c):f!=K(b,c)<0&&(b=c)}return new yh(e,b)}return new yh(c||b,b)}function ne(a,b,c,d,e){null==e&&(e=a.cm&&(a.cm.display.shift||a.extend)),te(a,new xh([me(a.sel.primary(),b,c,e)],0),d)}function oe(a,b,c){for(var d=[],e=a.cm&&(a.cm.display.shift||a.extend),f=0;f=b.ch:h.to>b.ch))){if(e&&(Ea(i,"beforeCursorEnter"),i.explicitlyCleared)){if(f.markedSpans){--g;continue}break}if(!i.atomic)continue;if(c){var j=i.find(d<0?1:-1),k=void 0;if((d<0?i.inclusiveRight:i.inclusiveLeft)&&(j=Ae(a,j,-d,j&&j.line==b.line?f:null)),j&&j.line==b.line&&(k=K(j,c))&&(d<0?k<0:k>0))return ye(a,j,b,d,e)}var l=i.find(d<0?-1:1);return(d<0?i.inclusiveLeft:i.inclusiveRight)&&(l=Ae(a,l,d,l.line==b.line?f:null)),l?ye(a,l,b,d,e):null}}return b}function ze(a,b,c,d,e){var f=d||1,g=ye(a,b,c,f,e)||!e&&ye(a,b,c,f,!0)||ye(a,b,c,-f,e)||!e&&ye(a,b,c,-f,!0);return g?g:(a.cantEdit=!0,J(a.first,0))}function Ae(a,b,c,d){return c<0&&0==b.ch?b.line>a.first?Q(a,J(b.line-1)):null:c>0&&b.ch==(d||B(a,b.line)).text.length?b.line=0;--e)Ee(a,{from:d[e].from,to:d[e].to,text:e?[""]:b.text,origin:b.origin});else Ee(a,b)}}function Ee(a,b){if(1!=b.text.length||""!=b.text[0]||0!=K(b.from,b.to)){var c=Qd(a,b);de(a,b,c,a.cm?a.cm.curOp.id:NaN),He(a,b,c,_(a,b));var d=[];Xd(a,function(a,c){c||m(d,a.history)!=-1||(Me(a.history,b),d.push(a.history)),He(a,b,null,_(a,b))})}}function Fe(a,b,c){if(!a.cm||!a.cm.state.suppressEdits||c){for(var d,e=a.history,f=a.sel,g="undo"==b?e.done:e.undone,h="undo"==b?e.undone:e.done,i=0;i=0;--n){var o=l(n);if(o)return o.v}}}}function Ge(a,b){if(0!=b&&(a.first+=b,a.sel=new xh(q(a.sel.ranges,function(a){return new yh(J(a.anchor.line+b,a.anchor.ch),J(a.head.line+b,a.head.ch))}),a.sel.primIndex),a.cm)){qd(a.cm,a.first,a.first-b,b);for(var c=a.cm.display,d=c.viewFrom;da.lastLine())){if(b.from.linef&&(b={from:b.from,to:J(f,B(a,f).text.length),text:[b.text[0]],origin:b.origin}),b.removed=C(a,b.from,b.to),c||(c=Qd(a,b)),a.cm?Ie(a.cm,b,d):Wd(a,b,d),ue(a,c,Ng)}}function Ie(a,b,c){var d=a.doc,e=a.display,f=b.from,g=b.to,h=!1,i=f.line;a.options.lineWrapping||(i=F(la(B(d,f.line))),d.iter(i,g.line+1,function(a){if(a==e.maxLine)return h=!0,!0})),d.sel.contains(b.from,b.to)>-1&&Ga(a),Wd(d,b,c,wc(a)),a.options.lineWrapping||(d.iter(i,f.line+b.text.length,function(a){var b=ta(a);b>e.maxLineLength&&(e.maxLine=a,e.maxLineLength=b,e.maxLineChanged=!0,h=!1)}),h&&(a.curOp.updateMaxLine=!0)),ib(d,f.line),wd(a,400);var j=b.text.length-(g.line-f.line)-1;b.full?qd(a):f.line!=g.line||1!=b.text.length||Vd(a.doc,b)?qd(a,f.line,g.line+1,j):rd(a,f.line,"text");var k=Ha(a,"changes"),l=Ha(a,"change");if(l||k){var m={from:f,to:g,text:b.text,removed:b.removed,origin:b.origin};l&&yb(a,"change",a,m),k&&(a.curOp.changeObjs||(a.curOp.changeObjs=[])).push(m)}a.display.selForContextMenu=null}function Je(a,b,c,d,e){if(d||(d=c),K(d,c)<0){var f=d;d=c,c=f}"string"==typeof b&&(b=a.splitLines(b)),De(a,{from:c,to:d,text:b,origin:e})}function Ke(a,b,c,d){c0||0==h&&g.clearWhenEmpty!==!1)return g;if(g.replacedWith&&(g.collapsed=!0,g.widgetNode=e("span",[g.replacedWith],"CodeMirror-widget"),d.handleMouseEvents||g.widgetNode.setAttribute("cm-ignore-events","true"),d.insertLeft&&(g.widgetNode.insertLeft=!0)),g.collapsed){if(ka(a,b.line,b,c,g)||b.line!=c.line&&ka(a,c.line,b,c,g))throw new Error("Inserting collapsed marker partially overlapping an existing one");U()}g.addToHistory&&de(a,{from:b,to:c,origin:"markText"},a.sel,NaN);var i,j=b.line,l=a.cm;if(a.iter(j,c.line+1,function(a){l&&g.collapsed&&!l.options.lineWrapping&&la(a)==l.display.maxLine&&(i=!0),g.collapsed&&j!=b.line&&E(a,0),Y(a,new V(g,j==b.line?b.ch:null,j==c.line?c.ch:null)),++j}),g.collapsed&&a.iter(b.line,c.line+1,function(b){qa(a,b)&&E(b,0)}),g.clearOnEnter&&Yg(g,"beforeCursorEnter",function(){return g.clear()}),g.readOnly&&(T(),(a.history.done.length||a.history.undone.length)&&a.clearHistory()),g.collapsed&&(g.id=++Ah,g.atomic=!0),l){if(i&&(l.curOp.updateMaxLine=!0),g.collapsed)qd(l,b.line,c.line+1);else if(g.className||g.title||g.startStyle||g.endStyle||g.css)for(var m=b.line;m<=c.line;m++)rd(l,m,"text");g.atomic&&we(l.doc),yb(l,"markerAdded",l,g)}return g}function Te(a,b,c,d,e){d=k(d),d.shared=!1;var f=[Se(a,b,c,d,e)],g=f[0],h=d.widgetNode;return Xd(a,function(a){h&&(d.widgetNode=h.cloneNode(!0)),f.push(Se(a,Q(a,b),Q(a,c),d,e));for(var i=0;i-1)return b.state.draggingText(a),void setTimeout(function(){return b.display.input.focus()},20);try{var j=a.dataTransfer.getData("Text");if(j){var k;if(b.state.draggingText&&!b.state.draggingText.copy&&(k=b.listSelections()),ue(b.doc,Nd(c,c)),k)for(var l=0;l=0;b--)Je(a.doc,"",d[b].from,d[b].to,"+delete");Uc(a)})}function mf(a,b){var c=B(a.doc,b),d=la(c);return d!=c&&(b=F(d)),Aa(!0,a,d,b,1)}function nf(a,b){var c=B(a.doc,b),d=ma(c);return d!=c&&(b=F(d)),Aa(!0,a,c,b,-1)}function of(a,b){var c=mf(a,b.line),d=B(a.doc,c.line),e=xa(d,a.doc.direction);if(!e||0==e[0].level){var f=Math.max(0,d.text.search(/\S/)),g=b.line==c.line&&b.ch<=f&&b.ch;return J(c.line,g?0:f,c.sticky)}return c}function pf(a,b,c){if("string"==typeof b&&(b=Mh[b],!b))return!1;a.display.input.ensurePolled();var d=a.display.shift,e=!1;try{a.isReadOnly()&&(a.state.suppressEdits=!0),c&&(a.display.shift=!1),e=b(a)!=Mg}finally{a.display.shift=d,a.state.suppressEdits=!1}return e}function qf(a,b,c){for(var d=0;d-1&&(K((e=h.ranges[e]).from(),b)<0||b.xRel>0)&&(K(e.to(),b)>0||b.xRel<0)?Df(a,d,b,f):Ff(a,d,b,f)}function Df(a,b,c,d){var e=a.display,f=!1,g=nd(a,function(b){pg&&(e.scroller.draggable=!1),a.state.draggingText=!1,Da(document,"mouseup",g),Da(document,"mousemove",h),Da(e.scroller,"dragstart",i),Da(e.scroller,"drop",g),f||(Ja(b),d.addNew||ne(a.doc,c,null,null,d.extend),pg||ng&&9==og?setTimeout(function(){document.body.focus(),e.input.focus()},20):e.input.focus())}),h=function(a){f=f||Math.abs(b.clientX-a.clientX)+Math.abs(b.clientY-a.clientY)>=10},i=function(){return f=!0};pg&&(e.scroller.draggable=!0),a.state.draggingText=g,g.copy=!d.moveOnDrag,e.scroller.dragDrop&&e.scroller.dragDrop(),Yg(document,"mouseup",g),Yg(document,"mousemove",h),Yg(e.scroller,"dragstart",i),Yg(e.scroller,"drop",g),Hc(a),setTimeout(function(){return e.input.focus()},20)}function Ef(a,b,c){if("char"==c)return new yh(b,b);if("word"==c)return a.findWordAt(b);if("line"==c)return new yh(J(b.line,0),Q(a.doc,J(b.line+1,0)));var d=c(a,b);return new yh(d.from,d.to)}function Ff(a,b,c,d){function e(b){if(0!=K(r,b))if(r=b,"rectangle"==d.unit){for(var e=[],f=a.options.tabSize,g=l(B(j,c.line).text,c.ch,f),h=l(B(j,b.line).text,b.ch,f),i=Math.min(g,h),p=Math.max(g,h),q=Math.min(c.line,b.line),s=Math.min(a.lastLine(),Math.max(c.line,b.line));q<=s;q++){var t=B(j,q).text,u=n(t,i,f);i==p?e.push(new yh(J(q,u),J(q,u))):t.length>u&&e.push(new yh(J(q,u),J(q,n(t,p,f))))}e.length||e.push(new yh(c,c)),te(j,Md(o.ranges.slice(0,m).concat(e),m),{origin:"*mouse",scroll:!1}),a.scrollIntoView(b)}else{var v,w=k,x=Ef(a,b,d.unit),y=w.anchor;K(x.anchor,y)>0?(v=x.head,y=O(w.from(),x.anchor)):(v=x.anchor,y=N(w.to(),x.head));var z=o.ranges.slice(0);z[m]=new yh(Q(j,y),v),te(j,Md(z,m),Og)}}function f(b){var c=++t,h=yc(a,b,!0,"rectangle"==d.unit);if(h)if(0!=K(h,r)){a.curOp.focus=g(),e(h);var k=Mc(i,j);(h.line>=k.to||h.lines.bottom?20:0;l&&setTimeout(nd(a,function(){t==c&&(i.scroller.scrollTop+=l,f(b))}),50)}}function h(b){a.state.selectingText=!1,t=1/0,Ja(b),i.input.focus(),Da(document,"mousemove",u),Da(document,"mouseup",v),j.history.lastSelOrigin=null}var i=a.display,j=a.doc;Ja(b);var k,m,o=j.sel,p=o.ranges;if(d.addNew&&!d.extend?(m=j.sel.contains(c),k=m>-1?p[m]:new yh(c,c)):(k=j.sel.primary(),m=j.sel.primIndex),"rectangle"==d.unit)d.addNew||(k=new yh(c,c)),c=yc(a,b,!0,!0),m=-1;else{var q=Ef(a,c,d.unit);k=d.extend?me(k,q.anchor,q.head,d.extend):q}d.addNew?m==-1?(m=p.length,te(j,Md(p.concat([k]),m),{scroll:!1,origin:"*mouse"})):p.length>1&&p[m].empty()&&"char"==d.unit&&!d.extend?(te(j,Md(p.slice(0,m).concat(p.slice(m+1)),0),{scroll:!1,origin:"*mouse"}),o=j.sel):pe(j,m,k,Og):(m=0,te(j,new xh([k],0),Og),o=j.sel);var r=c,s=i.wrapper.getBoundingClientRect(),t=0,u=nd(a,function(a){Oa(a)?f(a):h(a)}),v=nd(a,h);a.state.selectingText=v,Yg(document,"mousemove",u),Yg(document,"mouseup",v)}function Gf(a,b,c,d){var e,f;try{e=b.clientX,f=b.clientY}catch(b){return!1}if(e>=Math.floor(a.display.gutters.getBoundingClientRect().right))return!1;d&&Ja(b);var g=a.display,h=g.lineDiv.getBoundingClientRect();if(f>h.bottom||!Ha(a,c))return La(b);f-=h.top-g.viewOffset;for(var i=0;i=e){var k=G(a.doc,f),l=a.options.gutters[i];return Ea(a,c,a,k,l,b),La(b)}}}function Hf(a,b){return Gf(a,b,"gutterClick",!0)}function If(a,b){Nb(a.display,b)||Jf(a,b)||Fa(a,b,"contextmenu")||a.display.input.onContextMenu(b)}function Jf(a,b){return!!Ha(a,"gutterContextMenu")&&Gf(a,b,"gutterContextMenu",!1)}function Kf(a){a.display.wrapper.className=a.display.wrapper.className.replace(/\s*cm-s-\S+/g,"")+a.options.theme.replace(/(^|\s)\s*/g," cm-s-"),fc(a)}function Lf(a){function b(b,d,e,f){a.defaults[b]=d,e&&(c[b]=f?function(a,b,c){c!=Th&&e(a,b,c)}:e)}var c=a.optionHandlers;a.defineOption=b,a.Init=Th,b("value","",function(a,b){return a.setValue(b)},!0),b("mode",null,function(a,b){a.doc.modeOption=b,Td(a)},!0),b("indentUnit",2,Td,!0),b("indentWithTabs",!1),b("smartIndent",!0),b("tabSize",4,function(a){Ud(a),fc(a),qd(a)},!0),b("lineSeparator",null,function(a,b){if(a.doc.lineSep=b,b){var c=[],d=a.doc.first;a.doc.iter(function(a){for(var e=0;;){var f=a.text.indexOf(b,e);if(f==-1)break;e=f+b.length,c.push(J(d,f))}d++});for(var e=c.length-1;e>=0;e--)Je(a.doc,b,c[e],J(c[e].line,c[e].ch+b.length))}}),b("specialChars",/[\u0000-\u001f\u007f-\u009f\u00ad\u061c\u200b-\u200f\u2028\u2029\ufeff]/g,function(a,b,c){a.state.specialChars=new RegExp(b.source+(b.test("\t")?"":"|\t"),"g"),c!=Th&&a.refresh()}),b("specialCharPlaceholder",nb,function(a){return a.refresh()},!0),b("electricChars",!0),b("inputStyle",yg?"contenteditable":"textarea",function(){throw new Error("inputStyle can not (yet) be changed in a running editor")},!0),b("spellcheck",!1,function(a,b){return a.getInputField().spellcheck=b},!0),b("rtlMoveVisually",!Bg),b("wholeLineUpdateBefore",!0),b("theme","default",function(a){Kf(a),Mf(a)},!0),b("keyMap","default",function(a,b,c){var d=kf(b),e=c!=Th&&kf(c);e&&e.detach&&e.detach(a,d),d.attach&&d.attach(a,e||null)}),b("extraKeys",null),b("configureMouse",null),b("lineWrapping",!1,Of,!0),b("gutters",[],function(a){Id(a.options),Mf(a)},!0),b("fixedGutter",!0,function(a,b){a.display.gutters.style.left=b?vc(a.display)+"px":"0",a.refresh()},!0),b("coverGutterNextToScrollbar",!1,function(a){return bd(a)},!0),b("scrollbarStyle","native",function(a){dd(a),bd(a),a.display.scrollbars.setScrollTop(a.doc.scrollTop),a.display.scrollbars.setScrollLeft(a.doc.scrollLeft)},!0),b("lineNumbers",!1,function(a){Id(a.options),Mf(a)},!0),b("firstLineNumber",1,Mf,!0),b("lineNumberFormatter",function(a){return a},Mf,!0),b("showCursorWhenSelecting",!1,Ac,!0),b("resetSelectionOnContextMenu",!0),b("lineWiseCopyCut",!0),b("pasteLinesPerSelection",!0),b("readOnly",!1,function(a,b){"nocursor"==b&&(Jc(a),a.display.input.blur()),a.display.input.readOnlyChanged(b)}),b("disableInput",!1,function(a,b){b||a.display.input.reset()},!0),b("dragDrop",!0,Nf),b("allowDropFileTypes",null),b("cursorBlinkRate",530),b("cursorScrollMargin",0),b("cursorHeight",1,Ac,!0),b("singleCursorHeightPerLine",!0,Ac,!0),b("workTime",100),b("workDelay",100),b("flattenSpans",!0,Ud,!0),b("addModeClass",!1,Ud,!0),b("pollInterval",100),b("undoDepth",200,function(a,b){return a.doc.history.undoDepth=b}),b("historyEventDelay",1250),b("viewportMargin",10,function(a){return a.refresh()},!0),b("maxHighlightLength",1e4,Ud,!0),b("moveInputWithCursor",!0,function(a,b){b||a.display.input.resetPosition()}),b("tabindex",null,function(a,b){return a.display.input.getField().tabIndex=b||""}),b("autofocus",null),b("direction","ltr",function(a,b){return a.doc.setDirection(b)},!0)}function Mf(a){Hd(a),qd(a),Nc(a)}function Nf(a,b,c){var d=c&&c!=Th;if(!b!=!d){var e=a.display.dragFunctions,f=b?Yg:Da;f(a.display.scroller,"dragstart",e.start),f(a.display.scroller,"dragenter",e.enter),f(a.display.scroller,"dragover",e.over),f(a.display.scroller,"dragleave",e.leave),f(a.display.scroller,"drop",e.drop)}}function Of(a){a.options.lineWrapping?(h(a.display.wrapper,"CodeMirror-wrap"),a.display.sizer.style.minWidth="",a.display.sizerWidth=null):(Gg(a.display.wrapper,"CodeMirror-wrap"),ua(a)),xc(a),qd(a),fc(a),setTimeout(function(){return bd(a)},100)}function Pf(a,b){var c=this;if(!(this instanceof Pf))return new Pf(a,b);this.options=b=b?k(b):{},k(Uh,b,!1),Id(b);var d=b.value;"string"==typeof d&&(d=new Eh(d,b.mode,null,b.lineSeparator,b.direction)),this.doc=d;var e=new Pf.inputStyles[b.inputStyle](this),f=this.display=new A(a,d,e);f.wrapper.CodeMirror=this,Hd(this),Kf(this),b.lineWrapping&&(this.display.wrapper.className+=" CodeMirror-wrap"),dd(this),this.state={keyMaps:[],overlays:[],modeGen:0,overwrite:!1,delayingBlurEvent:!1,focused:!1,suppressEdits:!1,pasteIncoming:!1,cutIncoming:!1,selectingText:!1,draggingText:!1,highlight:new Ig,keySeq:null,specialChars:null},b.autofocus&&!yg&&f.input.focus(),ng&&og<11&&setTimeout(function(){return c.display.input.reset(!0)},20),Qf(this),af(),ed(this),this.curOp.forceUpdate=!0,Yd(this,d),b.autofocus&&!yg||this.hasFocus()?setTimeout(j(Ic,this),20):Jc(this);for(var g in Vh)Vh.hasOwnProperty(g)&&Vh[g](c,b[g],Th);Oc(this),b.finishInit&&b.finishInit(this);for(var h=0;h400}var e=a.display;Yg(e.scroller,"mousedown",nd(a,zf)),ng&&og<11?Yg(e.scroller,"dblclick",nd(a,function(b){if(!Fa(a,b)){var c=yc(a,b);if(c&&!Hf(a,b)&&!Nb(a.display,b)){Ja(b);var d=a.findWordAt(c);ne(a.doc,d.anchor,d.head)}}})):Yg(e.scroller,"dblclick",function(b){return Fa(a,b)||Ja(b)}),Fg||Yg(e.scroller,"contextmenu",function(b){return If(a,b)});var f,g={end:0};Yg(e.scroller,"touchstart",function(b){if(!Fa(a,b)&&!c(b)){e.input.ensurePolled(),clearTimeout(f);var d=+new Date;e.activeTouch={start:d,moved:!1,prev:d-g.end<=300?g:null},1==b.touches.length&&(e.activeTouch.left=b.touches[0].pageX,e.activeTouch.top=b.touches[0].pageY)}}),Yg(e.scroller,"touchmove",function(){e.activeTouch&&(e.activeTouch.moved=!0)}),Yg(e.scroller,"touchend",function(c){var f=e.activeTouch;if(f&&!Nb(e,c)&&null!=f.left&&!f.moved&&new Date-f.start<300){var g,h=a.coordsChar(e.activeTouch,"page");g=!f.prev||d(f,f.prev)?new yh(h,h):!f.prev.prev||d(f,f.prev.prev)?a.findWordAt(h):new yh(J(h.line,0),Q(a.doc,J(h.line+1,0))),a.setSelection(g.anchor,g.head),a.focus(),Ja(c)}b()}),Yg(e.scroller,"touchcancel",b),Yg(e.scroller,"scroll",function(){e.scroller.clientHeight&&(Zc(a,e.scroller.scrollTop),_c(a,e.scroller.scrollLeft,!0),Ea(a,"scroll",a))}),Yg(e.scroller,"mousewheel",function(b){return Ld(a,b)}),Yg(e.scroller,"DOMMouseScroll",function(b){return Ld(a,b)}),Yg(e.wrapper,"scroll",function(){return e.wrapper.scrollTop=e.wrapper.scrollLeft=0}),e.dragFunctions={enter:function(b){Fa(a,b)||Ma(b)},over:function(b){Fa(a,b)||(Ze(a,b),Ma(b))},start:function(b){return Ye(a,b)},drop:nd(a,Xe),leave:function(b){Fa(a,b)||$e(a)}};var h=e.input.getField();Yg(h,"keyup",function(b){return wf.call(a,b)}),Yg(h,"keydown",nd(a,uf)),Yg(h,"keypress",nd(a,xf)),Yg(h,"focus",function(b){return Ic(a,b)}),Yg(h,"blur",function(b){return Jc(a,b)})}function Rf(a,b,c,d){var e,f=a.doc;null==c&&(c="add"),"smart"==c&&(f.mode.indent?e=ab(a,b).state:c="prev");var g=a.options.tabSize,h=B(f,b),i=l(h.text,null,g);h.stateAfter&&(h.stateAfter=null);var j,k=h.text.match(/^\s*/)[0];if(d||/\S/.test(h.text)){if("smart"==c&&(j=f.mode.indent(e,h.text.slice(k.length),h.text),j==Mg||j>150)){if(!d)return;c="prev"}}else j=0,c="not";"prev"==c?j=b>f.first?l(B(f,b-1).text,null,g):0:"add"==c?j=i+a.options.indentUnit:"subtract"==c?j=i-a.options.indentUnit:"number"==typeof c&&(j=i+c),j=Math.max(0,j);var m="",n=0;if(a.options.indentWithTabs)for(var p=Math.floor(j/g);p;--p)n+=g,m+="\t";if(n1)if(Xh&&Xh.text.join("\n")==b){if(d.ranges.length%Xh.text.length==0){i=[];for(var j=0;j=0;l--){var m=d.ranges[l],n=m.from(),o=m.to();m.empty()&&(c&&c>0?n=J(n.line,n.ch-c):a.state.overwrite&&!g?o=J(o.line,Math.min(B(f,o.line).text.length,o.ch+p(h).length)):Xh&&Xh.lineWise&&Xh.text.join("\n")==b&&(n=o=J(n.line,0))),k=a.curOp.updateInput;var r={from:n,to:o,text:i?i[l%i.length]:h,origin:e||(g?"paste":a.state.cutIncoming?"cut":"+input")};De(a.doc,r),yb(a,"inputRead",a,r)}b&&!g&&Vf(a,b),Uc(a),a.curOp.updateInput=k,a.curOp.typing=!0,a.state.pasteIncoming=a.state.cutIncoming=!1}function Uf(a,b){var c=a.clipboardData&&a.clipboardData.getData("Text");if(c)return a.preventDefault(),b.isReadOnly()||b.options.disableInput||md(b,function(){return Tf(b,c,0,null,"paste")}),!0}function Vf(a,b){if(a.options.electricChars&&a.options.smartIndent)for(var c=a.doc.sel,d=c.ranges.length-1;d>=0;d--){var e=c.ranges[d];if(!(e.head.ch>100||d&&c.ranges[d-1].head.line==e.head.line)){var f=a.getModeAt(e.head),g=!1;if(f.electricChars){for(var h=0;h-1){g=Rf(a,e.head.line,"smart");break}}else f.electricInput&&f.electricInput.test(B(a.doc,e.head.line).text.slice(0,e.head.ch))&&(g=Rf(a,e.head.line,"smart"));g&&yb(a,"electricInput",a,e.head.line)}}}function Wf(a){for(var b=[],c=[],d=0;d=a.first+a.size)&&(b=new J(d,b.ch,b.sticky),j=B(a,d))}function g(d){var g;if(g=e?Ba(a.cm,j,b,c):za(j,b,c),null==g){if(d||!f())return!1;b=Aa(e,a.cm,j,b.line,c)}else b=g;return!0}var h=b,i=c,j=B(a,b.line);if("char"==d)g();else if("column"==d)g(!0);else if("word"==d||"group"==d)for(var k=null,l="group"==d,m=a.cm&&a.cm.getHelper(b,"wordChars"),n=!0;!(c<0)||g(!n);n=!1){var o=j.text.charAt(b.ch)||"\n",p=v(o,m)?"w":l&&"\n"==o?"n":!l||/\s/.test(o)?null:"p";if(!l||n||p||(p="s"),k&&k!=p){c<0&&(c=1,g(),b.sticky="after");break}if(p&&(k=p),c>0&&!g(!n))break}var q=ze(a,b,h,i,!0);return L(h,q)&&(q.hitSide=!0),q}function $f(a,b,c,d){var e,f=a.doc,g=b.left;if("page"==d){var h=Math.min(a.display.wrapper.clientHeight,window.innerHeight||document.documentElement.clientHeight),i=Math.max(h-.5*sc(a.display),3);e=(c>0?b.bottom:b.top)+c*i}else"line"==d&&(e=c>0?b.bottom+3:b.top-3);for(var j;j=oc(a,g,e),j.outside;){if(c<0?e<=0:e>=f.height){j.hitSide=!0;break}e+=5*c}return j}function _f(a,b){var c=Yb(a,b.line);if(!c||c.hidden)return null;var d=B(a.doc,b.line),e=Vb(c,d,b.line),f=xa(d,a.doc.direction),g="left";if(f){var h=wa(f,b.ch);g=h%2?"right":"left"}var i=_b(e.map,b.ch,g);return i.offset="right"==i.collapse?i.end:i.start,i}function ag(a){for(var b=a;b;b=b.parentNode)if(/CodeMirror-gutter-wrapper/.test(b.className))return!0;return!1}function bg(a,b){return b&&(a.bad=!0),a}function cg(a,b,c,d,e){function f(a){return function(b){return b.id==a}}function g(){k&&(j+=l,k=!1)}function h(a){a&&(g(),j+=a)}function i(b){if(1==b.nodeType){var c=b.getAttribute("cm-text");if(null!=c)return void h(c||b.textContent.replace(/\u200b/g,""));var j,m=b.getAttribute("cm-marker");if(m){var n=a.findMarks(J(d,0),J(e+1,0),f(+m));return void(n.length&&(j=n[0].find(0))&&h(C(a.doc,j.from,j.to).join(l)))}if("false"==b.getAttribute("contenteditable"))return;var o=/^(pre|div|p)$/i.test(b.nodeName);o&&g();for(var p=0;p=15&&(sg=!1,pg=!0);var Dg,Eg=zg&&(qg||sg&&(null==Cg||Cg<12.11)),Fg=jg||ng&&og>=9,Gg=function(b,c){var d=b.className,e=a(c).exec(d);if(e){var f=d.slice(e.index+e[0].length);b.className=d.slice(0,e.index)+(f?e[1]+f:"")}};Dg=document.createRange?function(a,b,c,d){var e=document.createRange();return e.setEnd(d||a,c),e.setStart(a,b),e}:function(a,b,c){var d=document.body.createTextRange();try{d.moveToElementText(a.parentNode)}catch(e){return d}return d.collapse(!0),d.moveEnd("character",c),d.moveStart("character",b),d};var Hg=function(a){a.select()};wg?Hg=function(a){a.selectionStart=0,a.selectionEnd=a.value.length}:ng&&(Hg=function(a){try{a.select()}catch(b){}});var Ig=function(){this.id=null};Ig.prototype.set=function(a,b){clearTimeout(this.id),this.id=setTimeout(b,a)};var Jg,Kg,Lg=30,Mg={toString:function(){return"CodeMirror.Pass"}},Ng={scroll:!1},Og={origin:"*mouse"},Pg={origin:"+move"},Qg=[""],Rg=/[\u00df\u0587\u0590-\u05f4\u0600-\u06ff\u3040-\u309f\u30a0-\u30ff\u3400-\u4db5\u4e00-\u9fcc\uac00-\ud7af]/,Sg=/[\u0300-\u036f\u0483-\u0489\u0591-\u05bd\u05bf\u05c1\u05c2\u05c4\u05c5\u05c7\u0610-\u061a\u064b-\u065e\u0670\u06d6-\u06dc\u06de-\u06e4\u06e7\u06e8\u06ea-\u06ed\u0711\u0730-\u074a\u07a6-\u07b0\u07eb-\u07f3\u0816-\u0819\u081b-\u0823\u0825-\u0827\u0829-\u082d\u0900-\u0902\u093c\u0941-\u0948\u094d\u0951-\u0955\u0962\u0963\u0981\u09bc\u09be\u09c1-\u09c4\u09cd\u09d7\u09e2\u09e3\u0a01\u0a02\u0a3c\u0a41\u0a42\u0a47\u0a48\u0a4b-\u0a4d\u0a51\u0a70\u0a71\u0a75\u0a81\u0a82\u0abc\u0ac1-\u0ac5\u0ac7\u0ac8\u0acd\u0ae2\u0ae3\u0b01\u0b3c\u0b3e\u0b3f\u0b41-\u0b44\u0b4d\u0b56\u0b57\u0b62\u0b63\u0b82\u0bbe\u0bc0\u0bcd\u0bd7\u0c3e-\u0c40\u0c46-\u0c48\u0c4a-\u0c4d\u0c55\u0c56\u0c62\u0c63\u0cbc\u0cbf\u0cc2\u0cc6\u0ccc\u0ccd\u0cd5\u0cd6\u0ce2\u0ce3\u0d3e\u0d41-\u0d44\u0d4d\u0d57\u0d62\u0d63\u0dca\u0dcf\u0dd2-\u0dd4\u0dd6\u0ddf\u0e31\u0e34-\u0e3a\u0e47-\u0e4e\u0eb1\u0eb4-\u0eb9\u0ebb\u0ebc\u0ec8-\u0ecd\u0f18\u0f19\u0f35\u0f37\u0f39\u0f71-\u0f7e\u0f80-\u0f84\u0f86\u0f87\u0f90-\u0f97\u0f99-\u0fbc\u0fc6\u102d-\u1030\u1032-\u1037\u1039\u103a\u103d\u103e\u1058\u1059\u105e-\u1060\u1071-\u1074\u1082\u1085\u1086\u108d\u109d\u135f\u1712-\u1714\u1732-\u1734\u1752\u1753\u1772\u1773\u17b7-\u17bd\u17c6\u17c9-\u17d3\u17dd\u180b-\u180d\u18a9\u1920-\u1922\u1927\u1928\u1932\u1939-\u193b\u1a17\u1a18\u1a56\u1a58-\u1a5e\u1a60\u1a62\u1a65-\u1a6c\u1a73-\u1a7c\u1a7f\u1b00-\u1b03\u1b34\u1b36-\u1b3a\u1b3c\u1b42\u1b6b-\u1b73\u1b80\u1b81\u1ba2-\u1ba5\u1ba8\u1ba9\u1c2c-\u1c33\u1c36\u1c37\u1cd0-\u1cd2\u1cd4-\u1ce0\u1ce2-\u1ce8\u1ced\u1dc0-\u1de6\u1dfd-\u1dff\u200c\u200d\u20d0-\u20f0\u2cef-\u2cf1\u2de0-\u2dff\u302a-\u302f\u3099\u309a\ua66f-\ua672\ua67c\ua67d\ua6f0\ua6f1\ua802\ua806\ua80b\ua825\ua826\ua8c4\ua8e0-\ua8f1\ua926-\ua92d\ua947-\ua951\ua980-\ua982\ua9b3\ua9b6-\ua9b9\ua9bc\uaa29-\uaa2e\uaa31\uaa32\uaa35\uaa36\uaa43\uaa4c\uaab0\uaab2-\uaab4\uaab7\uaab8\uaabe\uaabf\uaac1\uabe5\uabe8\uabed\udc00-\udfff\ufb1e\ufe00-\ufe0f\ufe20-\ufe26\uff9e\uff9f]/,Tg=!1,Ug=!1,Vg=null,Wg=function(){function a(a){return a<=247?c.charAt(a):1424<=a&&a<=1524?"R":1536<=a&&a<=1785?d.charAt(a-1536):1774<=a&&a<=2220?"r":8192<=a&&a<=8203?"w":8204==a?"b":"L"}function b(a,b,c){this.level=a,this.from=b,this.to=c}var c="bbbbbbbbbtstwsbbbbbbbbbbbbbbssstwNN%%%NNNNNN,N,N1111111111NNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNNNLLLLLLLLLLLLLLLLLLLLLLLLLLNNNNbbbbbbsbbbbbbbbbbbbbbbbbbbbbbbbbb,N%%%%NNNNLNNNNN%%11NLNNN1LNNNNNLLLLLLLLLLLLLLLLLLLLLLLNLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLLN",d="nnnnnnNNr%%r,rNNmmmmmmmmmmmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmmmmmmmmmmmmmmmnnnnnnnnnn%nnrrrmrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrrmmmmmmmnNmmmmmmrrmmNmmmmrr1111111111",e=/[\u0590-\u05f4\u0600-\u06ff\u0700-\u08ac]/,f=/[stwN]/,g=/[LRr]/,h=/[Lb1n]/,i=/[1n]/;return function(c,d){var j="ltr"==d?"L":"R";if(0==c.length||"ltr"==d&&!e.test(c))return!1;for(var k=c.length,l=[],m=0;m=this.string.length},fh.prototype.sol=function(){return this.pos==this.lineStart},fh.prototype.peek=function(){return this.string.charAt(this.pos)||void 0},fh.prototype.next=function(){if(this.posb},fh.prototype.eatSpace=function(){for(var a=this,b=this.pos;/[\s\u00a0]/.test(this.string.charAt(this.pos));)++a.pos;return this.pos>b},fh.prototype.skipToEnd=function(){this.pos=this.string.length},fh.prototype.skipTo=function(a){var b=this.string.indexOf(a,this.pos);if(b>-1)return this.pos=b,!0},fh.prototype.backUp=function(a){this.pos-=a},fh.prototype.column=function(){return this.lastColumnPos0?null:(d&&b!==!1&&(this.pos+=d[0].length),d)}var e=function(a){return c?a.toLowerCase():a},f=this.string.substr(this.pos,a.length);if(e(f)==e(a))return b!==!1&&(this.pos+=a.length),!0},fh.prototype.current=function(){return this.string.slice(this.start,this.pos)},fh.prototype.hideFirstChars=function(a,b){this.lineStart+=a;try{return b()}finally{this.lineStart-=a}},fh.prototype.lookAhead=function(a){var b=this.lineOracle;return b&&b.lookAhead(a)};var gh=function(a,b){this.state=a,this.lookAhead=b},hh=function(a,b,c,d){this.state=b,this.doc=a,this.line=c,this.maxLookAhead=d||0};hh.prototype.lookAhead=function(a){var b=this.doc.getLine(this.line+a);return null!=b&&a>this.maxLookAhead&&(this.maxLookAhead=a),b},hh.prototype.nextLine=function(){this.line++,this.maxLookAhead>0&&this.maxLookAhead--},hh.fromSaved=function(a,b,c){return b instanceof gh?new hh(a,Xa(a.mode,b.state),c,b.lookAhead):new hh(a,Xa(a.mode,b),c)},hh.prototype.save=function(a){var b=a!==!1?Xa(this.doc.mode,this.state):this.state;return this.maxLookAhead>0?new gh(b,this.maxLookAhead):b};var ih=function(a,b,c){this.start=a.start,this.end=a.pos,this.string=a.current(),this.type=b||null,this.state=c},jh=function(a,b,c){this.text=a,da(this,b),this.height=c?c(this):1};jh.prototype.lineNo=function(){return F(this)},Ia(jh);var kh,lh={},mh={},nh=null,oh=null,ph={left:0,right:0,top:0,bottom:0},qh=function(a,b,c){this.cm=c;var e=this.vert=d("div",[d("div",null,null,"min-width: 1px")],"CodeMirror-vscrollbar"),f=this.horiz=d("div",[d("div",null,null,"height: 100%; min-height: 1px")],"CodeMirror-hscrollbar");a(e),a(f),Yg(e,"scroll",function(){e.clientHeight&&b(e.scrollTop,"vertical")}),Yg(f,"scroll",function(){f.clientWidth&&b(f.scrollLeft,"horizontal")}),this.checkedZeroWidth=!1,ng&&og<8&&(this.horiz.style.minHeight=this.vert.style.minWidth="18px")};qh.prototype.update=function(a){var b=a.scrollWidth>a.clientWidth+1,c=a.scrollHeight>a.clientHeight+1,d=a.nativeBarWidth;if(c){this.vert.style.display="block",this.vert.style.bottom=b?d+"px":"0";var e=a.viewHeight-(b?d:0);this.vert.firstChild.style.height=Math.max(0,a.scrollHeight-a.clientHeight+e)+"px"}else this.vert.style.display="",this.vert.firstChild.style.height="0";if(b){this.horiz.style.display="block",this.horiz.style.right=c?d+"px":"0",this.horiz.style.left=a.barLeft+"px";var f=a.viewWidth-a.barLeft-(c?d:0);this.horiz.firstChild.style.width=Math.max(0,a.scrollWidth-a.clientWidth+f)+"px"}else this.horiz.style.display="",this.horiz.firstChild.style.width="0";return!this.checkedZeroWidth&&a.clientHeight>0&&(0==d&&this.zeroWidthHack(),this.checkedZeroWidth=!0),{right:c?d:0,bottom:b?d:0}},qh.prototype.setScrollLeft=function(a){this.horiz.scrollLeft!=a&&(this.horiz.scrollLeft=a),this.disableHoriz&&this.enableZeroWidthBar(this.horiz,this.disableHoriz,"horiz")},qh.prototype.setScrollTop=function(a){this.vert.scrollTop!=a&&(this.vert.scrollTop=a),this.disableVert&&this.enableZeroWidthBar(this.vert,this.disableVert,"vert")},qh.prototype.zeroWidthHack=function(){var a=zg&&!ug?"12px":"18px";this.horiz.style.height=this.vert.style.width=a,this.horiz.style.pointerEvents=this.vert.style.pointerEvents="none",this.disableHoriz=new Ig,this.disableVert=new Ig},qh.prototype.enableZeroWidthBar=function(a,b,c){function d(){var e=a.getBoundingClientRect(),f="vert"==c?document.elementFromPoint(e.right-1,(e.top+e.bottom)/2):document.elementFromPoint((e.right+e.left)/2,e.bottom-1);f!=a?a.style.pointerEvents="none":b.set(1e3,d)}a.style.pointerEvents="auto",b.set(1e3,d)},qh.prototype.clear=function(){var a=this.horiz.parentNode;a.removeChild(this.horiz),a.removeChild(this.vert)};var rh=function(){};rh.prototype.update=function(){return{bottom:0,right:0}},rh.prototype.setScrollLeft=function(){},rh.prototype.setScrollTop=function(){},rh.prototype.clear=function(){};var sh={"native":qh,"null":rh},th=0,uh=function(a,b,c){var d=a.display;this.viewport=b,this.visible=Mc(d,a.doc,b),this.editorIsHidden=!d.wrapper.offsetWidth,this.wrapperHeight=d.wrapper.clientHeight,this.wrapperWidth=d.wrapper.clientWidth,this.oldDisplayWidth=Sb(a),this.force=c,this.dims=uc(a),this.events=[]};uh.prototype.signal=function(a,b){Ha(a,b)&&this.events.push(arguments)},uh.prototype.finish=function(){for(var a=this,b=0;b=0&&K(a,e.to())<=0)return d}return-1};var yh=function(a,b){this.anchor=a,this.head=b};yh.prototype.from=function(){return O(this.anchor,this.head)},yh.prototype.to=function(){return N(this.anchor,this.head)},yh.prototype.empty=function(){return this.head.line==this.anchor.line&&this.head.ch==this.anchor.ch},Oe.prototype={chunkSize:function(){return this.lines.length},removeInner:function(a,b){for(var c=this,d=a,e=a+b;d1||!(this.children[0]instanceof Oe))){var i=[];this.collapse(i),this.children=[new Oe(i)],this.children[0].parent=this}},collapse:function(a){for(var b=this,c=0;c50){for(var h=f.lines.length%25+25,i=h;i10);a.parent.maybeSpill()}},iterN:function(a,b,c){for(var d=this,e=0;eb.display.maxLineLength&&(b.display.maxLine=k,b.display.maxLineLength=l,b.display.maxLineChanged=!0)}null!=e&&b&&this.collapsed&&qd(b,e,f+1),this.lines.length=0,this.explicitlyCleared=!0,this.atomic&&this.doc.cantEdit&&(this.doc.cantEdit=!1,b&&we(b.doc)),b&&yb(b,"markerCleared",b,this,e,f),c&&fd(b),this.parent&&this.parent.clear()}},Bh.prototype.find=function(a,b){var c=this;null==a&&"bookmark"==this.type&&(a=1);for(var d,e,f=0;f=0;j--)De(d,e[j]);i?se(this,i):this.cm&&Uc(this.cm)}),undo:pd(function(){Fe(this,"undo")}),redo:pd(function(){Fe(this,"redo")}),undoSelection:pd(function(){Fe(this,"undo",!0)}),redoSelection:pd(function(){Fe(this,"redo",!0)}),setExtending:function(a){this.extend=a},getExtending:function(){return this.extend},historySize:function(){for(var a=this.history,b=0,c=0,d=0;d=a.ch)&&b.push(e.marker.parent||e.marker)}return b},findMarks:function(a,b,c){a=Q(this,a),b=Q(this,b);var d=[],e=a.line;return this.iter(a.line,b.line+1,function(f){var g=f.markedSpans;if(g)for(var h=0;h=i.to||null==i.from&&e!=a.line||null!=i.from&&e==b.line&&i.from>=b.ch||c&&!c(i.marker)||d.push(i.marker.parent||i.marker)}++e}),d},getAllMarks:function(){var a=[];return this.iter(function(b){var c=b.markedSpans;if(c)for(var d=0;da?(b=a,!0):(a-=f,void++c)}),Q(this,J(c,b))},indexFromPos:function(a){a=Q(this,a);var b=a.ch;if(a.lineb&&(b=a.from),null!=a.to&&a.to0)e=new J(e.line,e.ch+1),a.replaceRange(f.charAt(e.ch-1)+f.charAt(e.ch-2),J(e.line,e.ch-2),e,"+transpose");else if(e.line>a.doc.first){var g=B(a.doc,e.line-1).text;g&&(e=new J(e.line,1),a.replaceRange(f.charAt(0)+a.doc.lineSeparator()+g.charAt(g.length-1),J(e.line-1,g.length-1),e,"+transpose"))}c.push(new yh(e,e))}a.setSelections(c)})},newlineAndIndent:function(a){return md(a,function(){for(var b=a.listSelections(),c=b.length-1;c>=0;c--)a.replaceRange(a.doc.lineSeparator(),b[c].anchor,b[c].head,"+input");b=a.listSelections();for(var d=0;da&&0==K(b,this.pos)&&c==this.button};var Rh,Sh,Th={toString:function(){return"CodeMirror.Init"}},Uh={},Vh={};Pf.defaults=Uh,Pf.optionHandlers=Vh;var Wh=[];Pf.defineInitHook=function(a){return Wh.push(a)};var Xh=null,Yh=function(a){var b=a.optionHandlers,c=a.helpers={};a.prototype={constructor:a,focus:function(){window.focus(),this.display.input.focus()},setOption:function(a,c){var d=this.options,e=d[a];d[a]==c&&"mode"!=a||(d[a]=c,b.hasOwnProperty(a)&&nd(this,b[a])(this,c,e),Ea(this,"optionChange",this,a))},getOption:function(a){return this.options[a]},getDoc:function(){return this.doc},addKeyMap:function(a,b){this.state.keyMaps[b?"push":"unshift"](kf(a))},removeKeyMap:function(a){for(var b=this.state.keyMaps,c=0;cd&&(Rf(b,f.head.line,a,!0),d=f.head.line,e==b.doc.sel.primIndex&&Uc(b));else{var g=f.from(),h=f.to(),i=Math.max(d,g.line);d=Math.min(b.lastLine(),h.line-(h.ch?0:1))+1;for(var j=i;j0&&pe(b.doc,e,new yh(g,k[e].to()),Ng)}}}),getTokenAt:function(a,b){return eb(this,a,b)},getLineTokens:function(a,b){return eb(this,J(a),b,!0)},getTokenTypeAt:function(a){a=Q(this.doc,a);var b,c=_a(this,B(this.doc,a.line)),d=0,e=(c.length-1)/2,f=a.ch;if(0==f)b=c[2];else for(;;){var g=d+e>>1;if((g?c[2*g-1]:0)>=f)e=g;else{if(!(c[2*g+1]f&&(a=f,e=!0),d=B(this.doc,a)}else d=a;return ic(this,d,{top:0,left:0},b||"page",c||e).top+(e?this.doc.height-sa(d):0)},defaultTextHeight:function(){return sc(this.display)},defaultCharWidth:function(){return tc(this.display)},getViewport:function(){return{from:this.display.viewFrom,to:this.display.viewTo}},addWidget:function(a,b,c,d,e){var f=this.display;a=lc(this,Q(this.doc,a));var g=a.bottom,h=a.left;if(b.style.position="absolute",b.setAttribute("cm-ignore-events","true"),this.display.input.setUneditable(b),f.sizer.appendChild(b),"over"==d)g=a.top;else if("above"==d||"near"==d){var i=Math.max(f.wrapper.clientHeight,this.doc.height),j=Math.max(f.sizer.clientWidth,f.lineSpace.clientWidth);("above"==d||a.bottom+b.offsetHeight>i)&&a.top>b.offsetHeight?g=a.top-b.offsetHeight:a.bottom+b.offsetHeight<=i&&(g=a.bottom),h+b.offsetWidth>j&&(h=j-b.offsetWidth)}b.style.top=g+"px",b.style.left=b.style.right="","right"==e?(h=f.sizer.clientWidth-b.offsetWidth,b.style.right="0px"):("left"==e?h=0:"middle"==e&&(h=(f.sizer.clientWidth-b.offsetWidth)/2),b.style.left=h+"px"),c&&Rc(this,{left:h,top:g,right:h+b.offsetWidth,bottom:g+b.offsetHeight})},triggerOnKeyDown:od(uf),triggerOnKeyPress:od(xf),triggerOnKeyUp:wf,triggerOnMouseDown:od(zf),execCommand:function(a){if(Mh.hasOwnProperty(a))return Mh[a].call(null,this)},triggerElectric:od(function(a){Vf(this,a)}),findPosH:function(a,b,c,d){var e=this,f=1;b<0&&(f=-1,b=-b);for(var g=Q(this.doc,a),h=0;h0&&h(c.charAt(d-1));)--d;for(;e.5)&&xc(this),Ea(this,"refresh",this)}),swapDoc:od(function(a){var b=this.doc;return b.cm=null,Yd(this,a),fc(this),this.display.input.reset(),Vc(this,a.scrollLeft,a.scrollTop),this.curOp.forceScroll=!0,yb(this,"swapDoc",this,b),b}),getInputField:function(){return this.display.input.getField()},getWrapperElement:function(){return this.display.wrapper},getScrollerElement:function(){return this.display.scroller},getGutterElement:function(){return this.display.gutters}},Ia(a),a.registerHelper=function(b,d,e){c.hasOwnProperty(b)||(c[b]=a[b]={_global:[]}),c[b][d]=e},a.registerGlobalHelper=function(b,d,e,f){a.registerHelper(b,d,f),c[b]._global.push({pred:e,val:f})}},Zh=function(a){this.cm=a,this.lastAnchorNode=this.lastAnchorOffset=this.lastFocusNode=this.lastFocusOffset=null,this.polling=new Ig,this.composing=null,this.gracePeriod=!1,this.readDOMTimeout=null};Zh.prototype.init=function(a){function b(a){if(!Fa(e,a)){if(e.somethingSelected())Sf({lineWise:!1,text:e.getSelections()}),"cut"==a.type&&e.replaceSelection("",null,"cut");else{if(!e.options.lineWiseCopyCut)return;var b=Wf(e);Sf({lineWise:!0,text:b.text}),"cut"==a.type&&e.operation(function(){e.setSelections(b.ranges,0,Ng),e.replaceSelection("",null,"cut")})}if(a.clipboardData){a.clipboardData.clearData();var c=Xh.text.join("\n");if(a.clipboardData.setData("Text",c),a.clipboardData.getData("Text")==c)return void a.preventDefault()}var g=Yf(),h=g.firstChild;e.display.lineSpace.insertBefore(g,e.display.lineSpace.firstChild),h.value=Xh.text.join("\n");var i=document.activeElement;Hg(h),setTimeout(function(){e.display.lineSpace.removeChild(g),i.focus(),i==f&&d.showPrimarySelection()},50)}}var c=this,d=this,e=d.cm,f=d.div=a.lineDiv;Xf(f,e.options.spellcheck),Yg(f,"paste",function(a){Fa(e,a)||Uf(a,e)||og<=11&&setTimeout(nd(e,function(){return c.updateFromDOM()}),20)}),Yg(f,"compositionstart",function(a){c.composing={data:a.data,done:!1}}),Yg(f,"compositionupdate",function(a){c.composing||(c.composing={data:a.data,done:!1})}),Yg(f,"compositionend",function(a){c.composing&&(a.data!=c.composing.data&&c.readFromDOMSoon(),c.composing.done=!0)}),Yg(f,"touchstart",function(){return d.forceCompositionEnd()}),Yg(f,"input",function(){c.composing||c.readFromDOMSoon()}),Yg(f,"copy",b),Yg(f,"cut",b)},Zh.prototype.prepareSelection=function(){var a=Bc(this.cm,!1);return a.focus=this.cm.state.focused,a},Zh.prototype.showSelection=function(a,b){a&&this.cm.display.view.length&&((a.focus||b)&&this.showPrimarySelection(),this.showMultipleSelections(a))},Zh.prototype.showPrimarySelection=function(){var a=window.getSelection(),b=this.cm,c=b.doc.sel.primary(),d=c.from(),e=c.to();if(b.display.viewTo==b.display.viewFrom||d.line>=b.display.viewTo||e.line=b.display.viewFrom&&_f(b,d)||{node:h[0].measure.map[2],offset:0},j=e.linea.firstLine()&&(d=J(d.line-1,B(a.doc,d.line-1).length)),e.ch==B(a.doc,e.line).text.length&&e.lineb.viewTo-1)return!1;var f,g,h;d.line==b.viewFrom||0==(f=zc(a,d.line))?(g=F(b.view[0].line),h=b.view[0].node):(g=F(b.view[f].line),h=b.view[f-1].node.nextSibling);var i,j,k=zc(a,e.line);if(k==b.view.length-1?(i=b.viewTo-1,j=b.lineDiv.lastChild):(i=F(b.view[k+1].line)-1,j=b.view[k+1].node.previousSibling),!h)return!1;for(var l=a.doc.splitLines(cg(a,h,j,g,i)),m=C(a.doc,J(g,0),J(i,B(a.doc,i).text.length));l.length>1&&m.length>1;)if(p(l)==p(m))l.pop(),m.pop(),i--;else{if(l[0]!=m[0])break;l.shift(),m.shift(),g++}for(var n=0,o=0,q=l[0],r=m[0],s=Math.min(q.length,r.length);nd.ch&&t.charCodeAt(t.length-o-1)==u.charCodeAt(u.length-o-1);)n--,o++;l[l.length-1]=t.slice(0,t.length-o).replace(/^\u200b+/,""),l[0]=l[0].slice(n).replace(/\u200b+$/,"");var w=J(g,n),x=J(i,m.length?p(m).length-o:0);return l.length>1||l[0]||K(w,x)?(Je(a.doc,l,w,x,"+input"),!0):void 0},Zh.prototype.ensurePolled=function(){this.forceCompositionEnd()},Zh.prototype.reset=function(){this.forceCompositionEnd()},Zh.prototype.forceCompositionEnd=function(){this.composing&&(clearTimeout(this.readDOMTimeout),this.composing=null,this.updateFromDOM(),this.div.blur(),this.div.focus())},Zh.prototype.readFromDOMSoon=function(){var a=this;null==this.readDOMTimeout&&(this.readDOMTimeout=setTimeout(function(){if(a.readDOMTimeout=null,a.composing){if(!a.composing.done)return;a.composing=null}a.updateFromDOM()},80))},Zh.prototype.updateFromDOM=function(){var a=this;!this.cm.isReadOnly()&&this.pollContent()||md(this.cm,function(){return qd(a.cm)})},Zh.prototype.setUneditable=function(a){a.contentEditable="false"},Zh.prototype.onKeyPress=function(a){0!=a.charCode&&(a.preventDefault(),this.cm.isReadOnly()||nd(this.cm,Tf)(this.cm,String.fromCharCode(null==a.charCode?a.keyCode:a.charCode),0))},Zh.prototype.readOnlyChanged=function(a){this.div.contentEditable=String("nocursor"!=a)},Zh.prototype.onContextMenu=function(){},Zh.prototype.resetPosition=function(){},Zh.prototype.needsContentAttribute=!0;var $h=function(a){this.cm=a,this.prevInput="",this.pollingFast=!1,this.polling=new Ig,this.hasSelection=!1,this.composing=null};$h.prototype.init=function(a){function b(a){if(!Fa(e,a)){if(e.somethingSelected())Sf({lineWise:!1,text:e.getSelections()});else{if(!e.options.lineWiseCopyCut)return;var b=Wf(e);Sf({lineWise:!0,text:b.text}),"cut"==a.type?e.setSelections(b.ranges,null,Ng):(d.prevInput="",g.value=b.text.join("\n"),Hg(g))}"cut"==a.type&&(e.state.cutIncoming=!0)}}var c=this,d=this,e=this.cm,f=this.wrapper=Yf(),g=this.textarea=f.firstChild;a.wrapper.insertBefore(f,a.wrapper.firstChild),wg&&(g.style.width="0px"),Yg(g,"input",function(){ng&&og>=9&&c.hasSelection&&(c.hasSelection=null),d.poll()}),Yg(g,"paste",function(a){Fa(e,a)||Uf(a,e)||(e.state.pasteIncoming=!0,d.fastPoll())}),Yg(g,"cut",b),Yg(g,"copy",b),Yg(a.scroller,"paste",function(b){Nb(a,b)||Fa(e,b)||(e.state.pasteIncoming=!0,d.focus())}),Yg(a.lineSpace,"selectstart",function(b){Nb(a,b)||Ja(b)}),Yg(g,"compositionstart",function(){var a=e.getCursor("from");d.composing&&d.composing.range.clear(),d.composing={start:a,range:e.markText(a,e.getCursor("to"),{className:"CodeMirror-composing"})}}),Yg(g,"compositionend",function(){d.composing&&(d.poll(),d.composing.range.clear(),d.composing=null)})},$h.prototype.prepareSelection=function(){var a=this.cm,b=a.display,c=a.doc,d=Bc(a);if(a.options.moveInputWithCursor){var e=lc(a,c.sel.primary().head,"div"),f=b.wrapper.getBoundingClientRect(),g=b.lineDiv.getBoundingClientRect();d.teTop=Math.max(0,Math.min(b.wrapper.clientHeight-10,e.top+g.top-f.top)),d.teLeft=Math.max(0,Math.min(b.wrapper.clientWidth-10,e.left+g.left-f.left))}return d},$h.prototype.showSelection=function(a){var b=this.cm,d=b.display;c(d.cursorDiv,a.cursors),c(d.selectionDiv,a.selection),null!=a.teTop&&(this.wrapper.style.top=a.teTop+"px",this.wrapper.style.left=a.teLeft+"px")},$h.prototype.reset=function(a){if(!this.contextMenuPending&&!this.composing){var b=this.cm;if(b.somethingSelected()){this.prevInput="";var c=b.getSelection();this.textarea.value=c,b.state.focused&&Hg(this.textarea),ng&&og>=9&&(this.hasSelection=c)}else a||(this.prevInput=this.textarea.value="",ng&&og>=9&&(this.hasSelection=null))}},$h.prototype.getField=function(){return this.textarea},$h.prototype.supportsTouch=function(){return!1},$h.prototype.focus=function(){if("nocursor"!=this.cm.options.readOnly&&(!yg||g()!=this.textarea))try{this.textarea.focus()}catch(a){}},$h.prototype.blur=function(){this.textarea.blur()},$h.prototype.resetPosition=function(){this.wrapper.style.top=this.wrapper.style.left=0},$h.prototype.receivedFocus=function(){this.slowPoll()},$h.prototype.slowPoll=function(){var a=this;this.pollingFast||this.polling.set(this.cm.options.pollInterval,function(){a.poll(),a.cm.state.focused&&a.slowPoll()})},$h.prototype.fastPoll=function(){function a(){var d=c.poll();d||b?(c.pollingFast=!1,c.slowPoll()):(b=!0,c.polling.set(60,a))}var b=!1,c=this;c.pollingFast=!0,c.polling.set(20,a)},$h.prototype.poll=function(){var a=this,b=this.cm,c=this.textarea,d=this.prevInput;if(this.contextMenuPending||!b.state.focused||_g(c)&&!d&&!this.composing||b.isReadOnly()||b.options.disableInput||b.state.keySeq)return!1;var e=c.value;if(e==d&&!b.somethingSelected())return!1;if(ng&&og>=9&&this.hasSelection===e||zg&&/[\uf700-\uf7ff]/.test(e))return b.display.input.reset(),!1;if(b.doc.sel==b.display.selForContextMenu){var f=e.charCodeAt(0);if(8203!=f||d||(d="\u200b"),8666==f)return this.reset(),this.cm.execCommand("undo")}for(var g=0,h=Math.min(d.length,e.length);g1e3||e.indexOf("\n")>-1?c.value=a.prevInput="":a.prevInput=e,a.composing&&(a.composing.range.clear(),a.composing.range=b.markText(a.composing.start,b.getCursor("to"),{className:"CodeMirror-composing"}))}),!0},$h.prototype.ensurePolled=function(){this.pollingFast&&this.poll()&&(this.pollingFast=!1)},$h.prototype.onKeyPress=function(){ng&&og>=9&&(this.hasSelection=null),this.fastPoll()},$h.prototype.onContextMenu=function(a){function b(){if(null!=g.selectionStart){var a=e.somethingSelected(),b="\u200b"+(a?g.value:"");g.value="\u21da",g.value=b,d.prevInput=a?"":"\u200b",g.selectionStart=1,g.selectionEnd=b.length,f.selForContextMenu=e.doc.sel}}function c(){if(d.contextMenuPending=!1,d.wrapper.style.cssText=l,g.style.cssText=k,ng&&og<9&&f.scrollbars.setScrollTop(f.scroller.scrollTop=i),null!=g.selectionStart){(!ng||ng&&og<9)&&b();var a=0,c=function(){f.selForContextMenu==e.doc.sel&&0==g.selectionStart&&g.selectionEnd>0&&"\u200b"==d.prevInput?nd(e,Be)(e):a++<10?f.detectingSelectAll=setTimeout(c,500):(f.selForContextMenu=null,f.input.reset())};f.detectingSelectAll=setTimeout(c,200)}}var d=this,e=d.cm,f=e.display,g=d.textarea,h=yc(e,a),i=f.scroller.scrollTop;if(h&&!sg){var j=e.options.resetSelectionOnContextMenu;j&&e.doc.sel.contains(h)==-1&&nd(e,te)(e.doc,Nd(h),Ng);var k=g.style.cssText,l=d.wrapper.style.cssText;d.wrapper.style.cssText="position: absolute";var m=d.wrapper.getBoundingClientRect();g.style.cssText="position: absolute; width: 30px; height: 30px;\n top: "+(a.clientY-m.top-5)+"px; left: "+(a.clientX-m.left-5)+"px;\n z-index: 1000; background: "+(ng?"rgba(255, 255, 255, .05)":"transparent")+";\n outline: none; border-width: 0; outline: none; overflow: hidden; opacity: .05; filter: alpha(opacity=5);";var n;if(pg&&(n=window.scrollY),f.input.focus(),pg&&window.scrollTo(null,n),f.input.reset(),e.somethingSelected()||(g.value=d.prevInput=" "),d.contextMenuPending=!0,f.selForContextMenu=e.doc.sel,clearTimeout(f.detectingSelectAll),ng&&og>=9&&b(),Fg){Ma(a);var o=function(){Da(window,"mouseup",o),setTimeout(c,20)};Yg(window,"mouseup",o)}else setTimeout(c,50)}},$h.prototype.readOnlyChanged=function(a){a||this.reset(),this.textarea.disabled="nocursor"==a},$h.prototype.setUneditable=function(){},$h.prototype.needsContentAttribute=!1,Lf(Pf),Yh(Pf);var _h="iter insert remove copy getEditor constructor".split(" ");for(var ai in Eh.prototype)Eh.prototype.hasOwnProperty(ai)&&m(_h,ai)<0&&(Pf.prototype[ai]=function(a){return function(){return a.apply(this.doc,arguments)}}(Eh.prototype[ai]));return Ia(Eh),Pf.inputStyles={textarea:$h,contenteditable:Zh},Pf.defineMode=function(a){Pf.defaults.mode||"null"==a||(Pf.defaults.mode=a),Sa.apply(this,arguments)},Pf.defineMIME=Ta,Pf.defineMode("null",function(){return{token:function(a){return a.skipToEnd()}}}),Pf.defineMIME("text/plain","null"),Pf.defineExtension=function(a,b){Pf.prototype[a]=b},Pf.defineDocExtension=function(a,b){Eh.prototype[a]=b},Pf.fromTextArea=fg,gg(Pf),Pf.version="5.29.1",Pf})},{}],60:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c,d,e,f){this.indented=a,this.column=b,this.type=c,this.info=d,this.align=e,this.prev=f}function c(a,c,d,e){var f=a.indented;return a.context&&"statement"==a.context.type&&"statement"!=d&&(f=a.context.indented),a.context=new b(f,c,d,e,null,a.context)}function d(a){var b=a.context.type;return")"!=b&&"]"!=b&&"}"!=b||(a.indented=a.context.indented),a.context=a.context.prev}function e(a,b,c){return"variable"==b.prevToken||"type"==b.prevToken||(!!/\S(?:[^- ]>|[*\]])\s*$|\*$/.test(a.string.slice(0,c))||(!(!b.typeAtEndOfLine||a.column()!=a.indentation())||void 0))}function f(a){for(;;){if(!a||"top"==a.type)return!0;if("}"==a.type&&"namespace"!=a.prev.info)return!1;a=a.prev}}function g(a){for(var b={},c=a.split(" "),d=0;d!?|\/]/,H=i.isIdentifierChar||/[\w\$_\xa1-\uffff]/;return{startState:function(a){return{tokenize:null,context:new b((a||0)-p,0,"top",null,!1),indented:0, -startOfLine:!0,prevToken:null}},token:function(a,b){var g=b.context;if(a.sol()&&(null==g.align&&(g.align=!1),b.indented=a.indentation(),b.startOfLine=!0),a.eatSpace())return m(a,b),null;n=o=null;var h=(b.tokenize||j)(a,b);if("comment"==h||"meta"==h)return h;if(null==g.align&&(g.align=!0),";"==n||":"==n||","==n&&a.match(/^\s*(?:\/\/.*)?$/,!1))for(;"statement"==b.context.type;)d(b);else if("{"==n)c(b,a.column(),"}");else if("["==n)c(b,a.column(),"]");else if("("==n)c(b,a.column(),")");else if("}"==n){for(;"statement"==g.type;)g=d(b);for("}"==g.type&&(g=d(b));"statement"==g.type;)g=d(b)}else n==g.type?d(b):A&&(("}"==g.type||"top"==g.type)&&";"!=n||"statement"==g.type&&"newstatement"==n)&&c(b,a.column(),"statement",a.current());if("variable"==h&&("def"==b.prevToken||i.typeFirstDefinitions&&e(a,b,a.start)&&f(b.context)&&a.match(/^\s*\(/,!1))&&(h="def"),y.token){var k=y.token(a,b,h);void 0!==k&&(h=k)}return"def"==h&&i.styleDefs===!1&&(h="variable"),b.startOfLine=!1,b.prevToken=o?"def":h||n,m(a,b),h},indent:function(b,c){if(b.tokenize!=j&&null!=b.tokenize||b.typeAtEndOfLine)return a.Pass;var d=b.context,e=c&&c.charAt(0);if("statement"==d.type&&"}"==e&&(d=d.prev),i.dontIndentStatements)for(;"statement"==d.type&&i.dontIndentStatements.test(d.info);)d=d.prev;if(y.indent){var f=y.indent(b,d,c);if("number"==typeof f)return f}var g=e==d.type,h=d.prev&&"switch"==d.prev.info;if(i.allmanIndentation&&/[{(]/.test(e)){for(;"top"!=d.type&&"}"!=d.type;)d=d.prev;return d.indented}return"statement"==d.type?d.indented+("{"==e?0:q):!d.align||r&&")"==d.type?")"!=d.type||g?d.indented+(g?0:p)+(g||!h||/^(?:case|default)\b/.test(c)?0:p):d.indented+q:d.column+(g?0:1)},electricInput:B?/^\s*(?:case .*?:|default:|\{\}?|\})$/:/^\s*[{}]$/,blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//",fold:"brace"}});var t="auto if break case register continue return default do sizeof static else struct switch extern typedef union for goto while enum const volatile",u="int long char short double float unsigned signed void size_t ptrdiff_t";p(["text/x-csrc","text/x-c","text/x-chdr"],{name:"clike",keywords:g(t),types:g(u+" bool _Complex _Bool float_t double_t intptr_t intmax_t int8_t int16_t int32_t int64_t uintptr_t uintmax_t uint8_t uint16_t uint32_t uint64_t"),blockKeywords:g("case do else for if switch while struct"),defKeywords:g("struct"),typeFirstDefinitions:!0,atoms:g("null true false"),hooks:{"#":i,"*":j},modeProps:{fold:["brace","include"]}}),p(["text/x-c++src","text/x-c++hdr"],{name:"clike",keywords:g(t+" asm dynamic_cast namespace reinterpret_cast try explicit new static_cast typeid catch operator template typename class friend private this using const_cast inline public throw virtual delete mutable protected alignas alignof constexpr decltype nullptr noexcept thread_local final static_assert override"),types:g(u+" bool wchar_t"),blockKeywords:g("catch class do else finally for if struct switch try while"),defKeywords:g("class namespace struct enum union"),typeFirstDefinitions:!0,atoms:g("true false null"),dontIndentStatements:/^template$/,isIdentifierChar:/[\w\$_~\xa1-\uffff]/,hooks:{"#":i,"*":j,u:l,U:l,L:l,R:l,0:k,1:k,2:k,3:k,4:k,5:k,6:k,7:k,8:k,9:k,token:function(a,b,c){if("variable"==c&&"("==a.peek()&&(";"==b.prevToken||null==b.prevToken||"}"==b.prevToken)&&m(a.current()))return"def"}},namespaceSeparator:"::",modeProps:{fold:["brace","include"]}}),p("text/x-java",{name:"clike",keywords:g("abstract assert break case catch class const continue default do else enum extends final finally float for goto if implements import instanceof interface native new package private protected public return static strictfp super switch synchronized this throw throws transient try volatile while @interface"),types:g("byte short int long float double boolean char void Boolean Byte Character Double Float Integer Long Number Object Short String StringBuffer StringBuilder Void"),blockKeywords:g("catch class do else finally for if switch try while"),defKeywords:g("class interface package enum @interface"),typeFirstDefinitions:!0,atoms:g("true false null"),number:/^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,hooks:{"@":function(a){return!a.match("interface",!1)&&(a.eatWhile(/[\w\$_]/),"meta")}},modeProps:{fold:["brace","import"]}}),p("text/x-csharp",{name:"clike",keywords:g("abstract as async await base break case catch checked class const continue default delegate do else enum event explicit extern finally fixed for foreach goto if implicit in interface internal is lock namespace new operator out override params private protected public readonly ref return sealed sizeof stackalloc static struct switch this throw try typeof unchecked unsafe using virtual void volatile while add alias ascending descending dynamic from get global group into join let orderby partial remove select set value var yield"),types:g("Action Boolean Byte Char DateTime DateTimeOffset Decimal Double Func Guid Int16 Int32 Int64 Object SByte Single String Task TimeSpan UInt16 UInt32 UInt64 bool byte char decimal double short int long object sbyte float string ushort uint ulong"),blockKeywords:g("catch class do else finally for foreach if struct switch try while"),defKeywords:g("class interface namespace struct var"),typeFirstDefinitions:!0,atoms:g("true false null"),hooks:{"@":function(a,b){return a.eat('"')?(b.tokenize=n,n(a,b)):(a.eatWhile(/[\w\$_]/),"meta")}}}),p("text/x-scala",{name:"clike",keywords:g("abstract case catch class def do else extends final finally for forSome if implicit import lazy match new null object override package private protected return sealed super this throw trait try type val var while with yield _ assert assume require print println printf readLine readBoolean readByte readShort readChar readInt readLong readFloat readDouble"),types:g("AnyVal App Application Array BufferedIterator BigDecimal BigInt Char Console Either Enumeration Equiv Error Exception Fractional Function IndexedSeq Int Integral Iterable Iterator List Map Numeric Nil NotNull Option Ordered Ordering PartialFunction PartialOrdering Product Proxy Range Responder Seq Serializable Set Specializable Stream StringBuilder StringContext Symbol Throwable Traversable TraversableOnce Tuple Unit Vector Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable Compiler Double Exception Float Integer Long Math Number Object Package Pair Process Runtime Runnable SecurityManager Short StackTraceElement StrictMath String StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"),multiLineStrings:!0,blockKeywords:g("catch class enum do else finally for forSome if match switch try while"),defKeywords:g("class enum def object package trait type val var"),atoms:g("true false null"),indentStatements:!1,indentSwitch:!1,isOperatorChar:/[+\-*&%=<>!?|\/#:@]/,hooks:{"@":function(a){return a.eatWhile(/[\w\$_]/),"meta"},'"':function(a,b){return!!a.match('""')&&(b.tokenize=q,b.tokenize(a,b))},"'":function(a){return a.eatWhile(/[\w\$_\xa1-\uffff]/),"atom"},"=":function(a,c){var d=c.context;return!("}"!=d.type||!d.align||!a.eat(">"))&&(c.context=new b(d.indented,d.column,d.type,d.info,null,d.prev),"operator")}},modeProps:{closeBrackets:{triples:'"'}}}),p("text/x-kotlin",{name:"clike",keywords:g("package as typealias class interface this super val var fun for is in This throw return break continue object if else while do try when !in !is as? file import where by get set abstract enum open inner override private public internal protected catch finally out final vararg reified dynamic companion constructor init sealed field property receiver param sparam lateinit data inline noinline tailrec external annotation crossinline const operator infix suspend"),types:g("Boolean Byte Character CharSequence Class ClassLoader Cloneable Comparable Compiler Double Exception Float Integer Long Math Number Object Package Pair Process Runtime Runnable SecurityManager Short StackTraceElement StrictMath String StringBuffer System Thread ThreadGroup ThreadLocal Throwable Triple Void"),intendSwitch:!1,indentStatements:!1,multiLineStrings:!0,number:/^(?:0x[a-f\d_]+|0b[01_]+|(?:[\d_]+\.?\d*|\.\d+)(?:e[-+]?[\d_]+)?)(u|ll?|l|f)?/i,blockKeywords:g("catch class do else finally for if where try while enum"),defKeywords:g("class val var object package interface fun"),atoms:g("true false null this"),hooks:{'"':function(a,b){return b.tokenize=r(a.match('""')),b.tokenize(a,b)}},modeProps:{closeBrackets:{triples:'"'}}}),p(["x-shader/x-vertex","x-shader/x-fragment"],{name:"clike",keywords:g("sampler1D sampler2D sampler3D samplerCube sampler1DShadow sampler2DShadow const attribute uniform varying break continue discard return for while do if else struct in out inout"),types:g("float int bool void vec2 vec3 vec4 ivec2 ivec3 ivec4 bvec2 bvec3 bvec4 mat2 mat3 mat4"),blockKeywords:g("for while do if else struct"),builtin:g("radians degrees sin cos tan asin acos atan pow exp log exp2 sqrt inversesqrt abs sign floor ceil fract mod min max clamp mix step smoothstep length distance dot cross normalize ftransform faceforward reflect refract matrixCompMult lessThan lessThanEqual greaterThan greaterThanEqual equal notEqual any all not texture1D texture1DProj texture1DLod texture1DProjLod texture2D texture2DProj texture2DLod texture2DProjLod texture3D texture3DProj texture3DLod texture3DProjLod textureCube textureCubeLod shadow1D shadow2D shadow1DProj shadow2DProj shadow1DLod shadow2DLod shadow1DProjLod shadow2DProjLod dFdx dFdy fwidth noise1 noise2 noise3 noise4"),atoms:g("true false gl_FragColor gl_SecondaryColor gl_Normal gl_Vertex gl_MultiTexCoord0 gl_MultiTexCoord1 gl_MultiTexCoord2 gl_MultiTexCoord3 gl_MultiTexCoord4 gl_MultiTexCoord5 gl_MultiTexCoord6 gl_MultiTexCoord7 gl_FogCoord gl_PointCoord gl_Position gl_PointSize gl_ClipVertex gl_FrontColor gl_BackColor gl_FrontSecondaryColor gl_BackSecondaryColor gl_TexCoord gl_FogFragCoord gl_FragCoord gl_FrontFacing gl_FragData gl_FragDepth gl_ModelViewMatrix gl_ProjectionMatrix gl_ModelViewProjectionMatrix gl_TextureMatrix gl_NormalMatrix gl_ModelViewMatrixInverse gl_ProjectionMatrixInverse gl_ModelViewProjectionMatrixInverse gl_TexureMatrixTranspose gl_ModelViewMatrixInverseTranspose gl_ProjectionMatrixInverseTranspose gl_ModelViewProjectionMatrixInverseTranspose gl_TextureMatrixInverseTranspose gl_NormalScale gl_DepthRange gl_ClipPlane gl_Point gl_FrontMaterial gl_BackMaterial gl_LightSource gl_LightModel gl_FrontLightModelProduct gl_BackLightModelProduct gl_TextureColor gl_EyePlaneS gl_EyePlaneT gl_EyePlaneR gl_EyePlaneQ gl_FogParameters gl_MaxLights gl_MaxClipPlanes gl_MaxTextureUnits gl_MaxTextureCoords gl_MaxVertexAttribs gl_MaxVertexUniformComponents gl_MaxVaryingFloats gl_MaxVertexTextureImageUnits gl_MaxTextureImageUnits gl_MaxFragmentUniformComponents gl_MaxCombineTextureImageUnits gl_MaxDrawBuffers"),indentSwitch:!1,hooks:{"#":i},modeProps:{fold:["brace","include"]}}),p("text/x-nesc",{name:"clike",keywords:g(t+"as atomic async call command component components configuration event generic implementation includes interface module new norace nx_struct nx_union post provides signal task uses abstract extends"),types:g(u),blockKeywords:g("case do else for if switch while struct"),atoms:g("null true false"),hooks:{"#":i},modeProps:{fold:["brace","include"]}}),p("text/x-objectivec",{name:"clike",keywords:g(t+"inline restrict _Bool _Complex _Imaginary BOOL Class bycopy byref id IMP in inout nil oneway out Protocol SEL self super atomic nonatomic retain copy readwrite readonly"),types:g(u),atoms:g("YES NO NULL NILL ON OFF true false"),hooks:{"@":function(a){return a.eatWhile(/[\w\$]/),"keyword"},"#":i,indent:function(a,b,c){if("statement"==b.type&&/^@\w/.test(c))return b.indented}},modeProps:{fold:"brace"}}),p("text/x-squirrel",{name:"clike",keywords:g("base break clone continue const default delete enum extends function in class foreach local resume return this throw typeof yield constructor instanceof static"),types:g(u),blockKeywords:g("case catch class else for foreach if switch try while"),defKeywords:g("function local class"),typeFirstDefinitions:!0,atoms:g("true false null"),hooks:{"#":i},modeProps:{fold:["brace","include"]}});var v=null;p("text/x-ceylon",{name:"clike",keywords:g("abstracts alias assembly assert assign break case catch class continue dynamic else exists extends finally for function given if import in interface is let module new nonempty object of out outer package return satisfies super switch then this throw try value void while"),types:function(a){var b=a.charAt(0);return b===b.toUpperCase()&&b!==b.toLowerCase()},blockKeywords:g("case catch class dynamic else finally for function if interface module new object switch try while"),defKeywords:g("class dynamic function interface module object package value"),builtin:g("abstract actual aliased annotation by default deprecated doc final formal late license native optional sealed see serializable shared suppressWarnings tagged throws variable"),isPunctuationChar:/[\[\]{}\(\),;\:\.`]/,isOperatorChar:/[+\-*&%=<>!?|^~:\/]/,numberStart:/[\d#$]/,number:/^(?:#[\da-fA-F_]+|\$[01_]+|[\d_]+[kMGTPmunpf]?|[\d_]+\.[\d_]+(?:[eE][-+]?\d+|[kMGTPmunpf]|)|)/i,multiLineStrings:!0,typeFirstDefinitions:!0,atoms:g("true false null larger smaller equal empty finished"),indentSwitch:!1,styleDefs:!1,hooks:{"@":function(a){return a.eatWhile(/[\w\$_]/),"meta"},'"':function(a,b){return b.tokenize=s(a.match('""')?"triple":"single"),b.tokenize(a,b)},"`":function(a,b){return!(!v||!a.match("`"))&&(b.tokenize=v,v=null,b.tokenize(a,b))},"'":function(a){return a.eatWhile(/[\w\$_\xa1-\uffff]/),"atom"},token:function(a,b,c){if(("variable"==c||"type"==c)&&"."==b.prevToken)return"variable-2"}},modeProps:{fold:["brace","import"],closeBrackets:{triples:'"'}}})})},{"../../lib/codemirror":59}],61:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";function b(a){for(var b={},c=0;c*\/]/.test(c)?d(null,"select-op"):"."==c&&a.match(/^-?[_a-z][_a-z0-9-]*/i)?d("qualifier","qualifier"):/[:;{}\[\]\(\)]/.test(c)?d(null,c):"u"==c&&a.match(/rl(-prefix)?\(/)||"d"==c&&a.match("omain(")||"r"==c&&a.match("egexp(")?(a.backUp(1),b.tokenize=g,d("property","word")):/[\w\\\-]/.test(c)?(a.eatWhile(/[\w\\\-]/),d("property","word")):d(null,null):/[\d.]/.test(a.peek())?(a.eatWhile(/[\w.%]/),d("number","unit")):a.match(/^-[\w\\\-]+/)?(a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?d("variable-2","variable-definition"):d("variable-2","variable")):a.match(/^\w+-/)?d("meta","meta"):void 0}function f(a){return function(b,c){for(var e,f=!1;null!=(e=b.next());){if(e==a&&!f){")"==a&&b.backUp(1);break}f=!f&&"\\"==e}return(e==a||!f&&")"!=a)&&(c.tokenize=null),d("string","string")}}function g(a,b){return a.next(),a.match(/\s*[\"\')]/,!1)?b.tokenize=null:b.tokenize=f(")"),d(null,"(")}function h(a,b,c){this.type=a,this.indent=b,this.prev=c}function i(a,b,c,d){return a.context=new h(c,b.indentation()+(d===!1?0:q),a.context),c}function j(a){return a.context.prev&&(a.context=a.context.prev),a.context.type}function k(a,b,c){return F[c.context.type](a,b,c)}function l(a,b,c,d){for(var e=d||1;e>0;e--)c.context=c.context.prev;return k(a,b,c)}function m(a){var b=a.current().toLowerCase();p=B.hasOwnProperty(b)?"atom":A.hasOwnProperty(b)?"keyword":"variable"}var n=c.inline;c.propertyKeywords||(c=a.resolveMode("text/css"));var o,p,q=b.indentUnit,r=c.tokenHooks,s=c.documentTypes||{},t=c.mediaTypes||{},u=c.mediaFeatures||{},v=c.mediaValueKeywords||{},w=c.propertyKeywords||{},x=c.nonStandardPropertyKeywords||{},y=c.fontProperties||{},z=c.counterDescriptors||{},A=c.colorKeywords||{},B=c.valueKeywords||{},C=c.allowNested,D=c.lineComment,E=c.supportsAtComponent===!0,F={};return F.top=function(a,b,c){if("{"==a)return i(c,b,"block");if("}"==a&&c.context.prev)return j(c);if(E&&/@component/.test(a))return i(c,b,"atComponentBlock");if(/^@(-moz-)?document$/.test(a))return i(c,b,"documentTypes");if(/^@(media|supports|(-moz-)?document|import)$/.test(a))return i(c,b,"atBlock");if(/^@(font-face|counter-style)/.test(a))return c.stateArg=a,"restricted_atBlock_before";if(/^@(-(moz|ms|o|webkit)-)?keyframes$/.test(a))return"keyframes";if(a&&"@"==a.charAt(0))return i(c,b,"at");if("hash"==a)p="builtin";else if("word"==a)p="tag";else{if("variable-definition"==a)return"maybeprop";if("interpolation"==a)return i(c,b,"interpolation");if(":"==a)return"pseudo";if(C&&"("==a)return i(c,b,"parens")}return c.context.type},F.block=function(a,b,c){if("word"==a){var d=b.current().toLowerCase();return w.hasOwnProperty(d)?(p="property","maybeprop"):x.hasOwnProperty(d)?(p="string-2","maybeprop"):C?(p=b.match(/^\s*:(?:\s|$)/,!1)?"property":"tag","block"):(p+=" error","maybeprop")}return"meta"==a?"block":C||"hash"!=a&&"qualifier"!=a?F.top(a,b,c):(p="error","block")},F.maybeprop=function(a,b,c){return":"==a?i(c,b,"prop"):k(a,b,c)},F.prop=function(a,b,c){if(";"==a)return j(c);if("{"==a&&C)return i(c,b,"propBlock");if("}"==a||"{"==a)return l(a,b,c);if("("==a)return i(c,b,"parens");if("hash"!=a||/^#([0-9a-fA-f]{3,4}|[0-9a-fA-f]{6}|[0-9a-fA-f]{8})$/.test(b.current())){if("word"==a)m(b);else if("interpolation"==a)return i(c,b,"interpolation")}else p+=" error";return"prop"},F.propBlock=function(a,b,c){return"}"==a?j(c):"word"==a?(p="property","maybeprop"):c.context.type},F.parens=function(a,b,c){return"{"==a||"}"==a?l(a,b,c):")"==a?j(c):"("==a?i(c,b,"parens"):"interpolation"==a?i(c,b,"interpolation"):("word"==a&&m(b),"parens")},F.pseudo=function(a,b,c){return"meta"==a?"pseudo":"word"==a?(p="variable-3",c.context.type):k(a,b,c)},F.documentTypes=function(a,b,c){return"word"==a&&s.hasOwnProperty(b.current())?(p="tag",c.context.type):F.atBlock(a,b,c)},F.atBlock=function(a,b,c){if("("==a)return i(c,b,"atBlock_parens");if("}"==a||";"==a)return l(a,b,c);if("{"==a)return j(c)&&i(c,b,C?"block":"top");if("interpolation"==a)return i(c,b,"interpolation");if("word"==a){var d=b.current().toLowerCase();p="only"==d||"not"==d||"and"==d||"or"==d?"keyword":t.hasOwnProperty(d)?"attribute":u.hasOwnProperty(d)?"property":v.hasOwnProperty(d)?"keyword":w.hasOwnProperty(d)?"property":x.hasOwnProperty(d)?"string-2":B.hasOwnProperty(d)?"atom":A.hasOwnProperty(d)?"keyword":"error"}return c.context.type},F.atComponentBlock=function(a,b,c){return"}"==a?l(a,b,c):"{"==a?j(c)&&i(c,b,C?"block":"top",!1):("word"==a&&(p="error"),c.context.type)},F.atBlock_parens=function(a,b,c){return")"==a?j(c):"{"==a||"}"==a?l(a,b,c,2):F.atBlock(a,b,c)},F.restricted_atBlock_before=function(a,b,c){return"{"==a?i(c,b,"restricted_atBlock"):"word"==a&&"@counter-style"==c.stateArg?(p="variable","restricted_atBlock_before"):k(a,b,c)},F.restricted_atBlock=function(a,b,c){return"}"==a?(c.stateArg=null,j(c)):"word"==a?(p="@font-face"==c.stateArg&&!y.hasOwnProperty(b.current().toLowerCase())||"@counter-style"==c.stateArg&&!z.hasOwnProperty(b.current().toLowerCase())?"error":"property","maybeprop"):"restricted_atBlock"},F.keyframes=function(a,b,c){return"word"==a?(p="variable","keyframes"):"{"==a?i(c,b,"top"):k(a,b,c)},F.at=function(a,b,c){return";"==a?j(c):"{"==a||"}"==a?l(a,b,c):("word"==a?p="tag":"hash"==a&&(p="builtin"),"at")},F.interpolation=function(a,b,c){return"}"==a?j(c):"{"==a||";"==a?l(a,b,c):("word"==a?p="variable":"variable"!=a&&"("!=a&&")"!=a&&(p="error"),"interpolation")},{startState:function(a){return{tokenize:null,state:n?"block":"top",stateArg:null,context:new h(n?"block":"top",a||0,null)}},token:function(a,b){if(!b.tokenize&&a.eatSpace())return null;var c=(b.tokenize||e)(a,b);return c&&"object"==typeof c&&(o=c[1],c=c[0]),p=c,"comment"!=o&&(b.state=F[b.state](o,a,b)),p},indent:function(a,b){var c=a.context,d=b&&b.charAt(0),e=c.indent;return"prop"!=c.type||"}"!=d&&")"!=d||(c=c.prev),c.prev&&("}"!=d||"block"!=c.type&&"top"!=c.type&&"interpolation"!=c.type&&"restricted_atBlock"!=c.type?(")"!=d||"parens"!=c.type&&"atBlock_parens"!=c.type)&&("{"!=d||"at"!=c.type&&"atBlock"!=c.type)||(e=Math.max(0,c.indent-q)):(c=c.prev,e=c.indent)),e},electricChars:"}",blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:D,fold:"brace"}});var d=["domain","regexp","url","url-prefix"],e=b(d),f=["all","aural","braille","handheld","print","projection","screen","tty","tv","embossed"],g=b(f),h=["width","min-width","max-width","height","min-height","max-height","device-width","min-device-width","max-device-width","device-height","min-device-height","max-device-height","aspect-ratio","min-aspect-ratio","max-aspect-ratio","device-aspect-ratio","min-device-aspect-ratio","max-device-aspect-ratio","color","min-color","max-color","color-index","min-color-index","max-color-index","monochrome","min-monochrome","max-monochrome","resolution","min-resolution","max-resolution","scan","grid","orientation","device-pixel-ratio","min-device-pixel-ratio","max-device-pixel-ratio","pointer","any-pointer","hover","any-hover"],i=b(h),j=["landscape","portrait","none","coarse","fine","on-demand","hover","interlace","progressive"],k=b(j),l=["align-content","align-items","align-self","alignment-adjust","alignment-baseline","anchor-point","animation","animation-delay","animation-direction","animation-duration","animation-fill-mode","animation-iteration-count","animation-name","animation-play-state","animation-timing-function","appearance","azimuth","backface-visibility","background","background-attachment","background-blend-mode","background-clip","background-color","background-image","background-origin","background-position","background-repeat","background-size","baseline-shift","binding","bleed","bookmark-label","bookmark-level","bookmark-state","bookmark-target","border","border-bottom","border-bottom-color","border-bottom-left-radius","border-bottom-right-radius","border-bottom-style","border-bottom-width","border-collapse","border-color","border-image","border-image-outset","border-image-repeat","border-image-slice","border-image-source","border-image-width","border-left","border-left-color","border-left-style","border-left-width","border-radius","border-right","border-right-color","border-right-style","border-right-width","border-spacing","border-style","border-top","border-top-color","border-top-left-radius","border-top-right-radius","border-top-style","border-top-width","border-width","bottom","box-decoration-break","box-shadow","box-sizing","break-after","break-before","break-inside","caption-side","caret-color","clear","clip","color","color-profile","column-count","column-fill","column-gap","column-rule","column-rule-color","column-rule-style","column-rule-width","column-span","column-width","columns","content","counter-increment","counter-reset","crop","cue","cue-after","cue-before","cursor","direction","display","dominant-baseline","drop-initial-after-adjust","drop-initial-after-align","drop-initial-before-adjust","drop-initial-before-align","drop-initial-size","drop-initial-value","elevation","empty-cells","fit","fit-position","flex","flex-basis","flex-direction","flex-flow","flex-grow","flex-shrink","flex-wrap","float","float-offset","flow-from","flow-into","font","font-feature-settings","font-family","font-kerning","font-language-override","font-size","font-size-adjust","font-stretch","font-style","font-synthesis","font-variant","font-variant-alternates","font-variant-caps","font-variant-east-asian","font-variant-ligatures","font-variant-numeric","font-variant-position","font-weight","grid","grid-area","grid-auto-columns","grid-auto-flow","grid-auto-rows","grid-column","grid-column-end","grid-column-gap","grid-column-start","grid-gap","grid-row","grid-row-end","grid-row-gap","grid-row-start","grid-template","grid-template-areas","grid-template-columns","grid-template-rows","hanging-punctuation","height","hyphens","icon","image-orientation","image-rendering","image-resolution","inline-box-align","justify-content","justify-items","justify-self","left","letter-spacing","line-break","line-height","line-stacking","line-stacking-ruby","line-stacking-shift","line-stacking-strategy","list-style","list-style-image","list-style-position","list-style-type","margin","margin-bottom","margin-left","margin-right","margin-top","marks","marquee-direction","marquee-loop","marquee-play-count","marquee-speed","marquee-style","max-height","max-width","min-height","min-width","move-to","nav-down","nav-index","nav-left","nav-right","nav-up","object-fit","object-position","opacity","order","orphans","outline","outline-color","outline-offset","outline-style","outline-width","overflow","overflow-style","overflow-wrap","overflow-x","overflow-y","padding","padding-bottom","padding-left","padding-right","padding-top","page","page-break-after","page-break-before","page-break-inside","page-policy","pause","pause-after","pause-before","perspective","perspective-origin","pitch","pitch-range","place-content","place-items","place-self","play-during","position","presentation-level","punctuation-trim","quotes","region-break-after","region-break-before","region-break-inside","region-fragment","rendering-intent","resize","rest","rest-after","rest-before","richness","right","rotation","rotation-point","ruby-align","ruby-overhang","ruby-position","ruby-span","shape-image-threshold","shape-inside","shape-margin","shape-outside","size","speak","speak-as","speak-header","speak-numeral","speak-punctuation","speech-rate","stress","string-set","tab-size","table-layout","target","target-name","target-new","target-position","text-align","text-align-last","text-decoration","text-decoration-color","text-decoration-line","text-decoration-skip","text-decoration-style","text-emphasis","text-emphasis-color","text-emphasis-position","text-emphasis-style","text-height","text-indent","text-justify","text-outline","text-overflow","text-shadow","text-size-adjust","text-space-collapse","text-transform","text-underline-position","text-wrap","top","transform","transform-origin","transform-style","transition","transition-delay","transition-duration","transition-property","transition-timing-function","unicode-bidi","user-select","vertical-align","visibility","voice-balance","voice-duration","voice-family","voice-pitch","voice-range","voice-rate","voice-stress","voice-volume","volume","white-space","widows","width","will-change","word-break","word-spacing","word-wrap","z-index","clip-path","clip-rule","mask","enable-background","filter","flood-color","flood-opacity","lighting-color","stop-color","stop-opacity","pointer-events","color-interpolation","color-interpolation-filters","color-rendering","fill","fill-opacity","fill-rule","image-rendering","marker","marker-end","marker-mid","marker-start","shape-rendering","stroke","stroke-dasharray","stroke-dashoffset","stroke-linecap","stroke-linejoin","stroke-miterlimit","stroke-opacity","stroke-width","text-rendering","baseline-shift","dominant-baseline","glyph-orientation-horizontal","glyph-orientation-vertical","text-anchor","writing-mode"],m=b(l),n=["scrollbar-arrow-color","scrollbar-base-color","scrollbar-dark-shadow-color","scrollbar-face-color","scrollbar-highlight-color","scrollbar-shadow-color","scrollbar-3d-light-color","scrollbar-track-color","shape-inside","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","zoom"],o=b(n),p=["font-family","src","unicode-range","font-variant","font-feature-settings","font-stretch","font-weight","font-style"],q=b(p),r=["additive-symbols","fallback","negative","pad","prefix","range","speak-as","suffix","symbols","system"],s=b(r),t=["aliceblue","antiquewhite","aqua","aquamarine","azure","beige","bisque","black","blanchedalmond","blue","blueviolet","brown","burlywood","cadetblue","chartreuse","chocolate","coral","cornflowerblue","cornsilk","crimson","cyan","darkblue","darkcyan","darkgoldenrod","darkgray","darkgreen","darkkhaki","darkmagenta","darkolivegreen","darkorange","darkorchid","darkred","darksalmon","darkseagreen","darkslateblue","darkslategray","darkturquoise","darkviolet","deeppink","deepskyblue","dimgray","dodgerblue","firebrick","floralwhite","forestgreen","fuchsia","gainsboro","ghostwhite","gold","goldenrod","gray","grey","green","greenyellow","honeydew","hotpink","indianred","indigo","ivory","khaki","lavender","lavenderblush","lawngreen","lemonchiffon","lightblue","lightcoral","lightcyan","lightgoldenrodyellow","lightgray","lightgreen","lightpink","lightsalmon","lightseagreen","lightskyblue","lightslategray","lightsteelblue","lightyellow","lime","limegreen","linen","magenta","maroon","mediumaquamarine","mediumblue","mediumorchid","mediumpurple","mediumseagreen","mediumslateblue","mediumspringgreen","mediumturquoise","mediumvioletred","midnightblue","mintcream","mistyrose","moccasin","navajowhite","navy","oldlace","olive","olivedrab","orange","orangered","orchid","palegoldenrod","palegreen","paleturquoise","palevioletred","papayawhip","peachpuff","peru","pink","plum","powderblue","purple","rebeccapurple","red","rosybrown","royalblue","saddlebrown","salmon","sandybrown","seagreen","seashell","sienna","silver","skyblue","slateblue","slategray","snow","springgreen","steelblue","tan","teal","thistle","tomato","turquoise","violet","wheat","white","whitesmoke","yellow","yellowgreen"],u=b(t),v=["above","absolute","activeborder","additive","activecaption","afar","after-white-space","ahead","alias","all","all-scroll","alphabetic","alternate","always","amharic","amharic-abegede","antialiased","appworkspace","arabic-indic","armenian","asterisks","attr","auto","auto-flow","avoid","avoid-column","avoid-page","avoid-region","background","backwards","baseline","below","bidi-override","binary","bengali","blink","block","block-axis","bold","bolder","border","border-box","both","bottom","break","break-all","break-word","bullets","button","button-bevel","buttonface","buttonhighlight","buttonshadow","buttontext","calc","cambodian","capitalize","caps-lock-indicator","caption","captiontext","caret","cell","center","checkbox","circle","cjk-decimal","cjk-earthly-branch","cjk-heavenly-stem","cjk-ideographic","clear","clip","close-quote","col-resize","collapse","color","color-burn","color-dodge","column","column-reverse","compact","condensed","contain","content","contents","content-box","context-menu","continuous","copy","counter","counters","cover","crop","cross","crosshair","currentcolor","cursive","cyclic","darken","dashed","decimal","decimal-leading-zero","default","default-button","dense","destination-atop","destination-in","destination-out","destination-over","devanagari","difference","disc","discard","disclosure-closed","disclosure-open","document","dot-dash","dot-dot-dash","dotted","double","down","e-resize","ease","ease-in","ease-in-out","ease-out","element","ellipse","ellipsis","embed","end","ethiopic","ethiopic-abegede","ethiopic-abegede-am-et","ethiopic-abegede-gez","ethiopic-abegede-ti-er","ethiopic-abegede-ti-et","ethiopic-halehame-aa-er","ethiopic-halehame-aa-et","ethiopic-halehame-am-et","ethiopic-halehame-gez","ethiopic-halehame-om-et","ethiopic-halehame-sid-et","ethiopic-halehame-so-et","ethiopic-halehame-ti-er","ethiopic-halehame-ti-et","ethiopic-halehame-tig","ethiopic-numeric","ew-resize","exclusion","expanded","extends","extra-condensed","extra-expanded","fantasy","fast","fill","fixed","flat","flex","flex-end","flex-start","footnotes","forwards","from","geometricPrecision","georgian","graytext","grid","groove","gujarati","gurmukhi","hand","hangul","hangul-consonant","hard-light","hebrew","help","hidden","hide","higher","highlight","highlighttext","hiragana","hiragana-iroha","horizontal","hsl","hsla","hue","icon","ignore","inactiveborder","inactivecaption","inactivecaptiontext","infinite","infobackground","infotext","inherit","initial","inline","inline-axis","inline-block","inline-flex","inline-grid","inline-table","inset","inside","intrinsic","invert","italic","japanese-formal","japanese-informal","justify","kannada","katakana","katakana-iroha","keep-all","khmer","korean-hangul-formal","korean-hanja-formal","korean-hanja-informal","landscape","lao","large","larger","left","level","lighter","lighten","line-through","linear","linear-gradient","lines","list-item","listbox","listitem","local","logical","loud","lower","lower-alpha","lower-armenian","lower-greek","lower-hexadecimal","lower-latin","lower-norwegian","lower-roman","lowercase","ltr","luminosity","malayalam","match","matrix","matrix3d","media-controls-background","media-current-time-display","media-fullscreen-button","media-mute-button","media-play-button","media-return-to-realtime-button","media-rewind-button","media-seek-back-button","media-seek-forward-button","media-slider","media-sliderthumb","media-time-remaining-display","media-volume-slider","media-volume-slider-container","media-volume-sliderthumb","medium","menu","menulist","menulist-button","menulist-text","menulist-textfield","menutext","message-box","middle","min-intrinsic","mix","mongolian","monospace","move","multiple","multiply","myanmar","n-resize","narrower","ne-resize","nesw-resize","no-close-quote","no-drop","no-open-quote","no-repeat","none","normal","not-allowed","nowrap","ns-resize","numbers","numeric","nw-resize","nwse-resize","oblique","octal","opacity","open-quote","optimizeLegibility","optimizeSpeed","oriya","oromo","outset","outside","outside-shape","overlay","overline","padding","padding-box","painted","page","paused","persian","perspective","plus-darker","plus-lighter","pointer","polygon","portrait","pre","pre-line","pre-wrap","preserve-3d","progress","push-button","radial-gradient","radio","read-only","read-write","read-write-plaintext-only","rectangle","region","relative","repeat","repeating-linear-gradient","repeating-radial-gradient","repeat-x","repeat-y","reset","reverse","rgb","rgba","ridge","right","rotate","rotate3d","rotateX","rotateY","rotateZ","round","row","row-resize","row-reverse","rtl","run-in","running","s-resize","sans-serif","saturation","scale","scale3d","scaleX","scaleY","scaleZ","screen","scroll","scrollbar","scroll-position","se-resize","searchfield","searchfield-cancel-button","searchfield-decoration","searchfield-results-button","searchfield-results-decoration","self-start","self-end","semi-condensed","semi-expanded","separate","serif","show","sidama","simp-chinese-formal","simp-chinese-informal","single","skew","skewX","skewY","skip-white-space","slide","slider-horizontal","slider-vertical","sliderthumb-horizontal","sliderthumb-vertical","slow","small","small-caps","small-caption","smaller","soft-light","solid","somali","source-atop","source-in","source-out","source-over","space","space-around","space-between","space-evenly","spell-out","square","square-button","start","static","status-bar","stretch","stroke","sub","subpixel-antialiased","super","sw-resize","symbolic","symbols","system-ui","table","table-caption","table-cell","table-column","table-column-group","table-footer-group","table-header-group","table-row","table-row-group","tamil","telugu","text","text-bottom","text-top","textarea","textfield","thai","thick","thin","threeddarkshadow","threedface","threedhighlight","threedlightshadow","threedshadow","tibetan","tigre","tigrinya-er","tigrinya-er-abegede","tigrinya-et","tigrinya-et-abegede","to","top","trad-chinese-formal","trad-chinese-informal","transform","translate","translate3d","translateX","translateY","translateZ","transparent","ultra-condensed","ultra-expanded","underline","unset","up","upper-alpha","upper-armenian","upper-greek","upper-hexadecimal","upper-latin","upper-norwegian","upper-roman","uppercase","urdu","url","var","vertical","vertical-text","visible","visibleFill","visiblePainted","visibleStroke","visual","w-resize","wait","wave","wider","window","windowframe","windowtext","words","wrap","wrap-reverse","x-large","x-small","xor","xx-large","xx-small"],w=b(v),x=d.concat(f).concat(h).concat(j).concat(l).concat(n).concat(t).concat(v); -a.registerHelper("hintWords","css",x),a.defineMIME("text/css",{documentTypes:e,mediaTypes:g,mediaFeatures:i,mediaValueKeywords:k,propertyKeywords:m,nonStandardPropertyKeywords:o,fontProperties:q,counterDescriptors:s,colorKeywords:u,valueKeywords:w,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=c,c(a,b))}},name:"css"}),a.defineMIME("text/x-scss",{mediaTypes:g,mediaFeatures:i,mediaValueKeywords:k,propertyKeywords:m,nonStandardPropertyKeywords:o,colorKeywords:u,valueKeywords:w,fontProperties:q,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=c,c(a,b)):["operator","operator"]},":":function(a){return!!a.match(/\s*\{/,!1)&&[null,null]},$:function(a){return a.match(/^[\w-]+/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"]},"#":function(a){return!!a.eat("{")&&[null,"interpolation"]}},name:"css",helperType:"scss"}),a.defineMIME("text/x-less",{mediaTypes:g,mediaFeatures:i,mediaValueKeywords:k,propertyKeywords:m,nonStandardPropertyKeywords:o,colorKeywords:u,valueKeywords:w,fontProperties:q,allowNested:!0,lineComment:"//",tokenHooks:{"/":function(a,b){return a.eat("/")?(a.skipToEnd(),["comment","comment"]):a.eat("*")?(b.tokenize=c,c(a,b)):["operator","operator"]},"@":function(a){return a.eat("{")?[null,"interpolation"]:!a.match(/^(charset|document|font-face|import|(-(moz|ms|o|webkit)-)?keyframes|media|namespace|page|supports)\b/,!1)&&(a.eatWhile(/[\w\\\-]/),a.match(/^\s*:/,!1)?["variable-2","variable-definition"]:["variable-2","variable"])},"&":function(){return["atom","atom"]}},name:"css",helperType:"less"}),a.defineMIME("text/x-gss",{documentTypes:e,mediaTypes:g,mediaFeatures:i,propertyKeywords:m,nonStandardPropertyKeywords:o,fontProperties:q,counterDescriptors:s,colorKeywords:u,valueKeywords:w,supportsAtComponent:!0,tokenHooks:{"/":function(a,b){return!!a.eat("*")&&(b.tokenize=c,c(a,b))}},name:"css",helperType:"gss"})})},{"../../lib/codemirror":59}],62:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("diff",function(){var a={"+":"positive","-":"negative","@":"meta"};return{token:function(b){var c=b.string.search(/[\t ]+?$/);if(!b.sol()||0===c)return b.skipToEnd(),("error "+(a[b.string.charAt(0)]||"")).replace(/ $/,"");var d=a[b.peek()]||b.skipToEnd();return c===-1?b.skipToEnd():b.pos=c,d}}}),a.defineMIME("text/x-diff","diff")})},{"../../lib/codemirror":59}],63:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../markdown/markdown"),a("../../addon/mode/overlay")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../markdown/markdown","../../addon/mode/overlay"],d):d(CodeMirror)}(function(a){"use strict";var b=/^((?:(?:aaas?|about|acap|adiumxtra|af[ps]|aim|apt|attachment|aw|beshare|bitcoin|bolo|callto|cap|chrome(?:-extension)?|cid|coap|com-eventbrite-attendee|content|crid|cvs|data|dav|dict|dlna-(?:playcontainer|playsingle)|dns|doi|dtn|dvb|ed2k|facetime|feed|file|finger|fish|ftp|geo|gg|git|gizmoproject|go|gopher|gtalk|h323|hcp|https?|iax|icap|icon|im|imap|info|ipn|ipp|irc[6s]?|iris(?:\.beep|\.lwz|\.xpc|\.xpcs)?|itms|jar|javascript|jms|keyparc|lastfm|ldaps?|magnet|mailto|maps|market|message|mid|mms|ms-help|msnim|msrps?|mtqp|mumble|mupdate|mvn|news|nfs|nih?|nntp|notes|oid|opaquelocktoken|palm|paparazzi|platform|pop|pres|proxy|psyc|query|res(?:ource)?|rmi|rsync|rtmp|rtsp|secondlife|service|session|sftp|sgn|shttp|sieve|sips?|skype|sm[bs]|snmp|soap\.beeps?|soldat|spotify|ssh|steam|svn|tag|teamspeak|tel(?:net)?|tftp|things|thismessage|tip|tn3270|tv|udp|unreal|urn|ut2004|vemmi|ventrilo|view-source|webcal|wss?|wtai|wyciwyg|xcon(?:-userid)?|xfire|xmlrpc\.beeps?|xmpp|xri|ymsgr|z39\.50[rs]?):(?:\/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}\/)(?:[^\s()<>]|\([^\s()<>]*\))+(?:\([^\s()<>]*\)|[^\s`*!()\[\]{};:'".,<>?\xab\xbb\u201c\u201d\u2018\u2019]))/i;a.defineMode("gfm",function(c,d){function e(a){return a.code=!1,null}var f=0,g={startState:function(){return{code:!1,codeBlock:!1,ateSpace:!1}},copyState:function(a){return{code:a.code,codeBlock:a.codeBlock,ateSpace:a.ateSpace}},token:function(a,c){if(c.combineTokens=null,c.codeBlock)return a.match(/^```+/)?(c.codeBlock=!1,null):(a.skipToEnd(),null);if(a.sol()&&(c.code=!1),a.sol()&&a.match(/^```+/))return a.skipToEnd(),c.codeBlock=!0,null;if("`"===a.peek()){a.next();var e=a.pos;a.eatWhile("`");var g=1+a.pos-e;return c.code?g===f&&(c.code=!1):(f=g,c.code=!0),null}if(c.code)return a.next(),null;if(a.eatSpace())return c.ateSpace=!0,null;if((a.sol()||c.ateSpace)&&(c.ateSpace=!1,d.gitHubSpice!==!1)){if(a.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+@)?(?:[a-f0-9]{7,40}\b)/))return c.combineTokens=!0,"link";if(a.match(/^(?:[a-zA-Z0-9\-_]+\/)?(?:[a-zA-Z0-9\-_]+)?#[0-9]+\b/))return c.combineTokens=!0,"link"}return a.match(b)&&"]("!=a.string.slice(a.start-2,a.start)&&(0==a.start||/\W/.test(a.string.charAt(a.start-1)))?(c.combineTokens=!0,"link"):(a.next(),null)},blankLine:e},h={taskLists:!0,strikethrough:!0,emoji:!0};for(var i in d)h[i]=d[i];return h.name="markdown",a.overlayMode(a.getMode(c,h),g)},"markdown"),a.defineMIME("text/x-gfm","gfm")})},{"../../addon/mode/overlay":37,"../../lib/codemirror":59,"../markdown/markdown":68}],64:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../xml/xml"),a("../javascript/javascript"),a("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript","../css/css"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c){var d=a.current(),e=d.search(b);return e>-1?a.backUp(d.length-e):d.match(/<\/?$/)&&(a.backUp(d.length),a.match(b,!1)||a.match(d)),c}function c(a){var b=i[a];return b?b:i[a]=new RegExp("\\s+"+a+"\\s*=\\s*('|\")?([^'\"]+)('|\")?\\s*")}function d(a,b){var d=a.match(c(b));return d?/^\s*(.*?)\s*$/.exec(d[2])[1]:""}function e(a,b){return new RegExp((b?"^":"")+"","i")}function f(a,b){for(var c in a)for(var d=b[c]||(b[c]=[]),e=a[c],f=e.length-1;f>=0;f--)d.unshift(e[f])}function g(a,b){for(var c=0;c\s\/]/.test(d.current())&&(h=f.htmlState.tagName&&f.htmlState.tagName.toLowerCase())&&k.hasOwnProperty(h))f.inTag=h+" ";else if(f.inTag&&m&&/>$/.test(d.current())){var n=/^([\S]+) (.*)/.exec(f.inTag);f.inTag=null;var o=">"==d.current()&&g(k[n[1]],n[2]),p=a.getMode(c,o),q=e(n[1],!0),r=e(n[1],!1);f.token=function(a,c){return a.match(q,!1)?(c.token=i,c.localState=c.localMode=null,null):b(a,r,c.localMode.token(a,c.localState))},f.localMode=p,f.localState=a.startState(p,j.indent(f.htmlState,""))}else f.inTag&&(f.inTag+=d.current(),d.eol()&&(f.inTag+=" "));return l}var j=a.getMode(c,{name:"xml",htmlMode:!0,multilineTagIndentFactor:d.multilineTagIndentFactor,multilineTagIndentPastTag:d.multilineTagIndentPastTag}),k={},l=d&&d.tags,m=d&&d.scriptTypes;if(f(h,k),l&&f(l,k),m)for(var n=m.length-1;n>=0;n--)k.script.unshift(["type",m[n].matches,m[n].mode]);return{startState:function(){var b=a.startState(j);return{token:i,inTag:null,localMode:null,localState:null,htmlState:b}},copyState:function(b){var c;return b.localState&&(c=a.copyState(b.localMode,b.localState)),{token:b.token,inTag:b.inTag,localMode:b.localMode,localState:c,htmlState:a.copyState(j,b.htmlState)}},token:function(a,b){return b.token(a,b)},indent:function(b,c,d){return!b.localMode||/^\s*<\//.test(c)?j.indent(b.htmlState,c):b.localMode.indent?b.localMode.indent(b.localState,c,d):a.Pass},innerMode:function(a){return{state:a.localState||a.htmlState,mode:a.localMode||j}}}},"xml","javascript","css"),a.defineMIME("text/html","htmlmixed")})},{"../../lib/codemirror":59,"../css/css":61,"../javascript/javascript":66,"../xml/xml":75}],65:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("http",function(){function a(a,b){return a.skipToEnd(),b.cur=g,"error"}function b(b,d){return b.match(/^HTTP\/\d\.\d/)?(d.cur=c,"keyword"):b.match(/^[A-Z]+/)&&/[ \t]/.test(b.peek())?(d.cur=e,"keyword"):a(b,d)}function c(b,c){var e=b.match(/^\d+/);if(!e)return a(b,c);c.cur=d;var f=Number(e[0]);return f>=100&&f<200?"positive informational":f>=200&&f<300?"positive success":f>=300&&f<400?"positive redirect":f>=400&&f<500?"negative client-error":f>=500&&f<600?"negative server-error":"error"}function d(a,b){return a.skipToEnd(),b.cur=g,null}function e(a,b){return a.eatWhile(/\S/),b.cur=f,"string-2"}function f(b,c){return b.match(/^HTTP\/\d\.\d$/)?(c.cur=g,"keyword"):a(b,c)}function g(a){return a.sol()&&!a.eat(/[ \t]/)?a.match(/^.*?:/)?"atom":(a.skipToEnd(),"error"):(a.skipToEnd(),"string")}function h(a){return a.skipToEnd(),null}return{token:function(a,b){var c=b.cur;return c!=g&&c!=h&&a.eatSpace()?null:c(a,b)},blankLine:function(a){a.cur=h},startState:function(){return{cur:b}}}}),a.defineMIME("message/http","http")})},{"../../lib/codemirror":59}],66:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("javascript",function(b,c){function d(a){for(var b,c=!1,d=!1;null!=(b=a.next());){if(!c){if("/"==b&&!d)return;"["==b?d=!0:d&&"]"==b&&(d=!1)}c=!c&&"\\"==b}}function e(a,b,c){return Aa=a,Ba=c,b}function f(a,b){var c=a.next();if('"'==c||"'"==c)return b.tokenize=g(c),b.tokenize(a,b);if("."==c&&a.match(/^\d+(?:[eE][+\-]?\d+)?/))return e("number","number");if("."==c&&a.match(".."))return e("spread","meta");if(/[\[\]{}\(\),;\:\.]/.test(c))return e(c);if("="==c&&a.eat(">"))return e("=>","operator");if("0"==c&&a.eat(/x/i))return a.eatWhile(/[\da-f]/i),e("number","number");if("0"==c&&a.eat(/o/i))return a.eatWhile(/[0-7]/i),e("number","number");if("0"==c&&a.eat(/b/i))return a.eatWhile(/[01]/i),e("number","number");if(/\d/.test(c))return a.match(/^\d*(?:\.\d*)?(?:[eE][+\-]?\d+)?/),e("number","number");if("/"==c)return a.eat("*")?(b.tokenize=h,h(a,b)):a.eat("/")?(a.skipToEnd(),e("comment","comment")):za(a,b,1)?(d(a),a.match(/^\b(([gimyu])(?![gimyu]*\2))+\b/),e("regexp","string-2")):(a.eatWhile(Ja),e("operator","operator",a.current()));if("`"==c)return b.tokenize=i,i(a,b);if("#"==c)return a.skipToEnd(),e("error","error");if(Ja.test(c))return">"==c&&b.lexical&&">"==b.lexical.type||a.eatWhile(Ja),e("operator","operator",a.current());if(Ha.test(c)){a.eatWhile(Ha);var f=a.current();if("."!=b.lastType){if(Ia.propertyIsEnumerable(f)){var j=Ia[f];return e(j.type,j.style,f)}if("async"==f&&a.match(/^\s*[\(\w]/,!1))return e("async","keyword",f)}return e("variable","variable",f)}}function g(a){return function(b,c){var d,g=!1;if(Ea&&"@"==b.peek()&&b.match(Ka))return c.tokenize=f,e("jsonld-keyword","meta");for(;null!=(d=b.next())&&(d!=a||g);)g=!g&&"\\"==d;return g||(c.tokenize=f),e("string","string")}}function h(a,b){for(var c,d=!1;c=a.next();){if("/"==c&&d){b.tokenize=f;break}d="*"==c}return e("comment","comment")}function i(a,b){for(var c,d=!1;null!=(c=a.next());){if(!d&&("`"==c||"$"==c&&a.eat("{"))){b.tokenize=f;break}d=!d&&"\\"==c}return e("quasi","string-2",a.current())}function j(a,b){b.fatArrowAt&&(b.fatArrowAt=null);var c=a.string.indexOf("=>",a.start);if(!(c<0)){if(Ga){var d=/:\s*(?:\w+(?:<[^>]*>|\[\])?|\{[^}]*\})\s*$/.exec(a.string.slice(a.start,c));d&&(c=d.index)}for(var e=0,f=!1,g=c-1;g>=0;--g){var h=a.string.charAt(g),i=La.indexOf(h);if(i>=0&&i<3){if(!e){++g;break}if(0==--e){"("==h&&(f=!0);break}}else if(i>=3&&i<6)++e;else if(Ha.test(h))f=!0;else{if(/["'\/]/.test(h))return;if(f&&!e){++g;break}}}f&&!e&&(b.fatArrowAt=g)}}function k(a,b,c,d,e,f){this.indented=a,this.column=b,this.type=c,this.prev=e,this.info=f,null!=d&&(this.align=d)}function l(a,b){for(var c=a.localVars;c;c=c.next)if(c.name==b)return!0;for(var d=a.context;d;d=d.prev)for(var c=d.vars;c;c=c.next)if(c.name==b)return!0}function m(a,b,c,d,e){var f=a.cc;for(Na.state=a,Na.stream=e,Na.marked=null,Na.cc=f,Na.style=b,a.lexical.hasOwnProperty("align")||(a.lexical.align=!0);;){var g=f.length?f.pop():Fa?w:v;if(g(c,d)){for(;f.length&&f[f.length-1].lex;)f.pop()();return Na.marked?Na.marked:"variable"==c&&l(a,d)?"variable-2":b}}}function n(){for(var a=arguments.length-1;a>=0;a--)Na.cc.push(arguments[a])}function o(){return n.apply(null,arguments),!0}function p(a){function b(b){for(var c=b;c;c=c.next)if(c.name==a)return!0;return!1}var d=Na.state;if(Na.marked="def",d.context){if(b(d.localVars))return;d.localVars={name:a,next:d.localVars}}else{if(b(d.globalVars))return;c.globalVars&&(d.globalVars={name:a,next:d.globalVars})}}function q(){Na.state.context={prev:Na.state.context,vars:Na.state.localVars},Na.state.localVars=Oa}function r(){Na.state.localVars=Na.state.context.vars,Na.state.context=Na.state.context.prev}function s(a,b){var c=function(){var c=Na.state,d=c.indented;if("stat"==c.lexical.type)d=c.lexical.indented;else for(var e=c.lexical;e&&")"==e.type&&e.align;e=e.prev)d=e.indented;c.lexical=new k(d,Na.stream.column(),a,null,c.lexical,b)};return c.lex=!0,c}function t(){var a=Na.state;a.lexical.prev&&(")"==a.lexical.type&&(a.indented=a.lexical.indented),a.lexical=a.lexical.prev)}function u(a){function b(c){return c==a?o():";"==a?n():o(b)}return b}function v(a,b){return"var"==a?o(s("vardef",b.length),$,u(";"),t):"keyword a"==a?o(s("form"),y,v,t):"keyword b"==a?o(s("form"),v,t):"{"==a?o(s("}"),S,t):";"==a?o():"if"==a?("else"==Na.state.lexical.info&&Na.state.cc[Na.state.cc.length-1]==t&&Na.state.cc.pop()(),o(s("form"),y,v,t,da)):"function"==a?o(ja):"for"==a?o(s("form"),ea,v,t):"variable"==a?Ga&&"type"==b?(Na.marked="keyword",o(U,u("operator"),U,u(";"))):Ga&&"declare"==b?(Na.marked="keyword",o(v)):o(s("stat"),L):"switch"==a?o(s("form"),y,u("{"),s("}","switch"),S,t,t):"case"==a?o(w,u(":")):"default"==a?o(u(":")):"catch"==a?o(s("form"),q,u("("),ka,u(")"),v,t,r):"class"==a?o(s("form"),ma,t):"export"==a?o(s("stat"),qa,t):"import"==a?o(s("stat"),sa,t):"module"==a?o(s("form"),_,u("{"),s("}"),S,t,t):"async"==a?o(v):"@"==b?o(w,v):n(s("stat"),w,u(";"),t)}function w(a){return z(a,!1)}function x(a){return z(a,!0)}function y(a){return"("!=a?n():o(s(")"),w,u(")"),t)}function z(a,b){if(Na.state.fatArrowAt==Na.stream.start){var c=b?H:G;if("("==a)return o(q,s(")"),Q(ka,")"),t,u("=>"),c,r);if("variable"==a)return n(q,_,u("=>"),c,r)}var d=b?D:C;return Ma.hasOwnProperty(a)?o(d):"function"==a?o(ja,d):"class"==a?o(s("form"),la,t):"keyword c"==a||"async"==a?o(b?B:A):"("==a?o(s(")"),A,u(")"),t,d):"operator"==a||"spread"==a?o(b?x:w):"["==a?o(s("]"),xa,t,d):"{"==a?R(N,"}",null,d):"quasi"==a?n(E,d):"new"==a?o(I(b)):o()}function A(a){return a.match(/[;\}\)\],]/)?n():n(w)}function B(a){return a.match(/[;\}\)\],]/)?n():n(x)}function C(a,b){return","==a?o(w):D(a,b,!1)}function D(a,b,c){var d=0==c?C:D,e=0==c?w:x;return"=>"==a?o(q,c?H:G,r):"operator"==a?/\+\+|--/.test(b)||Ga&&"!"==b?o(d):"?"==b?o(w,u(":"),e):o(e):"quasi"==a?n(E,d):";"!=a?"("==a?R(x,")","call",d):"."==a?o(M,d):"["==a?o(s("]"),A,u("]"),t,d):Ga&&"as"==b?(Na.marked="keyword",o(U,d)):void 0:void 0}function E(a,b){return"quasi"!=a?n():"${"!=b.slice(b.length-2)?o(E):o(w,F)}function F(a){if("}"==a)return Na.marked="string-2",Na.state.tokenize=i,o(E)}function G(a){return j(Na.stream,Na.state),n("{"==a?v:w)}function H(a){return j(Na.stream,Na.state),n("{"==a?v:x)}function I(a){return function(b){return"."==b?o(a?K:J):"variable"==b&&Ga?o(Z,a?D:C):n(a?x:w)}}function J(a,b){if("target"==b)return Na.marked="keyword",o(C)}function K(a,b){if("target"==b)return Na.marked="keyword",o(D)}function L(a){return":"==a?o(t,v):n(C,u(";"),t)}function M(a){if("variable"==a)return Na.marked="property",o()}function N(a,b){if("async"==a)return Na.marked="property",o(N);if("variable"==a||"keyword"==Na.style){if(Na.marked="property","get"==b||"set"==b)return o(O);var c;return Ga&&Na.state.fatArrowAt==Na.stream.start&&(c=Na.stream.match(/^\s*:\s*/,!1))&&(Na.state.fatArrowAt=Na.stream.pos+c[0].length),o(P)}return"number"==a||"string"==a?(Na.marked=Ea?"property":Na.style+" property",o(P)):"jsonld-keyword"==a?o(P):"modifier"==a?o(N):"["==a?o(w,u("]"),P):"spread"==a?o(w,P):":"==a?n(P):void 0}function O(a){return"variable"!=a?n(P):(Na.marked="property",o(ja))}function P(a){return":"==a?o(x):"("==a?n(ja):void 0}function Q(a,b,c){function d(e,f){if(c?c.indexOf(e)>-1:","==e){var g=Na.state.lexical;return"call"==g.info&&(g.pos=(g.pos||0)+1),o(function(c,d){return c==b||d==b?n():n(a)},d)}return e==b||f==b?o():o(u(b))}return function(c,e){return c==b||e==b?o():n(a,d)}}function R(a,b,c){for(var d=3;d"==a)return o(U)}function W(a,b){return"variable"==a||"keyword"==Na.style?(Na.marked="property",o(W)):"?"==b?o(W):":"==a?o(U):"["==a?o(w,T,u("]"),W):void 0}function X(a){return"variable"==a?o(X):":"==a?o(U):void 0}function Y(a,b){return"<"==b?o(s(">"),Q(U,">"),t,Y):"|"==b||"."==a?o(U):"["==a?o(u("]"),Y):"extends"==b?o(U):void 0}function Z(a,b){if("<"==b)return o(s(">"),Q(U,">"),t,Y)}function $(){return n(_,T,ba,ca)}function _(a,b){return"modifier"==a?o(_):"variable"==a?(p(b),o()):"spread"==a?o(_):"["==a?R(_,"]"):"{"==a?R(aa,"}"):void 0}function aa(a,b){return"variable"!=a||Na.stream.match(/^\s*:/,!1)?("variable"==a&&(Na.marked="property"),"spread"==a?o(_):"}"==a?n():o(u(":"),_,ba)):(p(b),o(ba))}function ba(a,b){if("="==b)return o(x)}function ca(a){if(","==a)return o($)}function da(a,b){if("keyword b"==a&&"else"==b)return o(s("form","else"),v,t)}function ea(a){if("("==a)return o(s(")"),fa,u(")"),t)}function fa(a){return"var"==a?o($,u(";"),ha):";"==a?o(ha):"variable"==a?o(ga):n(w,u(";"),ha)}function ga(a,b){return"in"==b||"of"==b?(Na.marked="keyword",o(w)):o(C,ha)}function ha(a,b){return";"==a?o(ia):"in"==b||"of"==b?(Na.marked="keyword",o(w)):n(w,u(";"),ia)}function ia(a){")"!=a&&o(w)}function ja(a,b){return"*"==b?(Na.marked="keyword",o(ja)):"variable"==a?(p(b),o(ja)):"("==a?o(q,s(")"),Q(ka,")"),t,T,v,r):Ga&&"<"==b?o(s(">"),Q(U,">"),t,ja):void 0}function ka(a){return"spread"==a||"modifier"==a?o(ka):n(_,T,ba)}function la(a,b){return"variable"==a?ma(a,b):na(a,b)}function ma(a,b){if("variable"==a)return p(b),o(na)}function na(a,b){return"<"==b?o(s(">"),Q(U,">"),t,na):"extends"==b||"implements"==b||Ga&&","==a?o(Ga?U:w,na):"{"==a?o(s("}"),oa,t):void 0}function oa(a,b){return"modifier"==a||"async"==a||"variable"==a&&("static"==b||"get"==b||"set"==b)&&Na.stream.match(/^\s+[\w$\xa1-\uffff]/,!1)?(Na.marked="keyword",o(oa)):"variable"==a||"keyword"==Na.style?(Na.marked="property",o(Ga?pa:ja,oa)):"["==a?o(w,u("]"),Ga?pa:ja,oa):"*"==b?(Na.marked="keyword",o(oa)):";"==a?o(oa):"}"==a?o():"@"==b?o(w,oa):void 0}function pa(a,b){return"?"==b?o(pa):":"==a?o(U,ba):"="==b?o(x):n(ja)}function qa(a,b){return"*"==b?(Na.marked="keyword",o(wa,u(";"))):"default"==b?(Na.marked="keyword",o(w,u(";"))):"{"==a?o(Q(ra,"}"),wa,u(";")):n(v)}function ra(a,b){return"as"==b?(Na.marked="keyword",o(u("variable"))):"variable"==a?n(x,ra):void 0}function sa(a){return"string"==a?o():n(ta,ua,wa)}function ta(a,b){return"{"==a?R(ta,"}"):("variable"==a&&p(b),"*"==b&&(Na.marked="keyword"),o(va))}function ua(a){if(","==a)return o(ta,ua)}function va(a,b){if("as"==b)return Na.marked="keyword",o(ta)}function wa(a,b){if("from"==b)return Na.marked="keyword",o(w)}function xa(a){return"]"==a?o():n(Q(x,"]"))}function ya(a,b){return"operator"==a.lastType||","==a.lastType||Ja.test(b.charAt(0))||/[,.]/.test(b.charAt(0))}function za(a,b,c){return b.tokenize==f&&/^(?:operator|sof|keyword c|case|new|export|default|[\[{}\(,;:]|=>)$/.test(b.lastType)||"quasi"==b.lastType&&/\{\s*$/.test(a.string.slice(0,a.pos-(c||0)))}var Aa,Ba,Ca=b.indentUnit,Da=c.statementIndent,Ea=c.jsonld,Fa=c.json||Ea,Ga=c.typescript,Ha=c.wordCharacters||/[\w$\xa1-\uffff]/,Ia=function(){function a(a){return{type:a,style:"keyword"}}var b=a("keyword a"),c=a("keyword b"),d=a("keyword c"),e=a("operator"),f={type:"atom",style:"atom"},g={"if":a("if"),"while":b,"with":b,"else":c,"do":c,"try":c,"finally":c,"return":d,"break":d,"continue":d,"new":a("new"),"delete":d,"throw":d,"debugger":d,"var":a("var"),"const":a("var"),"let":a("var"),"function":a("function"),"catch":a("catch"),"for":a("for"),"switch":a("switch"),"case":a("case"),"default":a("default"),"in":e,"typeof":e,"instanceof":e,"true":f,"false":f,"null":f,undefined:f,NaN:f,Infinity:f,"this":a("this"),"class":a("class"),"super":a("atom"),"yield":d,"export":a("export"),"import":a("import"),"extends":d,await:d};if(Ga){var h={type:"variable",style:"type"},i={"interface":a("class"),"implements":d,namespace:d,module:a("module"),"enum":a("module"),"public":a("modifier"),"private":a("modifier"),"protected":a("modifier"),"abstract":a("modifier"),readonly:a("modifier"),string:h,number:h,"boolean":h,any:h};for(var j in i)g[j]=i[j]}return g}(),Ja=/[+\-*&%=<>!?|~^@]/,Ka=/^@(context|id|value|language|type|container|list|set|reverse|index|base|vocab|graph)"/,La="([{}])",Ma={atom:!0,number:!0,variable:!0,string:!0,regexp:!0,"this":!0,"jsonld-keyword":!0},Na={state:null,column:null,marked:null,cc:null},Oa={name:"this",next:{name:"arguments"}};return t.lex=!0,{startState:function(a){var b={tokenize:f,lastType:"sof",cc:[],lexical:new k((a||0)-Ca,0,"block",!1),localVars:c.localVars,context:c.localVars&&{vars:c.localVars},indented:a||0};return c.globalVars&&"object"==typeof c.globalVars&&(b.globalVars=c.globalVars),b},token:function(a,b){if(a.sol()&&(b.lexical.hasOwnProperty("align")||(b.lexical.align=!1),b.indented=a.indentation(),j(a,b)),b.tokenize!=h&&a.eatSpace())return null;var c=b.tokenize(a,b);return"comment"==Aa?c:(b.lastType="operator"!=Aa||"++"!=Ba&&"--"!=Ba?Aa:"incdec",m(b,c,Aa,Ba,a))},indent:function(b,d){if(b.tokenize==h)return a.Pass;if(b.tokenize!=f)return 0;var e,g=d&&d.charAt(0),i=b.lexical;if(!/^\s*else\b/.test(d))for(var j=b.cc.length-1;j>=0;--j){var k=b.cc[j];if(k==t)i=i.prev;else if(k!=da)break}for(;("stat"==i.type||"form"==i.type)&&("}"==g||(e=b.cc[b.cc.length-1])&&(e==C||e==D)&&!/^[,\.=+\-*:?[\(]/.test(d));)i=i.prev;Da&&")"==i.type&&"stat"==i.prev.type&&(i=i.prev);var l=i.type,m=g==l;return"vardef"==l?i.indented+("operator"==b.lastType||","==b.lastType?i.info+1:0):"form"==l&&"{"==g?i.indented:"form"==l?i.indented+Ca:"stat"==l?i.indented+(ya(b,d)?Da||Ca:0):"switch"!=i.info||m||0==c.doubleIndentSwitch?i.align?i.column+(m?0:1):i.indented+(m?0:Ca):i.indented+(/^(?:case|default)\b/.test(d)?Ca:2*Ca)},electricInput:/^\s*(?:case .*?:|default:|\{|\})$/,blockCommentStart:Fa?null:"/*",blockCommentEnd:Fa?null:"*/",lineComment:Fa?null:"//",fold:"brace",closeBrackets:"()[]{}''\"\"``",helperType:Fa?"json":"javascript",jsonldMode:Ea,jsonMode:Fa,expressionAllowed:za,skipExpression:function(a){var b=a.cc[a.cc.length-1];b!=w&&b!=x||a.cc.pop()}}}),a.registerHelper("wordChars","javascript",/[\w$]/),a.defineMIME("text/javascript","javascript"),a.defineMIME("text/ecmascript","javascript"),a.defineMIME("application/javascript","javascript"),a.defineMIME("application/x-javascript","javascript"),a.defineMIME("application/ecmascript","javascript"),a.defineMIME("application/json",{name:"javascript",json:!0}),a.defineMIME("application/x-json",{name:"javascript",json:!0}),a.defineMIME("application/ld+json",{name:"javascript",jsonld:!0}),a.defineMIME("text/typescript",{name:"javascript",typescript:!0}),a.defineMIME("application/typescript",{name:"javascript",typescript:!0})})},{"../../lib/codemirror":59}],67:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../xml/xml"),a("../javascript/javascript")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../javascript/javascript"],d):d(CodeMirror)}(function(a){"use strict";function b(a,b,c,d){this.state=a,this.mode=b,this.depth=c,this.prev=d}function c(d){return new b(a.copyState(d.mode,d.state),d.mode,d.depth,d.prev&&c(d.prev))}a.defineMode("jsx",function(d,e){function f(a){var b=a.tagName;a.tagName=null;var c=j.indent(a,"");return a.tagName=b,c}function g(a,b){return b.context.mode==j?h(a,b,b.context):i(a,b,b.context)}function h(c,e,h){if(2==h.depth)return c.match(/^.*?\*\//)?h.depth=1:c.skipToEnd(),"comment";if("{"==c.peek()){j.skipAttribute(h.state);var i=f(h.state),l=h.state.context;if(l&&c.match(/^[^>]*>\s*$/,!1)){for(;l.prev&&!l.startOfLine;)l=l.prev;l.startOfLine?i-=d.indentUnit:h.prev.state.lexical&&(i=h.prev.state.lexical.indented)}else 1==h.depth&&(i+=d.indentUnit);return e.context=new b(a.startState(k,i),k,0,e.context),null}if(1==h.depth){if("<"==c.peek())return j.skipAttribute(h.state),e.context=new b(a.startState(j,f(h.state)),j,0,e.context),null;if(c.match("//"))return c.skipToEnd(),"comment";if(c.match("/*"))return h.depth=2,g(c,e)}var m,n=j.token(c,h.state),o=c.current();return/\btag\b/.test(n)?/>$/.test(o)?h.state.context?h.depth=0:e.context=e.context.prev:/^-1&&c.backUp(o.length-m),n}function i(c,d,e){if("<"==c.peek()&&k.expressionAllowed(c,e.state))return k.skipExpression(e.state),d.context=new b(a.startState(j,k.indent(e.state,"")),j,0,d.context),null;var f=k.token(c,e.state);if(!f&&null!=e.depth){var g=c.current();"{"==g?e.depth++:"}"==g&&0==--e.depth&&(d.context=d.context.prev)}return f}var j=a.getMode(d,{name:"xml",allowMissing:!0,multilineTagIndentPastTag:!1}),k=a.getMode(d,e&&e.base||"javascript");return{startState:function(){return{context:new b(a.startState(k),k)}},copyState:function(a){return{context:c(a.context)}},token:g,indent:function(a,b,c){return a.context.mode.indent(a.context.state,b,c)},innerMode:function(a){return a.context}}},"xml","javascript"),a.defineMIME("text/jsx","jsx"),a.defineMIME("text/typescript-jsx",{name:"jsx",base:{name:"javascript",typescript:!0}})})},{"../../lib/codemirror":59,"../javascript/javascript":66,"../xml/xml":75}],68:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../xml/xml"),a("../meta")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../xml/xml","../meta"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("markdown",function(b,c){function d(c){if(a.findModeByName){var d=a.findModeByName(c);d&&(c=d.mime||d.mimes[0])}var e=a.getMode(b,c);return"null"==e.name?null:e}function e(a,b,c){return b.f=b.inline=c,c(a,b)}function f(a,b,c){return b.f=b.block=c,c(a,b)}function g(a){return!a||!/\S/.test(a.string)}function h(a){return a.linkTitle=!1,a.em=!1,a.strong=!1,a.strikethrough=!1,a.quote=0,a.indentedCode=!1,a.f==j&&(a.f=n,a.block=i),a.trailingSpace=0,a.trailingSpaceNewLine=!1,a.prevLine=a.thisLine,a.thisLine={stream:null},null}function i(b,f){var h=b.column()===f.indentation,i=g(f.prevLine.stream),j=f.indentedCode,m=f.prevLine.hr,n=f.list!==!1,o=(f.listStack[f.listStack.length-1]||0)+3;f.indentedCode=!1;var p=f.indentation;if(null===f.indentationDiff&&(f.indentationDiff=f.indentation,n)){for(f.list=null;p=4&&(j||f.prevLine.fencedCodeEnd||f.prevLine.header||i))return b.skipToEnd(),f.indentedCode=!0,w.code;if(b.eatSpace())return null;if(h&&f.indentation<=o&&(t=b.match(B))&&t[1].length<=6)return f.quote=0,f.header=t[1].length,f.thisLine.header=!0,c.highlightFormatting&&(f.formatting="header"),f.f=f.inline,l(f);if(f.indentation<=o&&b.eat(">"))return f.quote=h?1:f.quote+1,c.highlightFormatting&&(f.formatting="quote"),b.eatSpace(),l(f);if(!s&&!f.setext&&h&&f.indentation<=o&&(t=b.match(z))){var u=t[1]?"ol":"ul";return f.indentation=p+b.current().length,f.list=!0,f.quote=0,f.listStack.push(f.indentation),c.taskLists&&b.match(A,!1)&&(f.taskList=!0),f.f=f.inline,c.highlightFormatting&&(f.formatting=["list","list-"+u]),l(f)}return h&&f.indentation<=o&&(t=b.match(E,!0))?(f.quote=0,f.fencedEndRE=new RegExp(t[1]+"+ *$"),f.localMode=c.fencedCodeBlockHighlighting&&d(t[2]),f.localMode&&(f.localState=a.startState(f.localMode)),f.f=f.block=k,c.highlightFormatting&&(f.formatting="code-block"),f.code=-1,l(f)):f.setext||!(q&&n||f.quote||f.list!==!1||f.code||s||F.test(b.string))&&(t=b.lookAhead(1))&&(t=t.match(C))?(f.setext?(f.header=f.setext,f.setext=0,b.skipToEnd(),c.highlightFormatting&&(f.formatting="header")):(f.header="="==t[0].charAt(0)?1:2,f.setext=f.header),f.thisLine.header=!0,f.f=f.inline,l(f)):s?(b.skipToEnd(),f.hr=!0,f.thisLine.hr=!0,w.hr):"["===b.peek()?e(b,f,r):e(b,f,f.inline)}function j(b,c){var d=u.token(b,c.htmlState);if(!v){var e=a.innerMode(u,c.htmlState);("xml"==e.mode.name&&null===e.state.tagStart&&!e.state.context&&e.state.tokenize.isInText||c.md_inside&&b.current().indexOf(">")>-1)&&(c.f=n,c.block=i,c.htmlState=null)}return d}function k(a,b){var d=b.listStack[b.listStack.length-1]||0,e=b.indentation=a.quote?b.push(w.formatting+"-"+a.formatting[d]+"-"+a.quote):b.push("error"))}if(a.taskOpen)return b.push("meta"),b.length?b.join(" "):null;if(a.taskClosed)return b.push("property"),b.length?b.join(" "):null;if(a.linkHref?b.push(w.linkHref,"url"):(a.strong&&b.push(w.strong),a.em&&b.push(w.em),a.strikethrough&&b.push(w.strikethrough),a.emoji&&b.push(w.emoji),a.linkText&&b.push(w.linkText),a.code&&b.push(w.code),a.image&&b.push(w.image),a.imageAltText&&b.push(w.imageAltText,"link"),a.imageMarker&&b.push(w.imageMarker)),a.header&&b.push(w.header,w.header+"-"+a.header),a.quote&&(b.push(w.quote),!c.maxBlockquoteDepth||c.maxBlockquoteDepth>=a.quote?b.push(w.quote+"-"+a.quote):b.push(w.quote+"-"+c.maxBlockquoteDepth)),a.list!==!1){var e=(a.listStack.length-1)%3;e?1===e?b.push(w.list2):b.push(w.list3):b.push(w.list1)}return a.trailingSpaceNewLine?b.push("trailing-space-new-line"):a.trailingSpace&&b.push("trailing-space-"+(a.trailingSpace%2?"a":"b")),b.length?b.join(" "):null}function m(a,b){if(a.match(D,!0))return l(b)}function n(b,d){var e=d.text(b,d);if("undefined"!=typeof e)return e;if(d.list)return d.list=null,l(d);if(d.taskList){var g=" "===b.match(A,!0)[1];return g?d.taskOpen=!0:d.taskClosed=!0,c.highlightFormatting&&(d.formatting="task"),d.taskList=!1, -l(d)}if(d.taskOpen=!1,d.taskClosed=!1,d.header&&b.match(/^#+$/,!0))return c.highlightFormatting&&(d.formatting="header"),l(d);var h=b.next();if(d.linkTitle){d.linkTitle=!1;var i=h;"("===h&&(i=")"),i=(i+"").replace(/([.?*+^\[\]\\(){}|-])/g,"\\$1");var k="^\\s*(?:[^"+i+"\\\\]+|\\\\\\\\|\\\\.)"+i;if(b.match(new RegExp(k),!0))return w.linkHref}if("`"===h){var m=d.formatting;c.highlightFormatting&&(d.formatting="code"),b.eatWhile("`");var q=b.current().length;if(0!=d.code||d.quote&&1!=q){if(q==d.code){var r=l(d);return d.code=0,r}return d.formatting=m,l(d)}return d.code=q,l(d)}if(d.code)return l(d);if("\\"===h&&(b.next(),c.highlightFormatting)){var s=l(d),t=w.formatting+"-escape";return s?s+" "+t:t}if("!"===h&&b.match(/\[[^\]]*\] ?(?:\(|\[)/,!1))return d.imageMarker=!0,d.image=!0,c.highlightFormatting&&(d.formatting="image"),l(d);if("["===h&&d.imageMarker&&b.match(/[^\]]*\](\(.*?\)| ?\[.*?\])/,!1))return d.imageMarker=!1,d.imageAltText=!0,c.highlightFormatting&&(d.formatting="image"),l(d);if("]"===h&&d.imageAltText){c.highlightFormatting&&(d.formatting="image");var s=l(d);return d.imageAltText=!1,d.image=!1,d.inline=d.f=p,s}if("["===h&&!d.image)return d.linkText=!0,c.highlightFormatting&&(d.formatting="link"),l(d);if("]"===h&&d.linkText){c.highlightFormatting&&(d.formatting="link");var s=l(d);return d.linkText=!1,d.inline=d.f=b.match(/\(.*?\)| ?\[.*?\]/,!1)?p:n,s}if("<"===h&&b.match(/^(https?|ftps?):\/\/(?:[^\\>]|\\.)+>/,!1)){d.f=d.inline=o,c.highlightFormatting&&(d.formatting="link");var s=l(d);return s?s+=" ":s="",s+w.linkInline}if("<"===h&&b.match(/^[^> \\]+@(?:[^\\>]|\\.)+>/,!1)){d.f=d.inline=o,c.highlightFormatting&&(d.formatting="link");var s=l(d);return s?s+=" ":s="",s+w.linkEmail}if(c.xml&&"<"===h&&b.match(/^(!--|[a-z]+(?:\s+[a-z_:.\-]+(?:\s*=\s*[^ >]+)?)*\s*>)/i,!1)){var v=b.string.indexOf(">",b.pos);if(v!=-1){var x=b.string.substring(b.start,v);/markdown\s*=\s*('|"){0,1}1('|"){0,1}/.test(x)&&(d.md_inside=!0)}return b.backUp(1),d.htmlState=a.startState(u),f(b,d,j)}if(c.xml&&"<"===h&&b.match(/^\/\w*?>/))return d.md_inside=!1,"tag";if("*"===h||"_"===h){for(var y=1,z=1==b.pos?" ":b.string.charAt(b.pos-2);y<3&&b.eat(h);)y++;var B=b.peek()||" ",C=!/\s/.test(B)&&(!G.test(B)||/\s/.test(z)||G.test(z)),D=!/\s/.test(z)&&(!G.test(z)||/\s/.test(B)||G.test(B)),E=null,F=null;if(y%2&&(d.em||!C||"*"!==h&&D&&!G.test(z)?d.em!=h||!D||"*"!==h&&C&&!G.test(B)||(E=!1):E=!0),y>1&&(d.strong||!C||"*"!==h&&D&&!G.test(z)?d.strong!=h||!D||"*"!==h&&C&&!G.test(B)||(F=!1):F=!0),null!=F||null!=E){c.highlightFormatting&&(d.formatting=null==E?"strong":null==F?"em":"strong em"),E===!0&&(d.em=h),F===!0&&(d.strong=h);var r=l(d);return E===!1&&(d.em=!1),F===!1&&(d.strong=!1),r}}else if(" "===h&&(b.eat("*")||b.eat("_"))){if(" "===b.peek())return l(d);b.backUp(1)}if(c.strikethrough)if("~"===h&&b.eatWhile(h)){if(d.strikethrough){c.highlightFormatting&&(d.formatting="strikethrough");var r=l(d);return d.strikethrough=!1,r}if(b.match(/^[^\s]/,!1))return d.strikethrough=!0,c.highlightFormatting&&(d.formatting="strikethrough"),l(d)}else if(" "===h&&b.match(/^~~/,!0)){if(" "===b.peek())return l(d);b.backUp(2)}if(c.emoji&&":"===h&&b.match(/^[a-z_\d+-]+:/)){d.emoji=!0,c.highlightFormatting&&(d.formatting="emoji");var H=l(d);return d.emoji=!1,H}return" "===h&&(b.match(/ +$/,!1)?d.trailingSpace++:d.trailingSpace&&(d.trailingSpaceNewLine=!0)),l(d)}function o(a,b){var d=a.next();if(">"===d){b.f=b.inline=n,c.highlightFormatting&&(b.formatting="link");var e=l(b);return e?e+=" ":e="",e+w.linkInline}return a.match(/^[^>]+/,!0),w.linkInline}function p(a,b){if(a.eatSpace())return null;var d=a.next();return"("===d||"["===d?(b.f=b.inline=q("("===d?")":"]"),c.highlightFormatting&&(b.formatting="link-string"),b.linkHref=!0,l(b)):"error"}function q(a){return function(b,d){var e=b.next();if(e===a){d.f=d.inline=n,c.highlightFormatting&&(d.formatting="link-string");var f=l(d);return d.linkHref=!1,f}return b.match(I[a]),d.linkHref=!0,l(d)}}function r(a,b){return a.match(/^([^\]\\]|\\.)*\]:/,!1)?(b.f=s,a.next(),c.highlightFormatting&&(b.formatting="link"),b.linkText=!0,l(b)):e(a,b,n)}function s(a,b){if(a.match(/^\]:/,!0)){b.f=b.inline=t,c.highlightFormatting&&(b.formatting="link");var d=l(b);return b.linkText=!1,d}return a.match(/^([^\]\\]|\\.)+/,!0),w.linkText}function t(a,b){return a.eatSpace()?null:(a.match(/^[^\s]+/,!0),void 0===a.peek()?b.linkTitle=!0:a.match(/^(?:\s+(?:"(?:[^"\\]|\\\\|\\.)+"|'(?:[^'\\]|\\\\|\\.)+'|\((?:[^)\\]|\\\\|\\.)+\)))?/,!0),b.f=b.inline=n,w.linkHref+" url")}var u=a.getMode(b,"text/html"),v="null"==u.name;void 0===c.highlightFormatting&&(c.highlightFormatting=!1),void 0===c.maxBlockquoteDepth&&(c.maxBlockquoteDepth=0),void 0===c.taskLists&&(c.taskLists=!1),void 0===c.strikethrough&&(c.strikethrough=!1),void 0===c.emoji&&(c.emoji=!1),void 0===c.fencedCodeBlockHighlighting&&(c.fencedCodeBlockHighlighting=!0),void 0===c.xml&&(c.xml=!0),void 0===c.tokenTypeOverrides&&(c.tokenTypeOverrides={});var w={header:"header",code:"comment",quote:"quote",list1:"variable-2",list2:"variable-3",list3:"keyword",hr:"hr",image:"image",imageAltText:"image-alt-text",imageMarker:"image-marker",formatting:"formatting",linkInline:"link",linkEmail:"link",linkText:"link",linkHref:"string",em:"em",strong:"strong",strikethrough:"strikethrough",emoji:"builtin"};for(var x in w)w.hasOwnProperty(x)&&c.tokenTypeOverrides[x]&&(w[x]=c.tokenTypeOverrides[x]);var y=/^([*\-_])(?:\s*\1){2,}\s*$/,z=/^(?:[*\-+]|^[0-9]+([.)]))\s+/,A=/^\[(x| )\](?=\s)/i,B=c.allowAtxHeaderWithoutSpace?/^(#+)/:/^(#+)(?: |$)/,C=/^ *(?:\={1,}|-{1,})\s*$/,D=/^[^#!\[\]*_\\<>` "'(~:]+/,E=/^(~~~+|```+)[ \t]*([\w+#-]*)[^\n`]*$/,F=/^\s*\[[^\]]+?\]:\s*\S+(\s*\S*\s*)?$/,G=/[!\"#$%&\'()*+,\-\.\/:;<=>?@\[\\\]^_`{|}~\u2014]/,H=" ",I={")":/^(?:[^\\\(\)]|\\.|\((?:[^\\\(\)]|\\.)*\))*?(?=\))/,"]":/^(?:[^\\\[\]]|\\.|\[(?:[^\\\[\]]|\\.)*\])*?(?=\])/},J={startState:function(){return{f:i,prevLine:{stream:null},thisLine:{stream:null},block:i,htmlState:null,indentation:0,inline:n,text:m,formatting:!1,linkText:!1,linkHref:!1,linkTitle:!1,code:0,em:!1,strong:!1,header:0,setext:0,hr:!1,taskList:!1,list:!1,listStack:[],quote:0,trailingSpace:0,trailingSpaceNewLine:!1,strikethrough:!1,emoji:!1,fencedEndRE:null}},copyState:function(b){return{f:b.f,prevLine:b.prevLine,thisLine:b.thisLine,block:b.block,htmlState:b.htmlState&&a.copyState(u,b.htmlState),indentation:b.indentation,localMode:b.localMode,localState:b.localMode?a.copyState(b.localMode,b.localState):null,inline:b.inline,text:b.text,formatting:!1,linkText:b.linkText,linkTitle:b.linkTitle,code:b.code,em:b.em,strong:b.strong,strikethrough:b.strikethrough,emoji:b.emoji,header:b.header,setext:b.setext,hr:b.hr,taskList:b.taskList,list:b.list,listStack:b.listStack.slice(0),quote:b.quote,indentedCode:b.indentedCode,trailingSpace:b.trailingSpace,trailingSpaceNewLine:b.trailingSpaceNewLine,md_inside:b.md_inside,fencedEndRE:b.fencedEndRE}},token:function(a,b){if(b.formatting=!1,a!=b.thisLine.stream){if(b.header=0,b.hr=!1,a.match(/^\s*$/,!0))return h(b),null;if(b.prevLine=b.thisLine,b.thisLine={stream:a},b.taskList=!1,b.trailingSpace=0,b.trailingSpaceNewLine=!1,b.f=b.block,b.f!=j){var c=a.match(/^\s*/,!0)[0].replace(/\t/g,H).length;if(b.indentation=c,b.indentationDiff=null,c>0)return null}}return b.f(a,b)},innerMode:function(a){return a.block==j?{state:a.htmlState,mode:u}:a.localState?{state:a.localState,mode:a.localMode}:{state:a,mode:J}},indent:function(b,c,d){return b.block==j&&u.indent?u.indent(b.htmlState,c,d):b.localState&&b.localMode.indent?b.localMode.indent(b.localState,c,d):a.Pass},blankLine:h,getType:l,closeBrackets:"()[]{}''\"\"``",fold:"markdown"};return J},"xml"),a.defineMIME("text/x-markdown","markdown")})},{"../../lib/codemirror":59,"../meta":69,"../xml/xml":75}],69:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../lib/codemirror")):"function"==typeof define&&define.amd?define(["../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.modeInfo=[{name:"APL",mime:"text/apl",mode:"apl",ext:["dyalog","apl"]},{name:"PGP",mimes:["application/pgp","application/pgp-encrypted","application/pgp-keys","application/pgp-signature"],mode:"asciiarmor",ext:["asc","pgp","sig"]},{name:"ASN.1",mime:"text/x-ttcn-asn",mode:"asn.1",ext:["asn","asn1"]},{name:"Asterisk",mime:"text/x-asterisk",mode:"asterisk",file:/^extensions\.conf$/i},{name:"Brainfuck",mime:"text/x-brainfuck",mode:"brainfuck",ext:["b","bf"]},{name:"C",mime:"text/x-csrc",mode:"clike",ext:["c","h"]},{name:"C++",mime:"text/x-c++src",mode:"clike",ext:["cpp","c++","cc","cxx","hpp","h++","hh","hxx"],alias:["cpp"]},{name:"Cobol",mime:"text/x-cobol",mode:"cobol",ext:["cob","cpy"]},{name:"C#",mime:"text/x-csharp",mode:"clike",ext:["cs"],alias:["csharp"]},{name:"Clojure",mime:"text/x-clojure",mode:"clojure",ext:["clj","cljc","cljx"]},{name:"ClojureScript",mime:"text/x-clojurescript",mode:"clojure",ext:["cljs"]},{name:"Closure Stylesheets (GSS)",mime:"text/x-gss",mode:"css",ext:["gss"]},{name:"CMake",mime:"text/x-cmake",mode:"cmake",ext:["cmake","cmake.in"],file:/^CMakeLists.txt$/},{name:"CoffeeScript",mimes:["application/vnd.coffeescript","text/coffeescript","text/x-coffeescript"],mode:"coffeescript",ext:["coffee"],alias:["coffee","coffee-script"]},{name:"Common Lisp",mime:"text/x-common-lisp",mode:"commonlisp",ext:["cl","lisp","el"],alias:["lisp"]},{name:"Cypher",mime:"application/x-cypher-query",mode:"cypher",ext:["cyp","cypher"]},{name:"Cython",mime:"text/x-cython",mode:"python",ext:["pyx","pxd","pxi"]},{name:"Crystal",mime:"text/x-crystal",mode:"crystal",ext:["cr"]},{name:"CSS",mime:"text/css",mode:"css",ext:["css"]},{name:"CQL",mime:"text/x-cassandra",mode:"sql",ext:["cql"]},{name:"D",mime:"text/x-d",mode:"d",ext:["d"]},{name:"Dart",mimes:["application/dart","text/x-dart"],mode:"dart",ext:["dart"]},{name:"diff",mime:"text/x-diff",mode:"diff",ext:["diff","patch"]},{name:"Django",mime:"text/x-django",mode:"django"},{name:"Dockerfile",mime:"text/x-dockerfile",mode:"dockerfile",file:/^Dockerfile$/},{name:"DTD",mime:"application/xml-dtd",mode:"dtd",ext:["dtd"]},{name:"Dylan",mime:"text/x-dylan",mode:"dylan",ext:["dylan","dyl","intr"]},{name:"EBNF",mime:"text/x-ebnf",mode:"ebnf"},{name:"ECL",mime:"text/x-ecl",mode:"ecl",ext:["ecl"]},{name:"edn",mime:"application/edn",mode:"clojure",ext:["edn"]},{name:"Eiffel",mime:"text/x-eiffel",mode:"eiffel",ext:["e"]},{name:"Elm",mime:"text/x-elm",mode:"elm",ext:["elm"]},{name:"Embedded Javascript",mime:"application/x-ejs",mode:"htmlembedded",ext:["ejs"]},{name:"Embedded Ruby",mime:"application/x-erb",mode:"htmlembedded",ext:["erb"]},{name:"Erlang",mime:"text/x-erlang",mode:"erlang",ext:["erl"]},{name:"Factor",mime:"text/x-factor",mode:"factor",ext:["factor"]},{name:"FCL",mime:"text/x-fcl",mode:"fcl"},{name:"Forth",mime:"text/x-forth",mode:"forth",ext:["forth","fth","4th"]},{name:"Fortran",mime:"text/x-fortran",mode:"fortran",ext:["f","for","f77","f90"]},{name:"F#",mime:"text/x-fsharp",mode:"mllike",ext:["fs"],alias:["fsharp"]},{name:"Gas",mime:"text/x-gas",mode:"gas",ext:["s"]},{name:"Gherkin",mime:"text/x-feature",mode:"gherkin",ext:["feature"]},{name:"GitHub Flavored Markdown",mime:"text/x-gfm",mode:"gfm",file:/^(readme|contributing|history).md$/i},{name:"Go",mime:"text/x-go",mode:"go",ext:["go"]},{name:"Groovy",mime:"text/x-groovy",mode:"groovy",ext:["groovy","gradle"],file:/^Jenkinsfile$/},{name:"HAML",mime:"text/x-haml",mode:"haml",ext:["haml"]},{name:"Haskell",mime:"text/x-haskell",mode:"haskell",ext:["hs"]},{name:"Haskell (Literate)",mime:"text/x-literate-haskell",mode:"haskell-literate",ext:["lhs"]},{name:"Haxe",mime:"text/x-haxe",mode:"haxe",ext:["hx"]},{name:"HXML",mime:"text/x-hxml",mode:"haxe",ext:["hxml"]},{name:"ASP.NET",mime:"application/x-aspx",mode:"htmlembedded",ext:["aspx"],alias:["asp","aspx"]},{name:"HTML",mime:"text/html",mode:"htmlmixed",ext:["html","htm"],alias:["xhtml"]},{name:"HTTP",mime:"message/http",mode:"http"},{name:"IDL",mime:"text/x-idl",mode:"idl",ext:["pro"]},{name:"Pug",mime:"text/x-pug",mode:"pug",ext:["jade","pug"],alias:["jade"]},{name:"Java",mime:"text/x-java",mode:"clike",ext:["java"]},{name:"Java Server Pages",mime:"application/x-jsp",mode:"htmlembedded",ext:["jsp"],alias:["jsp"]},{name:"JavaScript",mimes:["text/javascript","text/ecmascript","application/javascript","application/x-javascript","application/ecmascript"],mode:"javascript",ext:["js"],alias:["ecmascript","js","node"]},{name:"JSON",mimes:["application/json","application/x-json"],mode:"javascript",ext:["json","map"],alias:["json5"]},{name:"JSON-LD",mime:"application/ld+json",mode:"javascript",ext:["jsonld"],alias:["jsonld"]},{name:"JSX",mime:"text/jsx",mode:"jsx",ext:["jsx"]},{name:"Jinja2",mime:"null",mode:"jinja2"},{name:"Julia",mime:"text/x-julia",mode:"julia",ext:["jl"]},{name:"Kotlin",mime:"text/x-kotlin",mode:"clike",ext:["kt"]},{name:"LESS",mime:"text/x-less",mode:"css",ext:["less"]},{name:"LiveScript",mime:"text/x-livescript",mode:"livescript",ext:["ls"],alias:["ls"]},{name:"Lua",mime:"text/x-lua",mode:"lua",ext:["lua"]},{name:"Markdown",mime:"text/x-markdown",mode:"markdown",ext:["markdown","md","mkd"]},{name:"mIRC",mime:"text/mirc",mode:"mirc"},{name:"MariaDB SQL",mime:"text/x-mariadb",mode:"sql"},{name:"Mathematica",mime:"text/x-mathematica",mode:"mathematica",ext:["m","nb"]},{name:"Modelica",mime:"text/x-modelica",mode:"modelica",ext:["mo"]},{name:"MUMPS",mime:"text/x-mumps",mode:"mumps",ext:["mps"]},{name:"MS SQL",mime:"text/x-mssql",mode:"sql"},{name:"mbox",mime:"application/mbox",mode:"mbox",ext:["mbox"]},{name:"MySQL",mime:"text/x-mysql",mode:"sql"},{name:"Nginx",mime:"text/x-nginx-conf",mode:"nginx",file:/nginx.*\.conf$/i},{name:"NSIS",mime:"text/x-nsis",mode:"nsis",ext:["nsh","nsi"]},{name:"NTriples",mimes:["application/n-triples","application/n-quads","text/n-triples"],mode:"ntriples",ext:["nt","nq"]},{name:"Objective C",mime:"text/x-objectivec",mode:"clike",ext:["m","mm"],alias:["objective-c","objc"]},{name:"OCaml",mime:"text/x-ocaml",mode:"mllike",ext:["ml","mli","mll","mly"]},{name:"Octave",mime:"text/x-octave",mode:"octave",ext:["m"]},{name:"Oz",mime:"text/x-oz",mode:"oz",ext:["oz"]},{name:"Pascal",mime:"text/x-pascal",mode:"pascal",ext:["p","pas"]},{name:"PEG.js",mime:"null",mode:"pegjs",ext:["jsonld"]},{name:"Perl",mime:"text/x-perl",mode:"perl",ext:["pl","pm"]},{name:"PHP",mime:"application/x-httpd-php",mode:"php",ext:["php","php3","php4","php5","php7","phtml"]},{name:"Pig",mime:"text/x-pig",mode:"pig",ext:["pig"]},{name:"Plain Text",mime:"text/plain",mode:"null",ext:["txt","text","conf","def","list","log"]},{name:"PLSQL",mime:"text/x-plsql",mode:"sql",ext:["pls"]},{name:"PowerShell",mime:"application/x-powershell",mode:"powershell",ext:["ps1","psd1","psm1"]},{name:"Properties files",mime:"text/x-properties",mode:"properties",ext:["properties","ini","in"],alias:["ini","properties"]},{name:"ProtoBuf",mime:"text/x-protobuf",mode:"protobuf",ext:["proto"]},{name:"Python",mime:"text/x-python",mode:"python",ext:["BUILD","bzl","py","pyw"],file:/^(BUCK|BUILD)$/},{name:"Puppet",mime:"text/x-puppet",mode:"puppet",ext:["pp"]},{name:"Q",mime:"text/x-q",mode:"q",ext:["q"]},{name:"R",mime:"text/x-rsrc",mode:"r",ext:["r","R"],alias:["rscript"]},{name:"reStructuredText",mime:"text/x-rst",mode:"rst",ext:["rst"],alias:["rst"]},{name:"RPM Changes",mime:"text/x-rpm-changes",mode:"rpm"},{name:"RPM Spec",mime:"text/x-rpm-spec",mode:"rpm",ext:["spec"]},{name:"Ruby",mime:"text/x-ruby",mode:"ruby",ext:["rb"],alias:["jruby","macruby","rake","rb","rbx"]},{name:"Rust",mime:"text/x-rustsrc",mode:"rust",ext:["rs"]},{name:"SAS",mime:"text/x-sas",mode:"sas",ext:["sas"]},{name:"Sass",mime:"text/x-sass",mode:"sass",ext:["sass"]},{name:"Scala",mime:"text/x-scala",mode:"clike",ext:["scala"]},{name:"Scheme",mime:"text/x-scheme",mode:"scheme",ext:["scm","ss"]},{name:"SCSS",mime:"text/x-scss",mode:"css",ext:["scss"]},{name:"Shell",mimes:["text/x-sh","application/x-sh"],mode:"shell",ext:["sh","ksh","bash"],alias:["bash","sh","zsh"],file:/^PKGBUILD$/},{name:"Sieve",mime:"application/sieve",mode:"sieve",ext:["siv","sieve"]},{name:"Slim",mimes:["text/x-slim","application/x-slim"],mode:"slim",ext:["slim"]},{name:"Smalltalk",mime:"text/x-stsrc",mode:"smalltalk",ext:["st"]},{name:"Smarty",mime:"text/x-smarty",mode:"smarty",ext:["tpl"]},{name:"Solr",mime:"text/x-solr",mode:"solr"},{name:"Soy",mime:"text/x-soy",mode:"soy",ext:["soy"],alias:["closure template"]},{name:"SPARQL",mime:"application/sparql-query",mode:"sparql",ext:["rq","sparql"],alias:["sparul"]},{name:"Spreadsheet",mime:"text/x-spreadsheet",mode:"spreadsheet",alias:["excel","formula"]},{name:"SQL",mime:"text/x-sql",mode:"sql",ext:["sql"]},{name:"SQLite",mime:"text/x-sqlite",mode:"sql"},{name:"Squirrel",mime:"text/x-squirrel",mode:"clike",ext:["nut"]},{name:"Stylus",mime:"text/x-styl",mode:"stylus",ext:["styl"]},{name:"Swift",mime:"text/x-swift",mode:"swift",ext:["swift"]},{name:"sTeX",mime:"text/x-stex",mode:"stex"},{name:"LaTeX",mime:"text/x-latex",mode:"stex",ext:["text","ltx"],alias:["tex"]},{name:"SystemVerilog",mime:"text/x-systemverilog",mode:"verilog",ext:["v","sv","svh"]},{name:"Tcl",mime:"text/x-tcl",mode:"tcl",ext:["tcl"]},{name:"Textile",mime:"text/x-textile",mode:"textile",ext:["textile"]},{name:"TiddlyWiki ",mime:"text/x-tiddlywiki",mode:"tiddlywiki"},{name:"Tiki wiki",mime:"text/tiki",mode:"tiki"},{name:"TOML",mime:"text/x-toml",mode:"toml",ext:["toml"]},{name:"Tornado",mime:"text/x-tornado",mode:"tornado"},{name:"troff",mime:"text/troff",mode:"troff",ext:["1","2","3","4","5","6","7","8","9"]},{name:"TTCN",mime:"text/x-ttcn",mode:"ttcn",ext:["ttcn","ttcn3","ttcnpp"]},{name:"TTCN_CFG",mime:"text/x-ttcn-cfg",mode:"ttcn-cfg",ext:["cfg"]},{name:"Turtle",mime:"text/turtle",mode:"turtle",ext:["ttl"]},{name:"TypeScript",mime:"application/typescript",mode:"javascript",ext:["ts"],alias:["ts"]},{name:"TypeScript-JSX",mime:"text/typescript-jsx",mode:"jsx",ext:["tsx"],alias:["tsx"]},{name:"Twig",mime:"text/x-twig",mode:"twig"},{name:"Web IDL",mime:"text/x-webidl",mode:"webidl",ext:["webidl"]},{name:"VB.NET",mime:"text/x-vb",mode:"vb",ext:["vb"]},{name:"VBScript",mime:"text/vbscript",mode:"vbscript",ext:["vbs"]},{name:"Velocity",mime:"text/velocity",mode:"velocity",ext:["vtl"]},{name:"Verilog",mime:"text/x-verilog",mode:"verilog",ext:["v"]},{name:"VHDL",mime:"text/x-vhdl",mode:"vhdl",ext:["vhd","vhdl"]},{name:"Vue.js Component",mimes:["script/x-vue","text/x-vue"],mode:"vue",ext:["vue"]},{name:"XML",mimes:["application/xml","text/xml"],mode:"xml",ext:["xml","xsl","xsd","svg"],alias:["rss","wsdl","xsd"]},{name:"XQuery",mime:"application/xquery",mode:"xquery",ext:["xy","xquery"]},{name:"Yacas",mime:"text/x-yacas",mode:"yacas",ext:["ys"]},{name:"YAML",mimes:["text/x-yaml","text/yaml"],mode:"yaml",ext:["yaml","yml"],alias:["yml"]},{name:"Z80",mime:"text/x-z80",mode:"z80",ext:["z80"]},{name:"mscgen",mime:"text/x-mscgen",mode:"mscgen",ext:["mscgen","mscin","msc"]},{name:"xu",mime:"text/x-xu",mode:"mscgen",ext:["xu"]},{name:"msgenny",mime:"text/x-msgenny",mode:"mscgen",ext:["msgenny"]}];for(var b=0;b-1&&b.substring(e+1,b.length);if(f)return a.findModeByExtension(f)},a.findModeByName=function(b){b=b.toLowerCase();for(var c=0;c*\/]/.test(h)?c(null,"select-op"):/[;{}:\[\]]/.test(h)?c(null,h):(a.eatWhile(/[\w\\\-]/),c("variable","variable")):c(null,"compare"):void c(null,"compare")}function e(a,b){for(var e,f=!1;null!=(e=a.next());){if(f&&"/"==e){b.tokenize=d;break}f="*"==e}return c("comment","comment")}function f(a,b){for(var e,f=0;null!=(e=a.next());){if(f>=2&&">"==e){b.tokenize=d;break}f="-"==e?f+1:0}return c("comment","comment")}function g(a){return function(b,e){for(var f,g=!1;null!=(f=b.next())&&(f!=a||g);)g=!g&&"\\"==f;return g||(e.tokenize=d),c("string","string")}}var h,i=b("break return rewrite set accept_mutex accept_mutex_delay access_log add_after_body add_before_body add_header addition_types aio alias allow ancient_browser ancient_browser_value auth_basic auth_basic_user_file auth_http auth_http_header auth_http_timeout autoindex autoindex_exact_size autoindex_localtime charset charset_types client_body_buffer_size client_body_in_file_only client_body_in_single_buffer client_body_temp_path client_body_timeout client_header_buffer_size client_header_timeout client_max_body_size connection_pool_size create_full_put_path daemon dav_access dav_methods debug_connection debug_points default_type degradation degrade deny devpoll_changes devpoll_events directio directio_alignment empty_gif env epoll_events error_log eventport_events expires fastcgi_bind fastcgi_buffer_size fastcgi_buffers fastcgi_busy_buffers_size fastcgi_cache fastcgi_cache_key fastcgi_cache_methods fastcgi_cache_min_uses fastcgi_cache_path fastcgi_cache_use_stale fastcgi_cache_valid fastcgi_catch_stderr fastcgi_connect_timeout fastcgi_hide_header fastcgi_ignore_client_abort fastcgi_ignore_headers fastcgi_index fastcgi_intercept_errors fastcgi_max_temp_file_size fastcgi_next_upstream fastcgi_param fastcgi_pass_header fastcgi_pass_request_body fastcgi_pass_request_headers fastcgi_read_timeout fastcgi_send_lowat fastcgi_send_timeout fastcgi_split_path_info fastcgi_store fastcgi_store_access fastcgi_temp_file_write_size fastcgi_temp_path fastcgi_upstream_fail_timeout fastcgi_upstream_max_fails flv geoip_city geoip_country google_perftools_profiles gzip gzip_buffers gzip_comp_level gzip_disable gzip_hash gzip_http_version gzip_min_length gzip_no_buffer gzip_proxied gzip_static gzip_types gzip_vary gzip_window if_modified_since ignore_invalid_headers image_filter image_filter_buffer image_filter_jpeg_quality image_filter_transparency imap_auth imap_capabilities imap_client_buffer index ip_hash keepalive_requests keepalive_timeout kqueue_changes kqueue_events large_client_header_buffers limit_conn limit_conn_log_level limit_rate limit_rate_after limit_req limit_req_log_level limit_req_zone limit_zone lingering_time lingering_timeout lock_file log_format log_not_found log_subrequest map_hash_bucket_size map_hash_max_size master_process memcached_bind memcached_buffer_size memcached_connect_timeout memcached_next_upstream memcached_read_timeout memcached_send_timeout memcached_upstream_fail_timeout memcached_upstream_max_fails merge_slashes min_delete_depth modern_browser modern_browser_value msie_padding msie_refresh multi_accept open_file_cache open_file_cache_errors open_file_cache_events open_file_cache_min_uses open_file_cache_valid open_log_file_cache output_buffers override_charset perl perl_modules perl_require perl_set pid pop3_auth pop3_capabilities port_in_redirect postpone_gzipping postpone_output protocol proxy proxy_bind proxy_buffer proxy_buffer_size proxy_buffering proxy_buffers proxy_busy_buffers_size proxy_cache proxy_cache_key proxy_cache_methods proxy_cache_min_uses proxy_cache_path proxy_cache_use_stale proxy_cache_valid proxy_connect_timeout proxy_headers_hash_bucket_size proxy_headers_hash_max_size proxy_hide_header proxy_ignore_client_abort proxy_ignore_headers proxy_intercept_errors proxy_max_temp_file_size proxy_method proxy_next_upstream proxy_pass_error_message proxy_pass_header proxy_pass_request_body proxy_pass_request_headers proxy_read_timeout proxy_redirect proxy_send_lowat proxy_send_timeout proxy_set_body proxy_set_header proxy_ssl_session_reuse proxy_store proxy_store_access proxy_temp_file_write_size proxy_temp_path proxy_timeout proxy_upstream_fail_timeout proxy_upstream_max_fails random_index read_ahead real_ip_header recursive_error_pages request_pool_size reset_timedout_connection resolver resolver_timeout rewrite_log rtsig_overflow_events rtsig_overflow_test rtsig_overflow_threshold rtsig_signo satisfy secure_link_secret send_lowat send_timeout sendfile sendfile_max_chunk server_name_in_redirect server_names_hash_bucket_size server_names_hash_max_size server_tokens set_real_ip_from smtp_auth smtp_capabilities smtp_client_buffer smtp_greeting_delay so_keepalive source_charset ssi ssi_ignore_recycled_buffers ssi_min_file_chunk ssi_silent_errors ssi_types ssi_value_length ssl ssl_certificate ssl_certificate_key ssl_ciphers ssl_client_certificate ssl_crl ssl_dhparam ssl_engine ssl_prefer_server_ciphers ssl_protocols ssl_session_cache ssl_session_timeout ssl_verify_client ssl_verify_depth starttls stub_status sub_filter sub_filter_once sub_filter_types tcp_nodelay tcp_nopush thread_stack_size timeout timer_resolution types_hash_bucket_size types_hash_max_size underscores_in_headers uninitialized_variable_warn use user userid userid_domain userid_expires userid_mark userid_name userid_p3p userid_path userid_service valid_referers variables_hash_bucket_size variables_hash_max_size worker_connections worker_cpu_affinity worker_priority worker_processes worker_rlimit_core worker_rlimit_nofile worker_rlimit_sigpending worker_threads working_directory xclient xml_entities xslt_stylesheet xslt_typesdrew@li229-23"),j=b("http mail events server types location upstream charset_map limit_except if geo map"),k=b("include root server server_name listen internal proxy_pass memcached_pass fastcgi_pass try_files"),l=a.indentUnit;return{startState:function(a){return{tokenize:d,baseIndent:a||0,stack:[]}},token:function(a,b){if(a.eatSpace())return null;h=null;var c=b.tokenize(a,b),d=b.stack[b.stack.length-1];return"hash"==h&&"rule"==d?c="atom":"variable"==c&&("rule"==d?c="number":d&&"@media{"!=d||(c="tag")),"rule"==d&&/^[\{\};]$/.test(h)&&b.stack.pop(),"{"==h?"@media"==d?b.stack[b.stack.length-1]="@media{":b.stack.push("{"):"}"==h?b.stack.pop():"@media"==h?b.stack.push("@media"):"{"==d&&"comment"!=h&&b.stack.push("rule"),c},indent:function(a,b){var c=a.stack.length;return/^\}/.test(b)&&(c-="rule"==a.stack[a.stack.length-1]?2:1),a.baseIndent+c*l},electricChars:"}"}}),a.defineMIME("text/x-nginx-conf","nginx")})},{"../../lib/codemirror":59}],71:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../htmlmixed/htmlmixed"),a("../clike/clike")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../htmlmixed/htmlmixed","../clike/clike"],d):d(CodeMirror)}(function(a){"use strict";function b(a){for(var b={},c=a.split(" "),d=0;d\w/,!1)&&(b.tokenize=c([[["->",null]],[[/[\w]+/,"variable"]]],d,e)),"variable-2";for(var f=!1;!a.eol()&&(f||e===!1||!a.match("{$",!1)&&!a.match(/^(\$[a-zA-Z_][a-zA-Z0-9_]*|\$\{)/,!1));){if(!f&&a.match(d)){b.tokenize=null,b.tokStack.pop(),b.tokStack.pop();break}f="\\"==a.next()&&!f}return"string"}var f="abstract and array as break case catch class clone const continue declare default do else elseif enddeclare endfor endforeach endif endswitch endwhile extends final for foreach function global goto if implements interface instanceof namespace new or private protected public static switch throw trait try use var while xor die echo empty exit eval include include_once isset list require require_once return print unset __halt_compiler self static parent yield insteadof finally",g="true false null TRUE FALSE NULL __CLASS__ __DIR__ __FILE__ __LINE__ __METHOD__ __FUNCTION__ __NAMESPACE__ __TRAIT__",h="func_num_args func_get_arg func_get_args strlen strcmp strncmp strcasecmp strncasecmp each error_reporting define defined trigger_error user_error set_error_handler restore_error_handler get_declared_classes get_loaded_extensions extension_loaded get_extension_funcs debug_backtrace constant bin2hex hex2bin sleep usleep time mktime gmmktime strftime gmstrftime strtotime date gmdate getdate localtime checkdate flush wordwrap htmlspecialchars htmlentities html_entity_decode md5 md5_file crc32 getimagesize image_type_to_mime_type phpinfo phpversion phpcredits strnatcmp strnatcasecmp substr_count strspn strcspn strtok strtoupper strtolower strpos strrpos strrev hebrev hebrevc nl2br basename dirname pathinfo stripslashes stripcslashes strstr stristr strrchr str_shuffle str_word_count strcoll substr substr_replace quotemeta ucfirst ucwords strtr addslashes addcslashes rtrim str_replace str_repeat count_chars chunk_split trim ltrim strip_tags similar_text explode implode setlocale localeconv parse_str str_pad chop strchr sprintf printf vprintf vsprintf sscanf fscanf parse_url urlencode urldecode rawurlencode rawurldecode readlink linkinfo link unlink exec system escapeshellcmd escapeshellarg passthru shell_exec proc_open proc_close rand srand getrandmax mt_rand mt_srand mt_getrandmax base64_decode base64_encode abs ceil floor round is_finite is_nan is_infinite bindec hexdec octdec decbin decoct dechex base_convert number_format fmod ip2long long2ip getenv putenv getopt microtime gettimeofday getrusage uniqid quoted_printable_decode set_time_limit get_cfg_var magic_quotes_runtime set_magic_quotes_runtime get_magic_quotes_gpc get_magic_quotes_runtime import_request_variables error_log serialize unserialize memory_get_usage var_dump var_export debug_zval_dump print_r highlight_file show_source highlight_string ini_get ini_get_all ini_set ini_alter ini_restore get_include_path set_include_path restore_include_path setcookie header headers_sent connection_aborted connection_status ignore_user_abort parse_ini_file is_uploaded_file move_uploaded_file intval floatval doubleval strval gettype settype is_null is_resource is_bool is_long is_float is_int is_integer is_double is_real is_numeric is_string is_array is_object is_scalar ereg ereg_replace eregi eregi_replace split spliti join sql_regcase dl pclose popen readfile rewind rmdir umask fclose feof fgetc fgets fgetss fread fopen fpassthru ftruncate fstat fseek ftell fflush fwrite fputs mkdir rename copy tempnam tmpfile file file_get_contents file_put_contents stream_select stream_context_create stream_context_set_params stream_context_set_option stream_context_get_options stream_filter_prepend stream_filter_append fgetcsv flock get_meta_tags stream_set_write_buffer set_file_buffer set_socket_blocking stream_set_blocking socket_set_blocking stream_get_meta_data stream_register_wrapper stream_wrapper_register stream_set_timeout socket_set_timeout socket_get_status realpath fnmatch fsockopen pfsockopen pack unpack get_browser crypt opendir closedir chdir getcwd rewinddir readdir dir glob fileatime filectime filegroup fileinode filemtime fileowner fileperms filesize filetype file_exists is_writable is_writeable is_readable is_executable is_file is_dir is_link stat lstat chown touch clearstatcache mail ob_start ob_flush ob_clean ob_end_flush ob_end_clean ob_get_flush ob_get_clean ob_get_length ob_get_level ob_get_status ob_get_contents ob_implicit_flush ob_list_handlers ksort krsort natsort natcasesort asort arsort sort rsort usort uasort uksort shuffle array_walk count end prev next reset current key min max in_array array_search extract compact array_fill range array_multisort array_push array_pop array_shift array_unshift array_splice array_slice array_merge array_merge_recursive array_keys array_values array_count_values array_reverse array_reduce array_pad array_flip array_change_key_case array_rand array_unique array_intersect array_intersect_assoc array_diff array_diff_assoc array_sum array_filter array_map array_chunk array_key_exists array_intersect_key array_combine array_column pos sizeof key_exists assert assert_options version_compare ftok str_rot13 aggregate session_name session_module_name session_save_path session_id session_regenerate_id session_decode session_register session_unregister session_is_registered session_encode session_start session_destroy session_unset session_set_save_handler session_cache_limiter session_cache_expire session_set_cookie_params session_get_cookie_params session_write_close preg_match preg_match_all preg_replace preg_replace_callback preg_split preg_quote preg_grep overload ctype_alnum ctype_alpha ctype_cntrl ctype_digit ctype_lower ctype_graph ctype_print ctype_punct ctype_space ctype_upper ctype_xdigit virtual apache_request_headers apache_note apache_lookup_uri apache_child_terminate apache_setenv apache_response_headers apache_get_version getallheaders mysql_connect mysql_pconnect mysql_close mysql_select_db mysql_create_db mysql_drop_db mysql_query mysql_unbuffered_query mysql_db_query mysql_list_dbs mysql_list_tables mysql_list_fields mysql_list_processes mysql_error mysql_errno mysql_affected_rows mysql_insert_id mysql_result mysql_num_rows mysql_num_fields mysql_fetch_row mysql_fetch_array mysql_fetch_assoc mysql_fetch_object mysql_data_seek mysql_fetch_lengths mysql_fetch_field mysql_field_seek mysql_free_result mysql_field_name mysql_field_table mysql_field_len mysql_field_type mysql_field_flags mysql_escape_string mysql_real_escape_string mysql_stat mysql_thread_id mysql_client_encoding mysql_get_client_info mysql_get_host_info mysql_get_proto_info mysql_get_server_info mysql_info mysql mysql_fieldname mysql_fieldtable mysql_fieldlen mysql_fieldtype mysql_fieldflags mysql_selectdb mysql_createdb mysql_dropdb mysql_freeresult mysql_numfields mysql_numrows mysql_listdbs mysql_listtables mysql_listfields mysql_db_name mysql_dbname mysql_tablename mysql_table_name pg_connect pg_pconnect pg_close pg_connection_status pg_connection_busy pg_connection_reset pg_host pg_dbname pg_port pg_tty pg_options pg_ping pg_query pg_send_query pg_cancel_query pg_fetch_result pg_fetch_row pg_fetch_assoc pg_fetch_array pg_fetch_object pg_fetch_all pg_affected_rows pg_get_result pg_result_seek pg_result_status pg_free_result pg_last_oid pg_num_rows pg_num_fields pg_field_name pg_field_num pg_field_size pg_field_type pg_field_prtlen pg_field_is_null pg_get_notify pg_get_pid pg_result_error pg_last_error pg_last_notice pg_put_line pg_end_copy pg_copy_to pg_copy_from pg_trace pg_untrace pg_lo_create pg_lo_unlink pg_lo_open pg_lo_close pg_lo_read pg_lo_write pg_lo_read_all pg_lo_import pg_lo_export pg_lo_seek pg_lo_tell pg_escape_string pg_escape_bytea pg_unescape_bytea pg_client_encoding pg_set_client_encoding pg_meta_data pg_convert pg_insert pg_update pg_delete pg_select pg_exec pg_getlastoid pg_cmdtuples pg_errormessage pg_numrows pg_numfields pg_fieldname pg_fieldsize pg_fieldtype pg_fieldnum pg_fieldprtlen pg_fieldisnull pg_freeresult pg_result pg_loreadall pg_locreate pg_lounlink pg_loopen pg_loclose pg_loread pg_lowrite pg_loimport pg_loexport http_response_code get_declared_traits getimagesizefromstring socket_import_stream stream_set_chunk_size trait_exists header_register_callback class_uses session_status session_register_shutdown echo print global static exit array empty eval isset unset die include require include_once require_once json_decode json_encode json_last_error json_last_error_msg curl_close curl_copy_handle curl_errno curl_error curl_escape curl_exec curl_file_create curl_getinfo curl_init curl_multi_add_handle curl_multi_close curl_multi_exec curl_multi_getcontent curl_multi_info_read curl_multi_init curl_multi_remove_handle curl_multi_select curl_multi_setopt curl_multi_strerror curl_pause curl_reset curl_setopt_array curl_setopt curl_share_close curl_share_init curl_share_setopt curl_strerror curl_unescape curl_version mysqli_affected_rows mysqli_autocommit mysqli_change_user mysqli_character_set_name mysqli_close mysqli_commit mysqli_connect_errno mysqli_connect_error mysqli_connect mysqli_data_seek mysqli_debug mysqli_dump_debug_info mysqli_errno mysqli_error_list mysqli_error mysqli_fetch_all mysqli_fetch_array mysqli_fetch_assoc mysqli_fetch_field_direct mysqli_fetch_field mysqli_fetch_fields mysqli_fetch_lengths mysqli_fetch_object mysqli_fetch_row mysqli_field_count mysqli_field_seek mysqli_field_tell mysqli_free_result mysqli_get_charset mysqli_get_client_info mysqli_get_client_stats mysqli_get_client_version mysqli_get_connection_stats mysqli_get_host_info mysqli_get_proto_info mysqli_get_server_info mysqli_get_server_version mysqli_info mysqli_init mysqli_insert_id mysqli_kill mysqli_more_results mysqli_multi_query mysqli_next_result mysqli_num_fields mysqli_num_rows mysqli_options mysqli_ping mysqli_prepare mysqli_query mysqli_real_connect mysqli_real_escape_string mysqli_real_query mysqli_reap_async_query mysqli_refresh mysqli_rollback mysqli_select_db mysqli_set_charset mysqli_set_local_infile_default mysqli_set_local_infile_handler mysqli_sqlstate mysqli_ssl_set mysqli_stat mysqli_stmt_init mysqli_store_result mysqli_thread_id mysqli_thread_safe mysqli_use_result mysqli_warning_count"; -a.registerHelper("hintWords","php",[f,g,h].join(" ").split(" ")),a.registerHelper("wordChars","php",/[\w$]/);var i={name:"clike",helperType:"php",keywords:b(f),blockKeywords:b("catch do else elseif for foreach if switch try while finally"),defKeywords:b("class function interface namespace trait"),atoms:b(g),builtin:b(h),multiLineStrings:!0,hooks:{$:function(a){return a.eatWhile(/[\w\$_]/),"variable-2"},"<":function(a,b){var c;if(c=a.match(/<<\s*/)){var e=a.eat(/['"]/);a.eatWhile(/[\w\.]/);var f=a.current().slice(c[0].length+(e?2:1));if(e&&a.eat(e),f)return(b.tokStack||(b.tokStack=[])).push(f,0),b.tokenize=d(f,"'"!=e),"string"}return!1},"#":function(a){for(;!a.eol()&&!a.match("?>",!1);)a.next();return"comment"},"/":function(a){if(a.eat("/")){for(;!a.eol()&&!a.match("?>",!1);)a.next();return"comment"}return!1},'"':function(a,b){return(b.tokStack||(b.tokStack=[])).push('"',0),b.tokenize=d('"'),"string"},"{":function(a,b){return b.tokStack&&b.tokStack.length&&b.tokStack[b.tokStack.length-1]++,!1},"}":function(a,b){return b.tokStack&&b.tokStack.length>0&&!--b.tokStack[b.tokStack.length-1]&&(b.tokenize=d(b.tokStack[b.tokStack.length-2])),!1}}};a.defineMode("php",function(b,c){function d(b,c){var d=c.curMode==f;if(b.sol()&&c.pending&&'"'!=c.pending&&"'"!=c.pending&&(c.pending=null),d)return d&&null==c.php.tokenize&&b.match("?>")?(c.curMode=e,c.curState=c.html,c.php.context.prev||(c.php=null),"meta"):f.token(b,c.curState);if(b.match(/^<\?\w*/))return c.curMode=f,c.php||(c.php=a.startState(f,e.indent(c.html,""))),c.curState=c.php,"meta";if('"'==c.pending||"'"==c.pending){for(;!b.eol()&&b.next()!=c.pending;);var g="string"}else if(c.pending&&b.pos/.test(i)?c.pending=h[0]:c.pending={end:b.pos,style:g},b.backUp(i.length-j)),g}var e=a.getMode(b,"text/html"),f=a.getMode(b,i);return{startState:function(){var b=a.startState(e),d=c.startOpen?a.startState(f):null;return{html:b,php:d,curMode:c.startOpen?f:e,curState:c.startOpen?d:b,pending:null}},copyState:function(b){var c,d=b.html,g=a.copyState(e,d),h=b.php,i=h&&a.copyState(f,h);return c=b.curMode==e?g:i,{html:g,php:i,curMode:b.curMode,curState:c,pending:b.pending}},token:d,indent:function(a,b){return a.curMode!=f&&/^\s*<\//.test(b)||a.curMode==f&&/^\?>/.test(b)?e.indent(a.html,b):a.curMode.indent(a.curState,b)},blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:"//",innerMode:function(a){return{state:a.curState,mode:a.curMode}}}},"htmlmixed","clike"),a.defineMIME("application/x-httpd-php","php"),a.defineMIME("application/x-httpd-php-open",{name:"php",startOpen:!0}),a.defineMIME("text/x-php",i)})},{"../../lib/codemirror":59,"../clike/clike":60,"../htmlmixed/htmlmixed":64}],72:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror"),a("../css/css")):"function"==typeof define&&define.amd?define(["../../lib/codemirror","../css/css"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("sass",function(b){function c(a){return new RegExp("^"+a.join("|"))}function d(a){return!a.peek()||a.match(/\s+$/,!1)}function e(a,b){var c=a.peek();return")"===c?(a.next(),b.tokenizer=k,"operator"):"("===c?(a.next(),a.eatSpace(),"operator"):"'"===c||'"'===c?(b.tokenizer=g(a.next()),"string"):(b.tokenizer=g(")",!1),"string")}function f(a,b){return function(c,d){return c.sol()&&c.indentation()<=a?(d.tokenizer=k,k(c,d)):(b&&c.skipTo("*/")?(c.next(),c.next(),d.tokenizer=k):c.skipToEnd(),"comment")}}function g(a,b){function c(e,f){var g=e.next(),i=e.peek(),j=e.string.charAt(e.pos-2),l="\\"!==g&&i===a||g===a&&"\\"!==j;return l?(g!==a&&b&&e.next(),d(e)&&(f.cursorHalf=0),f.tokenizer=k,"string"):"#"===g&&"{"===i?(f.tokenizer=h(c),e.next(),"operator"):"string"}return null==b&&(b=!0),c}function h(a){return function(b,c){return"}"===b.peek()?(b.next(),c.tokenizer=a,"operator"):k(b,c)}}function i(a){if(0==a.indentCount){a.indentCount++;var c=a.scopes[0].offset,d=c+b.indentUnit;a.scopes.unshift({offset:d})}}function j(a){1!=a.scopes.length&&a.scopes.shift()}function k(a,b){var c=a.peek();if(a.match("/*"))return b.tokenizer=f(a.indentation(),!0),b.tokenizer(a,b);if(a.match("//"))return b.tokenizer=f(a.indentation(),!1),b.tokenizer(a,b);if(a.match("#{"))return b.tokenizer=h(k),"operator";if('"'===c||"'"===c)return a.next(),b.tokenizer=g(c),"string";if(b.cursorHalf){if("#"===c&&(a.next(),a.match(/[0-9a-fA-F]{6}|[0-9a-fA-F]{3}/)))return d(a)&&(b.cursorHalf=0),"number";if(a.match(/^-?[0-9\.]+/))return d(a)&&(b.cursorHalf=0),"number";if(a.match(/^(px|em|in)\b/))return d(a)&&(b.cursorHalf=0),"unit";if(a.match(t))return d(a)&&(b.cursorHalf=0),"keyword";if(a.match(/^url/)&&"("===a.peek())return b.tokenizer=e,d(a)&&(b.cursorHalf=0),"atom";if("$"===c)return a.next(),a.eatWhile(/[\w-]/),d(a)&&(b.cursorHalf=0),"variable-2";if("!"===c)return a.next(),b.cursorHalf=0,a.match(/^[\w]+/)?"keyword":"operator";if(a.match(v))return d(a)&&(b.cursorHalf=0),"operator";if(a.eatWhile(/[\w-]/))return d(a)&&(b.cursorHalf=0),m=a.current().toLowerCase(),q.hasOwnProperty(m)?"atom":p.hasOwnProperty(m)?"keyword":o.hasOwnProperty(m)?(b.prevProp=a.current().toLowerCase(),"property"):"tag";if(d(a))return b.cursorHalf=0,null}else{if("-"===c&&a.match(/^-\w+-/))return"meta";if("."===c){if(a.next(),a.match(/^[\w-]+/))return i(b),"qualifier";if("#"===a.peek())return i(b),"tag"}if("#"===c){if(a.next(),a.match(/^[\w-]+/))return i(b),"builtin";if("#"===a.peek())return i(b),"tag"}if("$"===c)return a.next(),a.eatWhile(/[\w-]/),"variable-2";if(a.match(/^-?[0-9\.]+/))return"number";if(a.match(/^(px|em|in)\b/))return"unit";if(a.match(t))return"keyword";if(a.match(/^url/)&&"("===a.peek())return b.tokenizer=e,"atom";if("="===c&&a.match(/^=[\w-]+/))return i(b),"meta";if("+"===c&&a.match(/^\+[\w-]+/))return"variable-3";if("@"===c&&a.match(/@extend/)&&(a.match(/\s*[\w]/)||j(b)),a.match(/^@(else if|if|media|else|for|each|while|mixin|function)/))return i(b),"def";if("@"===c)return a.next(),a.eatWhile(/[\w-]/),"def";if(a.eatWhile(/[\w-]/)){if(a.match(/ *: *[\w-\+\$#!\("']/,!1)){m=a.current().toLowerCase();var l=b.prevProp+"-"+m;return o.hasOwnProperty(l)?"property":o.hasOwnProperty(m)?(b.prevProp=m,"property"):r.hasOwnProperty(m)?"property":"tag"}return a.match(/ *:/,!1)?(i(b),b.cursorHalf=1,b.prevProp=a.current().toLowerCase(),"property"):a.match(/ *,/,!1)?"tag":(i(b),"tag")}if(":"===c)return a.match(w)?"variable-3":(a.next(),b.cursorHalf=1,"operator")}return a.match(v)?"operator":(a.next(),null)}function l(a,c){a.sol()&&(c.indentCount=0);var d=c.tokenizer(a,c),e=a.current();if("@return"!==e&&"}"!==e||j(c),null!==d){for(var f=a.pos-e.length,g=f+b.indentUnit*c.indentCount,h=[],i=0;i","<","==",">=","<=","\\+","-","\\!=","/","\\*","%","and","or","not",";","\\{","\\}",":"],v=c(u),w=/^::?[a-zA-Z_][\w\-]*/;return{startState:function(){return{tokenizer:k,scopes:[{offset:0,type:"sass"}],indentCount:0,cursorHalf:0,definedVars:[],definedMixins:[]}},token:function(a,b){var c=l(a,b);return b.lastToken={style:c,content:a.current()},c},indent:function(a){return a.scopes[0].offset}}},"css"),a.defineMIME("text/x-sass","sass")})},{"../../lib/codemirror":59,"../css/css":61}],73:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("shell",function(){function a(a,b){for(var c=b.split(" "),d=0;d1&&a.eat("$");var e=a.next();return/['"({]/.test(e)?(b.tokens[0]=c(e,"("==e?"quote":"{"==e?"def":"string"),d(a,b)):(/\d/.test(e)||a.eatWhile(/\w/),b.tokens.shift(),"def")};return{startState:function(){return{tokens:[]}},token:function(a,b){return d(a,b)},closeBrackets:"()[]{}''\"\"``",lineComment:"#",fold:"brace"}}),a.defineMIME("text/x-sh","shell"),a.defineMIME("application/x-sh","shell")})},{"../../lib/codemirror":59}],74:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("sql",function(b,c){function d(a,b){var c=a.next();if(o[c]){var d=o[c](a,b);if(d!==!1)return d}if(n.hexNumber&&("0"==c&&a.match(/^[xX][0-9a-fA-F]+/)||("x"==c||"X"==c)&&a.match(/^'[0-9a-fA-F]+'/)))return"number";if(n.binaryNumber&&(("b"==c||"B"==c)&&a.match(/^'[01]+'/)||"0"==c&&a.match(/^b[01]+/)))return"number";if(c.charCodeAt(0)>47&&c.charCodeAt(0)<58)return a.match(/^[0-9]*(\.[0-9]+)?([eE][-+]?[0-9]+)?/),n.decimallessFloat&&a.match(/^\.(?!\.)/),"number";if("?"==c&&(a.eatSpace()||a.eol()||a.eat(";")))return"variable-3";if("'"==c||'"'==c&&n.doubleQuote)return b.tokenize=e(c),b.tokenize(a,b);if((n.nCharCast&&("n"==c||"N"==c)||n.charsetCast&&"_"==c&&a.match(/[a-z][a-z0-9]*/i))&&("'"==a.peek()||'"'==a.peek()))return"keyword";if(/^[\(\),\;\[\]]/.test(c))return null;if(n.commentSlashSlash&&"/"==c&&a.eat("/"))return a.skipToEnd(),"comment";if(n.commentHash&&"#"==c||"-"==c&&a.eat("-")&&(!n.commentSpaceRequired||a.eat(" ")))return a.skipToEnd(),"comment";if("/"==c&&a.eat("*"))return b.tokenize=f(1),b.tokenize(a,b);if("."!=c){if(m.test(c))return a.eatWhile(m),null;if("{"==c&&(a.match(/^( )*(d|D|t|T|ts|TS)( )*'[^']*'( )*}/)||a.match(/^( )*(d|D|t|T|ts|TS)( )*"[^"]*"( )*}/)))return"number";a.eatWhile(/^[_\w\d]/);var g=a.current().toLowerCase();return p.hasOwnProperty(g)&&(a.match(/^( )+'[^']*'/)||a.match(/^( )+"[^"]*"/))?"number":j.hasOwnProperty(g)?"atom":k.hasOwnProperty(g)?"builtin":l.hasOwnProperty(g)?"keyword":i.hasOwnProperty(g)?"string-2":null}return n.zerolessFloat&&a.match(/^(?:\d+(?:e[+-]?\d+)?)/i)?"number":a.match(/^\.+/)?null:n.ODBCdotTable&&a.match(/^[\w\d_]+/)?"variable-2":void 0}function e(a){return function(b,c){for(var e,f=!1;null!=(e=b.next());){if(e==a&&!f){c.tokenize=d;break}f=!f&&"\\"==e}return"string"}}function f(a){return function(b,c){var e=b.match(/^.*?(\/\*|\*\/)/);return e?"/*"==e[1]?c.tokenize=f(a+1):a>1?c.tokenize=f(a-1):c.tokenize=d:b.skipToEnd(),"comment"}}function g(a,b,c){b.context={prev:b.context,indent:a.indentation(),col:a.column(),type:c}}function h(a){a.indent=a.context.indent,a.context=a.context.prev}var i=c.client||{},j=c.atoms||{"false":!0,"true":!0,"null":!0},k=c.builtin||{},l=c.keywords||{},m=c.operatorChars||/^[*+\-%<>!=&|~^]/,n=c.support||{},o=c.hooks||{},p=c.dateSQL||{date:!0,time:!0,timestamp:!0};return{startState:function(){return{tokenize:d,context:null}},token:function(a,b){if(a.sol()&&b.context&&null==b.context.align&&(b.context.align=!1),b.tokenize==d&&a.eatSpace())return null;var c=b.tokenize(a,b);if("comment"==c)return c;b.context&&null==b.context.align&&(b.context.align=!0);var e=a.current();return"("==e?g(a,b,")"):"["==e?g(a,b,"]"):b.context&&b.context.type==e&&h(b),c},indent:function(c,d){var e=c.context;if(!e)return a.Pass;var f=d.charAt(0)==e.type;return e.align?e.col+(f?0:1):e.indent+(f?0:b.indentUnit)},blockCommentStart:"/*",blockCommentEnd:"*/",lineComment:n.commentSlashSlash?"//":n.commentHash?"#":"--"}}),function(){function b(a){for(var b;null!=(b=a.next());)if("`"==b&&!a.eat("`"))return"variable-2";return a.backUp(a.current().length-1),a.eatWhile(/\w/)?"variable-2":null}function c(a){for(var b;null!=(b=a.next());)if('"'==b&&!a.eat('"'))return"variable-2";return a.backUp(a.current().length-1),a.eatWhile(/\w/)?"variable-2":null}function d(a){return a.eat("@")&&(a.match(/^session\./),a.match(/^local\./),a.match(/^global\./)),a.eat("'")?(a.match(/^.*'/),"variable-2"):a.eat('"')?(a.match(/^.*"/),"variable-2"):a.eat("`")?(a.match(/^.*`/),"variable-2"):a.match(/^[0-9a-zA-Z$\.\_]+/)?"variable-2":null}function e(a){return a.eat("N")?"atom":a.match(/^[a-zA-Z.#!?]/)?"variable-2":null}function f(a){for(var b={},c=a.split(" "),d=0;d!=]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable doubleQuote binaryNumber hexNumber")}),a.defineMIME("text/x-mssql",{name:"sql",client:f("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),keywords:f(g+"begin trigger proc view index for add constraint key primary foreign collate clustered nonclustered declare exec"),builtin:f("bigint numeric bit smallint decimal smallmoney int tinyint money float real char varchar text nchar nvarchar ntext binary varbinary image cursor timestamp hierarchyid uniqueidentifier sql_variant xml table "),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=]/,dateSQL:f("date datetimeoffset datetime2 smalldatetime datetime time"),hooks:{"@":d}}),a.defineMIME("text/x-mysql",{name:"sql",client:f("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),keywords:f(g+"accessible action add after algorithm all analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general get global grant grants group group_concat handler hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show signal slave slow smallint snapshot soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),builtin:f("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),hooks:{"@":d,"`":b,"\\":e}}),a.defineMIME("text/x-mariadb",{name:"sql",client:f("charset clear connect edit ego exit go help nopager notee nowarning pager print prompt quit rehash source status system tee"),keywords:f(g+"accessible action add after algorithm all always analyze asensitive at authors auto_increment autocommit avg avg_row_length before binary binlog both btree cache call cascade cascaded case catalog_name chain change changed character check checkpoint checksum class_origin client_statistics close coalesce code collate collation collations column columns comment commit committed completion concurrent condition connection consistent constraint contains continue contributors convert cross current current_date current_time current_timestamp current_user cursor data database databases day_hour day_microsecond day_minute day_second deallocate dec declare default delay_key_write delayed delimiter des_key_file describe deterministic dev_pop dev_samp deviance diagnostics directory disable discard distinctrow div dual dumpfile each elseif enable enclosed end ends engine engines enum errors escape escaped even event events every execute exists exit explain extended fast fetch field fields first flush for force foreign found_rows full fulltext function general generated get global grant grants group groupby_concat handler hard hash help high_priority hosts hour_microsecond hour_minute hour_second if ignore ignore_server_ids import index index_statistics infile inner innodb inout insensitive insert_method install interval invoker isolation iterate key keys kill language last leading leave left level limit linear lines list load local localtime localtimestamp lock logs low_priority master master_heartbeat_period master_ssl_verify_server_cert masters match max max_rows maxvalue message_text middleint migrate min min_rows minute_microsecond minute_second mod mode modifies modify mutex mysql_errno natural next no no_write_to_binlog offline offset one online open optimize option optionally out outer outfile pack_keys parser partition partitions password persistent phase plugin plugins prepare preserve prev primary privileges procedure processlist profile profiles purge query quick range read read_write reads real rebuild recover references regexp relaylog release remove rename reorganize repair repeatable replace require resignal restrict resume return returns revoke right rlike rollback rollup row row_format rtree savepoint schedule schema schema_name schemas second_microsecond security sensitive separator serializable server session share show shutdown signal slave slow smallint snapshot soft soname spatial specific sql sql_big_result sql_buffer_result sql_cache sql_calc_found_rows sql_no_cache sql_small_result sqlexception sqlstate sqlwarning ssl start starting starts status std stddev stddev_pop stddev_samp storage straight_join subclass_origin sum suspend table_name table_statistics tables tablespace temporary terminated to trailing transaction trigger triggers truncate uncommitted undo uninstall unique unlock upgrade usage use use_frm user user_resources user_statistics using utc_date utc_time utc_timestamp value variables varying view views virtual warnings when while with work write xa xor year_month zerofill begin do then else loop repeat"),builtin:f("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text bigint int int1 int2 int3 int4 int8 integer float float4 float8 double char varbinary varchar varcharacter precision date datetime year unsigned signed numeric"),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber doubleQuote nCharCast charsetCast commentHash commentSpaceRequired"),hooks:{"@":d,"`":b,"\\":e}}),a.defineMIME("text/x-sqlite",{name:"sql",client:f("auth backup bail binary changes check clone databases dbinfo dump echo eqp exit explain fullschema headers help import imposter indexes iotrace limit lint load log mode nullvalue once open output print prompt quit read restore save scanstats schema separator session shell show stats system tables testcase timeout timer trace vfsinfo vfslist vfsname width"),keywords:f(g+"abort action add after all analyze attach autoincrement before begin cascade case cast check collate column commit conflict constraint cross current_date current_time current_timestamp database default deferrable deferred detach each else end escape except exclusive exists explain fail for foreign full glob if ignore immediate index indexed initially inner instead intersect isnull key left limit match natural no notnull null of offset outer plan pragma primary query raise recursive references regexp reindex release rename replace restrict right rollback row savepoint temp temporary then to transaction trigger unique using vacuum view virtual when with without"),builtin:f("bool boolean bit blob decimal double float long longblob longtext medium mediumblob mediumint mediumtext time timestamp tinyblob tinyint tinytext text clob bigint int int2 int8 integer float double char varchar date datetime year unsigned signed numeric real"),atoms:f("null current_date current_time current_timestamp"),operatorChars:/^[*+\-%<>!=&|\/~]/,dateSQL:f("date time timestamp datetime"),support:f("decimallessFloat zerolessFloat"),identifierQuote:'"',hooks:{"@":d,":":d,"?":d,$:d,'"':c,"`":b}}),a.defineMIME("text/x-cassandra",{name:"sql",client:{},keywords:f("add all allow alter and any apply as asc authorize batch begin by clustering columnfamily compact consistency count create custom delete desc distinct drop each_quorum exists filtering from grant if in index insert into key keyspace keyspaces level limit local_one local_quorum modify nan norecursive nosuperuser not of on one order password permission permissions primary quorum rename revoke schema select set storage superuser table three to token truncate ttl two type unlogged update use user users using values where with writetime"),builtin:f("ascii bigint blob boolean counter decimal double float frozen inet int list map static text timestamp timeuuid tuple uuid varchar varint"),atoms:f("false true infinity NaN"),operatorChars:/^[<>=]/,dateSQL:{},support:f("commentSlashSlash decimallessFloat"),hooks:{}}),a.defineMIME("text/x-plsql",{name:"sql",client:f("appinfo arraysize autocommit autoprint autorecovery autotrace blockterminator break btitle cmdsep colsep compatibility compute concat copycommit copytypecheck define describe echo editfile embedded escape exec execute feedback flagger flush heading headsep instance linesize lno loboffset logsource long longchunksize markup native newpage numformat numwidth pagesize pause pno recsep recsepchar release repfooter repheader serveroutput shiftinout show showmode size spool sqlblanklines sqlcase sqlcode sqlcontinue sqlnumber sqlpluscompatibility sqlprefix sqlprompt sqlterminator suffix tab term termout time timing trimout trimspool ttitle underline verify version wrap"),keywords:f("abort accept access add all alter and any array arraylen as asc assert assign at attributes audit authorization avg base_table begin between binary_integer body boolean by case cast char char_base check close cluster clusters colauth column comment commit compress connect connected constant constraint crash create current currval cursor data_base database date dba deallocate debugoff debugon decimal declare default definition delay delete desc digits dispose distinct do drop else elseif elsif enable end entry escape exception exception_init exchange exclusive exists exit external fast fetch file for force form from function generic goto grant group having identified if immediate in increment index indexes indicator initial initrans insert interface intersect into is key level library like limited local lock log logging long loop master maxextents maxtrans member minextents minus mislabel mode modify multiset new next no noaudit nocompress nologging noparallel not nowait number_base object of off offline on online only open option or order out package parallel partition pctfree pctincrease pctused pls_integer positive positiven pragma primary prior private privileges procedure public raise range raw read rebuild record ref references refresh release rename replace resource restrict return returning returns reverse revoke rollback row rowid rowlabel rownum rows run savepoint schema segment select separate session set share snapshot some space split sql start statement storage subtype successful synonym tabauth table tables tablespace task terminate then to trigger truncate type union unique unlimited unrecoverable unusable update use using validate value values variable view views when whenever where while with work"),builtin:f("abs acos add_months ascii asin atan atan2 average bfile bfilename bigserial bit blob ceil character chartorowid chr clob concat convert cos cosh count dec decode deref dual dump dup_val_on_index empty error exp false float floor found glb greatest hextoraw initcap instr instrb int integer isopen last_day least length lengthb ln lower lpad ltrim lub make_ref max min mlslabel mod months_between natural naturaln nchar nclob new_time next_day nextval nls_charset_decl_len nls_charset_id nls_charset_name nls_initcap nls_lower nls_sort nls_upper nlssort no_data_found notfound null number numeric nvarchar2 nvl others power rawtohex real reftohex round rowcount rowidtochar rowtype rpad rtrim serial sign signtype sin sinh smallint soundex sqlcode sqlerrm sqrt stddev string substr substrb sum sysdate tan tanh to_char text to_date to_label to_multi_byte to_number to_single_byte translate true trunc uid unlogged upper user userenv varchar varchar2 variance varying vsize xml"),operatorChars:/^[*+\-%<>!=~]/,dateSQL:f("date time timestamp"),support:f("doubleQuote nCharCast zerolessFloat binaryNumber hexNumber")}),a.defineMIME("text/x-hive",{name:"sql",keywords:f("select alter $elem$ $key$ $value$ add after all analyze and archive as asc before between binary both bucket buckets by cascade case cast change cluster clustered clusterstatus collection column columns comment compute concatenate continue create cross cursor data database databases dbproperties deferred delete delimited desc describe directory disable distinct distribute drop else enable end escaped exclusive exists explain export extended external false fetch fields fileformat first format formatted from full function functions grant group having hold_ddltime idxproperties if import in index indexes inpath inputdriver inputformat insert intersect into is items join keys lateral left like limit lines load local location lock locks mapjoin materialized minus msck no_drop nocompress not of offline on option or order out outer outputdriver outputformat overwrite partition partitioned partitions percent plus preserve procedure purge range rcfile read readonly reads rebuild recordreader recordwriter recover reduce regexp rename repair replace restrict revoke right rlike row schema schemas semi sequencefile serde serdeproperties set shared show show_database sort sorted ssl statistics stored streamtable table tables tablesample tblproperties temporary terminated textfile then tmp to touch transform trigger true unarchive undo union uniquejoin unlock update use using utc utc_tmestamp view when where while with"),builtin:f("bool boolean long timestamp tinyint smallint bigint int float double date datetime unsigned string array struct map uniontype"),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=]/,dateSQL:f("date timestamp"),support:f("ODBCdotTable doubleQuote binaryNumber hexNumber")}),a.defineMIME("text/x-pgsql",{name:"sql",client:f("source"),keywords:f(g+"a abort abs absent absolute access according action ada add admin after aggregate all allocate also always analyse analyze any are array array_agg array_max_cardinality asensitive assertion assignment asymmetric at atomic attribute attributes authorization avg backward base64 before begin begin_frame begin_partition bernoulli binary bit_length blob blocked bom both breadth c cache call called cardinality cascade cascaded case cast catalog catalog_name ceil ceiling chain characteristics characters character_length character_set_catalog character_set_name character_set_schema char_length check checkpoint class class_origin clob close cluster coalesce cobol collate collation collation_catalog collation_name collation_schema collect column columns column_name command_function command_function_code comment comments commit committed concurrently condition condition_number configuration conflict connect connection connection_name constraint constraints constraint_catalog constraint_name constraint_schema constructor contains content continue control conversion convert copy corr corresponding cost covar_pop covar_samp cross csv cube cume_dist current current_catalog current_date current_default_transform_group current_path current_role current_row current_schema current_time current_timestamp current_transform_group_for_type current_user cursor cursor_name cycle data database datalink datetime_interval_code datetime_interval_precision day db deallocate dec declare default defaults deferrable deferred defined definer degree delimiter delimiters dense_rank depth deref derived describe descriptor deterministic diagnostics dictionary disable discard disconnect dispatch dlnewcopy dlpreviouscopy dlurlcomplete dlurlcompleteonly dlurlcompletewrite dlurlpath dlurlpathonly dlurlpathwrite dlurlscheme dlurlserver dlvalue do document domain dynamic dynamic_function dynamic_function_code each element else empty enable encoding encrypted end end-exec end_frame end_partition enforced enum equals escape event every except exception exclude excluding exclusive exec execute exists exp explain expression extension external extract false family fetch file filter final first first_value flag float floor following for force foreign fortran forward found frame_row free freeze fs full function functions fusion g general generated get global go goto grant granted greatest grouping groups handler header hex hierarchy hold hour id identity if ignore ilike immediate immediately immutable implementation implicit import including increment indent index indexes indicator inherit inherits initially inline inner inout input insensitive instance instantiable instead integrity intersect intersection invoker isnull isolation k key key_member key_type label lag language large last last_value lateral lead leading leakproof least left length level library like_regex link listen ln load local localtime localtimestamp location locator lock locked logged lower m map mapping match matched materialized max maxvalue max_cardinality member merge message_length message_octet_length message_text method min minute minvalue mod mode modifies module month more move multiset mumps name names namespace national natural nchar nclob nesting new next nfc nfd nfkc nfkd nil no none normalize normalized nothing notify notnull nowait nth_value ntile null nullable nullif nulls number object occurrences_regex octets octet_length of off offset oids old only open operator option options ordering ordinality others out outer output over overlaps overlay overriding owned owner p pad parallel parameter parameter_mode parameter_name parameter_ordinal_position parameter_specific_catalog parameter_specific_name parameter_specific_schema parser partial partition pascal passing passthrough password percent percentile_cont percentile_disc percent_rank period permission placing plans pli policy portion position position_regex power precedes preceding prepare prepared preserve primary prior privileges procedural procedure program public quote range rank read reads reassign recheck recovery recursive ref references referencing refresh regr_avgx regr_avgy regr_count regr_intercept regr_r2 regr_slope regr_sxx regr_sxy regr_syy reindex relative release rename repeatable replace replica requiring reset respect restart restore restrict restricted result return returned_cardinality returned_length returned_octet_length returned_sqlstate returning returns revoke right role rollback rollup routine routine_catalog routine_name routine_schema row rows row_count row_number rule savepoint scale schema schema_name scope scope_catalog scope_name scope_schema scroll search second section security selective self sensitive sequence sequences serializable server server_name session session_user setof sets share show similar simple size skip snapshot some source space specific specifictype specific_name sql sqlcode sqlerror sqlexception sqlstate sqlwarning sqrt stable standalone start state statement static statistics stddev_pop stddev_samp stdin stdout storage strict strip structure style subclass_origin submultiset substring substring_regex succeeds sum symmetric sysid system system_time system_user t tables tablesample tablespace table_name temp template temporary then ties timezone_hour timezone_minute to token top_level_count trailing transaction transactions_committed transactions_rolled_back transaction_active transform transforms translate translate_regex translation treat trigger trigger_catalog trigger_name trigger_schema trim trim_array true truncate trusted type types uescape unbounded uncommitted under unencrypted unique unknown unlink unlisten unlogged unnamed unnest until untyped upper uri usage user user_defined_type_catalog user_defined_type_code user_defined_type_name user_defined_type_schema using vacuum valid validate validator value value_of varbinary variadic var_pop var_samp verbose version versioning view views volatile when whenever whitespace width_bucket window within work wrapper write xmlagg xmlattributes xmlbinary xmlcast xmlcomment xmlconcat xmldeclaration xmldocument xmlelement xmlexists xmlforest xmliterate xmlnamespaces xmlparse xmlpi xmlquery xmlroot xmlschema xmlserialize xmltable xmltext xmlvalidate year yes loop repeat attach path depends detach zone"), -builtin:f("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^\/#@?~]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")}),a.defineMIME("text/x-gql",{name:"sql",keywords:f("ancestor and asc by contains desc descendant distinct from group has in is limit offset on order select superset where"),atoms:f("false true"),builtin:f("blob datetime first key __key__ string integer double boolean null"),operatorChars:/^[*+\-%<>!=]/}),a.defineMIME("text/x-gpsql",{name:"sql",client:f("source"),keywords:f("abort absolute access action active add admin after aggregate all also alter always analyse analyze and any array as asc assertion assignment asymmetric at authorization backward before begin between bigint binary bit boolean both by cache called cascade cascaded case cast chain char character characteristics check checkpoint class close cluster coalesce codegen collate column comment commit committed concurrency concurrently configuration connection constraint constraints contains content continue conversion copy cost cpu_rate_limit create createdb createexttable createrole createuser cross csv cube current current_catalog current_date current_role current_schema current_time current_timestamp current_user cursor cycle data database day deallocate dec decimal declare decode default defaults deferrable deferred definer delete delimiter delimiters deny desc dictionary disable discard distinct distributed do document domain double drop dxl each else enable encoding encrypted end enum errors escape every except exchange exclude excluding exclusive execute exists explain extension external extract false family fetch fields filespace fill filter first float following for force foreign format forward freeze from full function global grant granted greatest group group_id grouping handler hash having header hold host hour identity if ignore ilike immediate immutable implicit in including inclusive increment index indexes inherit inherits initially inline inner inout input insensitive insert instead int integer intersect interval into invoker is isnull isolation join key language large last leading least left level like limit list listen load local localtime localtimestamp location lock log login mapping master match maxvalue median merge minute minvalue missing mode modifies modify month move name names national natural nchar new newline next no nocreatedb nocreateexttable nocreaterole nocreateuser noinherit nologin none noovercommit nosuperuser not nothing notify notnull nowait null nullif nulls numeric object of off offset oids old on only operator option options or order ordered others out outer over overcommit overlaps overlay owned owner parser partial partition partitions passing password percent percentile_cont percentile_disc placing plans position preceding precision prepare prepared preserve primary prior privileges procedural procedure protocol queue quote randomly range read readable reads real reassign recheck recursive ref references reindex reject relative release rename repeatable replace replica reset resource restart restrict returning returns revoke right role rollback rollup rootpartition row rows rule savepoint scatter schema scroll search second security segment select sequence serializable session session_user set setof sets share show similar simple smallint some split sql stable standalone start statement statistics stdin stdout storage strict strip subpartition subpartitions substring superuser symmetric sysid system table tablespace temp template temporary text then threshold ties time timestamp to trailing transaction treat trigger trim true truncate trusted type unbounded uncommitted unencrypted union unique unknown unlisten until update user using vacuum valid validation validator value values varchar variadic varying verbose version view volatile web when where whitespace window with within without work writable write xml xmlattributes xmlconcat xmlelement xmlexists xmlforest xmlparse xmlpi xmlroot xmlserialize year yes zone"),builtin:f("bigint int8 bigserial serial8 bit varying varbit boolean bool box bytea character char varchar cidr circle date double precision float float8 inet integer int int4 interval json jsonb line lseg macaddr macaddr8 money numeric decimal path pg_lsn point polygon real float4 smallint int2 smallserial serial2 serial serial4 text time without zone with timetz timestamp timestamptz tsquery tsvector txid_snapshot uuid xml"),atoms:f("false true null unknown"),operatorChars:/^[*+\-%<>!=&|^\/#@?~]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable decimallessFloat zerolessFloat binaryNumber hexNumber nCharCast charsetCast")}),a.defineMIME("text/x-sparksql",{name:"sql",keywords:f("add after all alter analyze and anti archive array as asc at between bucket buckets by cache cascade case cast change clear cluster clustered codegen collection column columns comment commit compact compactions compute concatenate cost create cross cube current current_date current_timestamp database databases datata dbproperties defined delete delimited desc describe dfs directories distinct distribute drop else end escaped except exchange exists explain export extended external false fields fileformat first following for format formatted from full function functions global grant group grouping having if ignore import in index indexes inner inpath inputformat insert intersect interval into is items join keys last lateral lazy left like limit lines list load local location lock locks logical macro map minus msck natural no not null nulls of on option options or order out outer outputformat over overwrite partition partitioned partitions percent preceding principals purge range recordreader recordwriter recover reduce refresh regexp rename repair replace reset restrict revoke right rlike role roles rollback rollup row rows schema schemas select semi separated serde serdeproperties set sets show skewed sort sorted start statistics stored stratify struct table tables tablesample tblproperties temp temporary terminated then to touch transaction transactions transform true truncate unarchive unbounded uncache union unlock unset use using values view when where window with"),builtin:f("tinyint smallint int bigint boolean float double string binary timestamp decimal array map struct uniontype delimited serde sequencefile textfile rcfile inputformat outputformat"),atoms:f("false true null"),operatorChars:/^[*+\-%<>!=~&|^]/,dateSQL:f("date time timestamp"),support:f("ODBCdotTable doubleQuote zerolessFloat")})}()})},{"../../lib/codemirror":59}],75:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";var b={autoSelfClosers:{area:!0,base:!0,br:!0,col:!0,command:!0,embed:!0,frame:!0,hr:!0,img:!0,input:!0,keygen:!0,link:!0,meta:!0,param:!0,source:!0,track:!0,wbr:!0,menuitem:!0},implicitlyClosed:{dd:!0,li:!0,optgroup:!0,option:!0,p:!0,rp:!0,rt:!0,tbody:!0,td:!0,tfoot:!0,th:!0,tr:!0},contextGrabbers:{dd:{dd:!0,dt:!0},dt:{dd:!0,dt:!0},li:{li:!0},option:{option:!0,optgroup:!0},optgroup:{optgroup:!0},p:{address:!0,article:!0,aside:!0,blockquote:!0,dir:!0,div:!0,dl:!0,fieldset:!0,footer:!0,form:!0,h1:!0,h2:!0,h3:!0,h4:!0,h5:!0,h6:!0,header:!0,hgroup:!0,hr:!0,menu:!0,nav:!0,ol:!0,p:!0,pre:!0,section:!0,table:!0,ul:!0},rp:{rp:!0,rt:!0},rt:{rp:!0,rt:!0},tbody:{tbody:!0,tfoot:!0},td:{td:!0,th:!0},tfoot:{tbody:!0},th:{td:!0,th:!0},thead:{tbody:!0,tfoot:!0},tr:{tr:!0}},doNotIndent:{pre:!0},allowUnquoted:!0,allowMissing:!0,caseFold:!0},c={autoSelfClosers:{},implicitlyClosed:{},contextGrabbers:{},doNotIndent:{},allowUnquoted:!1,allowMissing:!1,caseFold:!1};a.defineMode("xml",function(d,e){function f(a,b){function c(c){return b.tokenize=c,c(a,b)}var d=a.next();if("<"==d)return a.eat("!")?a.eat("[")?a.match("CDATA[")?c(i("atom","]]>")):null:a.match("--")?c(i("comment","-->")):a.match("DOCTYPE",!0,!0)?(a.eatWhile(/[\w\._\-]/),c(j(1))):null:a.eat("?")?(a.eatWhile(/[\w\._\-]/),b.tokenize=i("meta","?>"),"meta"):(A=a.eat("/")?"closeTag":"openTag",b.tokenize=g,"tag bracket");if("&"==d){var e;return e=a.eat("#")?a.eat("x")?a.eatWhile(/[a-fA-F\d]/)&&a.eat(";"):a.eatWhile(/[\d]/)&&a.eat(";"):a.eatWhile(/[\w\.\-:]/)&&a.eat(";"),e?"atom":"error"}return a.eatWhile(/[^&<]/),null}function g(a,b){var c=a.next();if(">"==c||"/"==c&&a.eat(">"))return b.tokenize=f,A=">"==c?"endTag":"selfcloseTag","tag bracket";if("="==c)return A="equals",null;if("<"==c){b.tokenize=f,b.state=n,b.tagName=b.tagStart=null;var d=b.tokenize(a,b);return d?d+" tag error":"tag error"}return/[\'\"]/.test(c)?(b.tokenize=h(c),b.stringStartCol=a.column(),b.tokenize(a,b)):(a.match(/^[^\s\u00a0=<>\"\']*[^\s\u00a0=<>\"\'\/]/),"word")}function h(a){var b=function(b,c){for(;!b.eol();)if(b.next()==a){c.tokenize=g;break}return"string"};return b.isInAttribute=!0,b}function i(a,b){return function(c,d){for(;!c.eol();){if(c.match(b)){d.tokenize=f;break}c.next()}return a}}function j(a){return function(b,c){for(var d;null!=(d=b.next());){if("<"==d)return c.tokenize=j(a+1),c.tokenize(b,c);if(">"==d){if(1==a){c.tokenize=f;break}return c.tokenize=j(a-1),c.tokenize(b,c)}}return"meta"}}function k(a,b,c){this.prev=a.context,this.tagName=b,this.indent=a.indented,this.startOfLine=c,(x.doNotIndent.hasOwnProperty(b)||a.context&&a.context.noIndent)&&(this.noIndent=!0)}function l(a){a.context&&(a.context=a.context.prev)}function m(a,b){for(var c;;){if(!a.context)return;if(c=a.context.tagName,!x.contextGrabbers.hasOwnProperty(c)||!x.contextGrabbers[c].hasOwnProperty(b))return;l(a)}}function n(a,b,c){return"openTag"==a?(c.tagStart=b.column(),o):"closeTag"==a?p:n}function o(a,b,c){return"word"==a?(c.tagName=b.current(),B="tag",s):(B="error",o)}function p(a,b,c){if("word"==a){var d=b.current();return c.context&&c.context.tagName!=d&&x.implicitlyClosed.hasOwnProperty(c.context.tagName)&&l(c),c.context&&c.context.tagName==d||x.matchClosing===!1?(B="tag",q):(B="tag error",r)}return B="error",r}function q(a,b,c){return"endTag"!=a?(B="error",q):(l(c),n)}function r(a,b,c){return B="error",q(a,b,c)}function s(a,b,c){if("word"==a)return B="attribute",t;if("endTag"==a||"selfcloseTag"==a){var d=c.tagName,e=c.tagStart;return c.tagName=c.tagStart=null,"selfcloseTag"==a||x.autoSelfClosers.hasOwnProperty(d)?m(c,d):(m(c,d),c.context=new k(c,d,e==c.indented)),n}return B="error",s}function t(a,b,c){return"equals"==a?u:(x.allowMissing||(B="error"),s(a,b,c))}function u(a,b,c){return"string"==a?v:"word"==a&&x.allowUnquoted?(B="string",s):(B="error",s(a,b,c))}function v(a,b,c){return"string"==a?v:s(a,b,c)}var w=d.indentUnit,x={},y=e.htmlMode?b:c;for(var z in y)x[z]=y[z];for(var z in e)x[z]=e[z];var A,B;return f.isInText=!0,{startState:function(a){var b={tokenize:f,state:n,indented:a||0,tagName:null,tagStart:null,context:null};return null!=a&&(b.baseIndent=a),b},token:function(a,b){if(!b.tagName&&a.sol()&&(b.indented=a.indentation()),a.eatSpace())return null;A=null;var c=b.tokenize(a,b);return(c||A)&&"comment"!=c&&(B=null,b.state=b.state(A||c,a,b),B&&(c="error"==B?c+" error":B)),c},indent:function(b,c,d){var e=b.context;if(b.tokenize.isInAttribute)return b.tagStart==b.indented?b.stringStartCol+1:b.indented+w;if(e&&e.noIndent)return a.Pass;if(b.tokenize!=g&&b.tokenize!=f)return d?d.match(/^(\s*)/)[0].length:0;if(b.tagName)return x.multilineTagIndentPastTag!==!1?b.tagStart+b.tagName.length+2:b.tagStart+w*(x.multilineTagIndentFactor||1);if(x.alignCDATA&&/$/,blockCommentStart:"",configuration:x.htmlMode?"html":"xml",helperType:x.htmlMode?"html":"xml",skipAttribute:function(a){a.state==u&&(a.state=s)}}}),a.defineMIME("text/xml","xml"),a.defineMIME("application/xml","xml"),a.mimeModes.hasOwnProperty("text/html")||a.defineMIME("text/html",{name:"xml",htmlMode:!0})})},{"../../lib/codemirror":59}],76:[function(a,b,c){!function(d){"object"==typeof c&&"object"==typeof b?d(a("../../lib/codemirror")):"function"==typeof define&&define.amd?define(["../../lib/codemirror"],d):d(CodeMirror)}(function(a){"use strict";a.defineMode("yaml",function(){var a=["true","false","on","off","yes","no"],b=new RegExp("\\b(("+a.join(")|(")+"))$","i");return{token:function(a,c){var d=a.peek(),e=c.escaped;if(c.escaped=!1,"#"==d&&(0==a.pos||/\s/.test(a.string.charAt(a.pos-1))))return a.skipToEnd(),"comment";if(a.match(/^('([^']|\\.)*'?|"([^"]|\\.)*"?)/))return"string";if(c.literal&&a.indentation()>c.keyCol)return a.skipToEnd(),"string";if(c.literal&&(c.literal=!1),a.sol()){if(c.keyCol=0,c.pair=!1,c.pairStart=!1,a.match(/---/))return"def";if(a.match(/\.\.\./))return"def";if(a.match(/\s*-\s+/))return"meta"}if(a.match(/^(\{|\}|\[|\])/))return"{"==d?c.inlinePairs++:"}"==d?c.inlinePairs--:"["==d?c.inlineList++:c.inlineList--,"meta";if(c.inlineList>0&&!e&&","==d)return a.next(),"meta";if(c.inlinePairs>0&&!e&&","==d)return c.keyCol=0,c.pair=!1,c.pairStart=!1,a.next(),"meta";if(c.pairStart){if(a.match(/^\s*(\||\>)\s*/))return c.literal=!0,"meta";if(a.match(/^\s*(\&|\*)[a-z0-9\._-]+\b/i))return"variable-2";if(0==c.inlinePairs&&a.match(/^\s*-?[0-9\.\,]+\s?$/))return"number";if(c.inlinePairs>0&&a.match(/^\s*-?[0-9\.\,]+\s?(?=(,|}))/))return"number";if(a.match(b))return"keyword"}return!c.pair&&a.match(/^\s*(?:[,\[\]{}&*!|>'"%@`][^\s'":]|[^,\[\]{}#&*!|>'"%@`])[^#]*?(?=\s*:($|\s))/)?(c.pair=!0,c.keyCol=a.indentation(),"atom"):c.pair&&a.match(/^:\s*/)?(c.pairStart=!0,"meta"):(c.pairStart=!1,c.escaped="\\"==d,a.next(),null)},startState:function(){return{pair:!1,pairStart:!1,keyCol:0,inlinePairs:0,inlineList:0,literal:!1,escaped:!1}}}}),a.defineMIME("text/x-yaml","yaml"),a.defineMIME("text/yaml","yaml")})},{"../../lib/codemirror":59}],77:[function(a,b,c){var d=a("../../../node_modules/codemirror/lib/codemirror");a("../../../node_modules/codemirror/lib/codemirror.js"),a("../../../node_modules/codemirror/keymap/emacs.js"),a("../../../node_modules/codemirror/keymap/sublime.js"),a("../../../node_modules/codemirror/keymap/vim.js"),a("../../../node_modules/codemirror/addon/hint/show-hint.js"),a("../../../node_modules/codemirror/addon/hint/anyword-hint.js"),a("../../../node_modules/codemirror/addon/hint/css-hint.js"),a("../../../node_modules/codemirror/addon/hint/html-hint.js"),a("../../../node_modules/codemirror/addon/hint/javascript-hint.js"),a("../../../node_modules/codemirror/addon/hint/sql-hint.js"),a("../../../node_modules/codemirror/addon/hint/xml-hint.js"),a("../../../node_modules/codemirror/addon/lint/lint.js"),a("../../../node_modules/codemirror/addon/lint/css-lint.js"),a("../../../node_modules/codemirror/addon/lint/html-lint.js"),a("../../../node_modules/codemirror/addon/lint/javascript-lint.js"),a("../../../node_modules/codemirror/addon/lint/json-lint.js"),a("../../../node_modules/codemirror/addon/comment/comment.js"),a("../../../node_modules/codemirror/addon/comment/continuecomment.js"),a("../../../node_modules/codemirror/addon/fold/xml-fold.js"),a("../../../node_modules/codemirror/addon/mode/overlay.js"),a("../../../node_modules/codemirror/addon/edit/closebrackets.js"),a("../../../node_modules/codemirror/addon/edit/closetag.js"),a("../../../node_modules/codemirror/addon/edit/continuelist.js"),a("../../../node_modules/codemirror/addon/edit/matchbrackets.js"),a("../../../node_modules/codemirror/addon/edit/matchtags.js"),a("../../../node_modules/codemirror/addon/edit/trailingspace.js"),a("../../../node_modules/codemirror/addon/dialog/dialog.js"),a("../../../node_modules/codemirror/addon/display/autorefresh.js"),a("../../../node_modules/codemirror/addon/display/fullscreen.js"),a("../../../node_modules/codemirror/addon/display/panel.js"),a("../../../node_modules/codemirror/addon/display/placeholder.js"),a("../../../node_modules/codemirror/addon/display/rulers.js"),a("../../../node_modules/codemirror/addon/fold/brace-fold.js"),a("../../../node_modules/codemirror/addon/fold/comment-fold.js"),a("../../../node_modules/codemirror/addon/fold/foldcode.js"),a("../../../node_modules/codemirror/addon/fold/foldgutter.js"),a("../../../node_modules/codemirror/addon/fold/indent-fold.js"),a("../../../node_modules/codemirror/addon/fold/markdown-fold.js"),a("../../../node_modules/codemirror/addon/merge/merge.js"),a("../../../node_modules/codemirror/addon/mode/loadmode.js"),a("../../../node_modules/codemirror/addon/mode/multiplex.js"),a("../../../node_modules/codemirror/addon/mode/simple.js"),a("../../../node_modules/codemirror/addon/runmode/runmode.js"),a("../../../node_modules/codemirror/addon/runmode/colorize.js"),a("../../../node_modules/codemirror/addon/runmode/runmode-standalone.js"),a("../../../node_modules/codemirror/addon/scroll/annotatescrollbar.js"),a("../../../node_modules/codemirror/addon/scroll/scrollpastend.js"),a("../../../node_modules/codemirror/addon/scroll/simplescrollbars.js"),a("../../../node_modules/codemirror/addon/search/search.js"),a("../../../node_modules/codemirror/addon/search/jump-to-line.js"),a("../../../node_modules/codemirror/addon/search/match-highlighter.js"),a("../../../node_modules/codemirror/addon/search/matchesonscrollbar.js"),a("../../../node_modules/codemirror/addon/search/searchcursor.js"),a("../../../node_modules/codemirror/addon/tern/tern.js"),a("../../../node_modules/codemirror/addon/tern/worker.js"),a("../../../node_modules/codemirror/addon/wrap/hardwrap.js"),a("../../../node_modules/codemirror/addon/selection/active-line.js"),a("../../../node_modules/codemirror/addon/selection/mark-selection.js"),a("../../../node_modules/codemirror/addon/selection/selection-pointer.js"),a("../../../node_modules/codemirror/mode/meta.js"),a("../../../node_modules/codemirror/mode/clike/clike.js"),a("../../../node_modules/codemirror/mode/css/css.js"),a("../../../node_modules/codemirror/mode/diff/diff.js"),a("../../../node_modules/codemirror/mode/htmlmixed/htmlmixed.js"),a("../../../node_modules/codemirror/mode/http/http.js"),a("../../../node_modules/codemirror/mode/javascript/javascript.js"),a("../../../node_modules/codemirror/mode/jsx/jsx.js"),a("../../../node_modules/codemirror/mode/markdown/markdown.js"),a("../../../node_modules/codemirror/mode/gfm/gfm.js"),a("../../../node_modules/codemirror/mode/nginx/nginx.js"),a("../../../node_modules/codemirror/mode/php/php.js"),a("../../../node_modules/codemirror/mode/sass/sass.js"),a("../../../node_modules/codemirror/mode/shell/shell.js"),a("../../../node_modules/codemirror/mode/sql/sql.js"),a("../../../node_modules/codemirror/mode/xml/xml.js"),a("../../../node_modules/codemirror/mode/yaml/yaml.js"),window.wp||(window.wp={}),window.wp.CodeMirror=d},{"../../../node_modules/codemirror/addon/comment/comment.js":1,"../../../node_modules/codemirror/addon/comment/continuecomment.js":2,"../../../node_modules/codemirror/addon/dialog/dialog.js":3,"../../../node_modules/codemirror/addon/display/autorefresh.js":4,"../../../node_modules/codemirror/addon/display/fullscreen.js":5,"../../../node_modules/codemirror/addon/display/panel.js":6,"../../../node_modules/codemirror/addon/display/placeholder.js":7,"../../../node_modules/codemirror/addon/display/rulers.js":8,"../../../node_modules/codemirror/addon/edit/closebrackets.js":9,"../../../node_modules/codemirror/addon/edit/closetag.js":10,"../../../node_modules/codemirror/addon/edit/continuelist.js":11,"../../../node_modules/codemirror/addon/edit/matchbrackets.js":12,"../../../node_modules/codemirror/addon/edit/matchtags.js":13,"../../../node_modules/codemirror/addon/edit/trailingspace.js":14,"../../../node_modules/codemirror/addon/fold/brace-fold.js":15,"../../../node_modules/codemirror/addon/fold/comment-fold.js":16,"../../../node_modules/codemirror/addon/fold/foldcode.js":17,"../../../node_modules/codemirror/addon/fold/foldgutter.js":18,"../../../node_modules/codemirror/addon/fold/indent-fold.js":19,"../../../node_modules/codemirror/addon/fold/markdown-fold.js":20,"../../../node_modules/codemirror/addon/fold/xml-fold.js":21,"../../../node_modules/codemirror/addon/hint/anyword-hint.js":22,"../../../node_modules/codemirror/addon/hint/css-hint.js":23,"../../../node_modules/codemirror/addon/hint/html-hint.js":24,"../../../node_modules/codemirror/addon/hint/javascript-hint.js":25,"../../../node_modules/codemirror/addon/hint/show-hint.js":26,"../../../node_modules/codemirror/addon/hint/sql-hint.js":27,"../../../node_modules/codemirror/addon/hint/xml-hint.js":28,"../../../node_modules/codemirror/addon/lint/css-lint.js":29,"../../../node_modules/codemirror/addon/lint/html-lint.js":30,"../../../node_modules/codemirror/addon/lint/javascript-lint.js":31,"../../../node_modules/codemirror/addon/lint/json-lint.js":32,"../../../node_modules/codemirror/addon/lint/lint.js":33,"../../../node_modules/codemirror/addon/merge/merge.js":34,"../../../node_modules/codemirror/addon/mode/loadmode.js":35,"../../../node_modules/codemirror/addon/mode/multiplex.js":36,"../../../node_modules/codemirror/addon/mode/overlay.js":37,"../../../node_modules/codemirror/addon/mode/simple.js":38,"../../../node_modules/codemirror/addon/runmode/colorize.js":39,"../../../node_modules/codemirror/addon/runmode/runmode-standalone.js":40,"../../../node_modules/codemirror/addon/runmode/runmode.js":41,"../../../node_modules/codemirror/addon/scroll/annotatescrollbar.js":42,"../../../node_modules/codemirror/addon/scroll/scrollpastend.js":43,"../../../node_modules/codemirror/addon/scroll/simplescrollbars.js":44,"../../../node_modules/codemirror/addon/search/jump-to-line.js":45,"../../../node_modules/codemirror/addon/search/match-highlighter.js":46,"../../../node_modules/codemirror/addon/search/matchesonscrollbar.js":47,"../../../node_modules/codemirror/addon/search/search.js":48,"../../../node_modules/codemirror/addon/search/searchcursor.js":49,"../../../node_modules/codemirror/addon/selection/active-line.js":50,"../../../node_modules/codemirror/addon/selection/mark-selection.js":51,"../../../node_modules/codemirror/addon/selection/selection-pointer.js":52,"../../../node_modules/codemirror/addon/tern/tern.js":53,"../../../node_modules/codemirror/addon/tern/worker.js":54,"../../../node_modules/codemirror/addon/wrap/hardwrap.js":55,"../../../node_modules/codemirror/keymap/emacs.js":56,"../../../node_modules/codemirror/keymap/sublime.js":57,"../../../node_modules/codemirror/keymap/vim.js":58,"../../../node_modules/codemirror/lib/codemirror":59,"../../../node_modules/codemirror/lib/codemirror.js":59,"../../../node_modules/codemirror/mode/clike/clike.js":60,"../../../node_modules/codemirror/mode/css/css.js":61,"../../../node_modules/codemirror/mode/diff/diff.js":62,"../../../node_modules/codemirror/mode/gfm/gfm.js":63,"../../../node_modules/codemirror/mode/htmlmixed/htmlmixed.js":64,"../../../node_modules/codemirror/mode/http/http.js":65,"../../../node_modules/codemirror/mode/javascript/javascript.js":66,"../../../node_modules/codemirror/mode/jsx/jsx.js":67,"../../../node_modules/codemirror/mode/markdown/markdown.js":68,"../../../node_modules/codemirror/mode/meta.js":69,"../../../node_modules/codemirror/mode/nginx/nginx.js":70,"../../../node_modules/codemirror/mode/php/php.js":71,"../../../node_modules/codemirror/mode/sass/sass.js":72,"../../../node_modules/codemirror/mode/shell/shell.js":73,"../../../node_modules/codemirror/mode/sql/sql.js":74,"../../../node_modules/codemirror/mode/xml/xml.js":75,"../../../node_modules/codemirror/mode/yaml/yaml.js":76}]},{},[77]); \ No newline at end of file diff --git a/src/js/_enqueues/vendor/codemirror/csslint.js b/src/js/_enqueues/vendor/codemirror/csslint.js deleted file mode 100644 index 665e4a2179859..0000000000000 --- a/src/js/_enqueues/vendor/codemirror/csslint.js +++ /dev/null @@ -1,10859 +0,0 @@ -/*! -CSSLint v1.0.4 -Copyright (c) 2016 Nicole Sullivan and Nicholas C. Zakas. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the 'Software'), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. - -*/ - -var CSSLint = (function(){ - var module = module || {}, - exports = exports || {}; - -/*! -Parser-Lib -Copyright (c) 2009-2016 Nicholas C. Zakas. All rights reserved. - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -THE SOFTWARE. -*/ -/* Version v1.1.0, Build time: 6-December-2016 10:31:29 */ -var parserlib = (function () { -var require; -require=(function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o). - * @namespace parserlib.css - * @class Combinator - * @extends parserlib.util.SyntaxUnit - * @constructor - * @param {String} text The text representation of the unit. - * @param {int} line The line of text on which the unit resides. - * @param {int} col The column of text on which the unit resides. - */ -function Combinator(text, line, col) { - - SyntaxUnit.call(this, text, line, col, Parser.COMBINATOR_TYPE); - - /** - * The type of modifier. - * @type String - * @property type - */ - this.type = "unknown"; - - //pretty simple - if (/^\s+$/.test(text)) { - this.type = "descendant"; - } else if (text === ">") { - this.type = "child"; - } else if (text === "+") { - this.type = "adjacent-sibling"; - } else if (text === "~") { - this.type = "sibling"; - } - -} - -Combinator.prototype = new SyntaxUnit(); -Combinator.prototype.constructor = Combinator; - - -},{"../util/SyntaxUnit":26,"./Parser":6}],3:[function(require,module,exports){ -"use strict"; - -module.exports = Matcher; - -var StringReader = require("../util/StringReader"); -var SyntaxError = require("../util/SyntaxError"); - -/** - * This class implements a combinator library for matcher functions. - * The combinators are described at: - * https://developer.mozilla.org/en-US/docs/Web/CSS/Value_definition_syntax#Component_value_combinators - */ -function Matcher(matchFunc, toString) { - this.match = function(expression) { - // Save/restore marks to ensure that failed matches always restore - // the original location in the expression. - var result; - expression.mark(); - result = matchFunc(expression); - if (result) { - expression.drop(); - } else { - expression.restore(); - } - return result; - }; - this.toString = typeof toString === "function" ? toString : function() { - return toString; - }; -} - -/** Precedence table of combinators. */ -Matcher.prec = { - MOD: 5, - SEQ: 4, - ANDAND: 3, - OROR: 2, - ALT: 1 -}; - -/** Simple recursive-descent grammar to build matchers from strings. */ -Matcher.parse = function(str) { - var reader, eat, expr, oror, andand, seq, mod, term, result; - reader = new StringReader(str); - eat = function(matcher) { - var result = reader.readMatch(matcher); - if (result === null) { - throw new SyntaxError( - "Expected "+matcher, reader.getLine(), reader.getCol()); - } - return result; - }; - expr = function() { - // expr = oror (" | " oror)* - var m = [ oror() ]; - while (reader.readMatch(" | ") !== null) { - m.push(oror()); - } - return m.length === 1 ? m[0] : Matcher.alt.apply(Matcher, m); - }; - oror = function() { - // oror = andand ( " || " andand)* - var m = [ andand() ]; - while (reader.readMatch(" || ") !== null) { - m.push(andand()); - } - return m.length === 1 ? m[0] : Matcher.oror.apply(Matcher, m); - }; - andand = function() { - // andand = seq ( " && " seq)* - var m = [ seq() ]; - while (reader.readMatch(" && ") !== null) { - m.push(seq()); - } - return m.length === 1 ? m[0] : Matcher.andand.apply(Matcher, m); - }; - seq = function() { - // seq = mod ( " " mod)* - var m = [ mod() ]; - while (reader.readMatch(/^ (?![&|\]])/) !== null) { - m.push(mod()); - } - return m.length === 1 ? m[0] : Matcher.seq.apply(Matcher, m); - }; - mod = function() { - // mod = term ( "?" | "*" | "+" | "#" | "{,}" )? - var m = term(); - if (reader.readMatch("?") !== null) { - return m.question(); - } else if (reader.readMatch("*") !== null) { - return m.star(); - } else if (reader.readMatch("+") !== null) { - return m.plus(); - } else if (reader.readMatch("#") !== null) { - return m.hash(); - } else if (reader.readMatch(/^\{\s*/) !== null) { - var min = eat(/^\d+/); - eat(/^\s*,\s*/); - var max = eat(/^\d+/); - eat(/^\s*\}/); - return m.braces(+min, +max); - } - return m; - }; - term = function() { - // term = | literal | "[ " expression " ]" - if (reader.readMatch("[ ") !== null) { - var m = expr(); - eat(" ]"); - return m; - } - return Matcher.fromType(eat(/^[^ ?*+#{]+/)); - }; - result = expr(); - if (!reader.eof()) { - throw new SyntaxError( - "Expected end of string", reader.getLine(), reader.getCol()); - } - return result; -}; - -/** - * Convert a string to a matcher (parsing simple alternations), - * or do nothing if the argument is already a matcher. - */ -Matcher.cast = function(m) { - if (m instanceof Matcher) { - return m; - } - return Matcher.parse(m); -}; - -/** - * Create a matcher for a single type. - */ -Matcher.fromType = function(type) { - // Late require of ValidationTypes to break a dependency cycle. - var ValidationTypes = require("./ValidationTypes"); - return new Matcher(function(expression) { - return expression.hasNext() && ValidationTypes.isType(expression, type); - }, type); -}; - -/** - * Create a matcher for one or more juxtaposed words, which all must - * occur, in the given order. - */ -Matcher.seq = function() { - var ms = Array.prototype.slice.call(arguments).map(Matcher.cast); - if (ms.length === 1) { - return ms[0]; - } - return new Matcher(function(expression) { - var i, result = true; - for (i = 0; result && i < ms.length; i++) { - result = ms[i].match(expression); - } - return result; - }, function(prec) { - var p = Matcher.prec.SEQ; - var s = ms.map(function(m) { - return m.toString(p); - }).join(" "); - if (prec > p) { - s = "[ " + s + " ]"; - } - return s; - }); -}; - -/** - * Create a matcher for one or more alternatives, where exactly one - * must occur. - */ -Matcher.alt = function() { - var ms = Array.prototype.slice.call(arguments).map(Matcher.cast); - if (ms.length === 1) { - return ms[0]; - } - return new Matcher(function(expression) { - var i, result = false; - for (i = 0; !result && i < ms.length; i++) { - result = ms[i].match(expression); - } - return result; - }, function(prec) { - var p = Matcher.prec.ALT; - var s = ms.map(function(m) { - return m.toString(p); - }).join(" | "); - if (prec > p) { - s = "[ " + s + " ]"; - } - return s; - }); -}; - -/** - * Create a matcher for two or more options. This implements the - * double bar (||) and double ampersand (&&) operators, as well as - * variants of && where some of the alternatives are optional. - * This will backtrack through even successful matches to try to - * maximize the number of items matched. - */ -Matcher.many = function(required) { - var ms = Array.prototype.slice.call(arguments, 1).reduce(function(acc, v) { - if (v.expand) { - // Insert all of the options for the given complex rule as - // individual options. - var ValidationTypes = require("./ValidationTypes"); - acc.push.apply(acc, ValidationTypes.complex[v.expand].options); - } else { - acc.push(Matcher.cast(v)); - } - return acc; - }, []); - - if (required === true) { - required = ms.map(function() { - return true; - }); - } - - var result = new Matcher(function(expression) { - var seen = [], max = 0, pass = 0; - var success = function(matchCount) { - if (pass === 0) { - max = Math.max(matchCount, max); - return matchCount === ms.length; - } else { - return matchCount === max; - } - }; - var tryMatch = function(matchCount) { - for (var i = 0; i < ms.length; i++) { - if (seen[i]) { - continue; - } - expression.mark(); - if (ms[i].match(expression)) { - seen[i] = true; - // Increase matchCount iff this was a required element - // (or if all the elements are optional) - if (tryMatch(matchCount + ((required === false || required[i]) ? 1 : 0))) { - expression.drop(); - return true; - } - // Backtrack: try *not* matching using this rule, and - // let's see if it leads to a better overall match. - expression.restore(); - seen[i] = false; - } else { - expression.drop(); - } - } - return success(matchCount); - }; - if (!tryMatch(0)) { - // Couldn't get a complete match, retrace our steps to make the - // match with the maximum # of required elements. - pass++; - tryMatch(0); - } - - if (required === false) { - return max > 0; - } - // Use finer-grained specification of which matchers are required. - for (var i = 0; i < ms.length; i++) { - if (required[i] && !seen[i]) { - return false; - } - } - return true; - }, function(prec) { - var p = required === false ? Matcher.prec.OROR : Matcher.prec.ANDAND; - var s = ms.map(function(m, i) { - if (required !== false && !required[i]) { - return m.toString(Matcher.prec.MOD) + "?"; - } - return m.toString(p); - }).join(required === false ? " || " : " && "); - if (prec > p) { - s = "[ " + s + " ]"; - } - return s; - }); - result.options = ms; - return result; -}; - -/** - * Create a matcher for two or more options, where all options are - * mandatory but they may appear in any order. - */ -Matcher.andand = function() { - var args = Array.prototype.slice.call(arguments); - args.unshift(true); - return Matcher.many.apply(Matcher, args); -}; - -/** - * Create a matcher for two or more options, where options are - * optional and may appear in any order, but at least one must be - * present. - */ -Matcher.oror = function() { - var args = Array.prototype.slice.call(arguments); - args.unshift(false); - return Matcher.many.apply(Matcher, args); -}; - -/** Instance methods on Matchers. */ -Matcher.prototype = { - constructor: Matcher, - // These are expected to be overridden in every instance. - match: function() { throw new Error("unimplemented"); }, - toString: function() { throw new Error("unimplemented"); }, - // This returns a standalone function to do the matching. - func: function() { return this.match.bind(this); }, - // Basic combinators - then: function(m) { return Matcher.seq(this, m); }, - or: function(m) { return Matcher.alt(this, m); }, - andand: function(m) { return Matcher.many(true, this, m); }, - oror: function(m) { return Matcher.many(false, this, m); }, - // Component value multipliers - star: function() { return this.braces(0, Infinity, "*"); }, - plus: function() { return this.braces(1, Infinity, "+"); }, - question: function() { return this.braces(0, 1, "?"); }, - hash: function() { - return this.braces(1, Infinity, "#", Matcher.cast(",")); - }, - braces: function(min, max, marker, optSep) { - var m1 = this, m2 = optSep ? optSep.then(this) : this; - if (!marker) { - marker = "{" + min + "," + max + "}"; - } - return new Matcher(function(expression) { - var result = true, i; - for (i = 0; i < max; i++) { - if (i > 0 && optSep) { - result = m2.match(expression); - } else { - result = m1.match(expression); - } - if (!result) { - break; - } - } - return i >= min; - }, function() { - return m1.toString(Matcher.prec.MOD) + marker; - }); - } -}; - -},{"../util/StringReader":24,"../util/SyntaxError":25,"./ValidationTypes":21}],4:[function(require,module,exports){ -"use strict"; - -module.exports = MediaFeature; - -var SyntaxUnit = require("../util/SyntaxUnit"); - -var Parser = require("./Parser"); - -/** - * Represents a media feature, such as max-width:500. - * @namespace parserlib.css - * @class MediaFeature - * @extends parserlib.util.SyntaxUnit - * @constructor - * @param {SyntaxUnit} name The name of the feature. - * @param {SyntaxUnit} value The value of the feature or null if none. - */ -function MediaFeature(name, value) { - - SyntaxUnit.call(this, "(" + name + (value !== null ? ":" + value : "") + ")", name.startLine, name.startCol, Parser.MEDIA_FEATURE_TYPE); - - /** - * The name of the media feature - * @type String - * @property name - */ - this.name = name; - - /** - * The value for the feature or null if there is none. - * @type SyntaxUnit - * @property value - */ - this.value = value; -} - -MediaFeature.prototype = new SyntaxUnit(); -MediaFeature.prototype.constructor = MediaFeature; - - -},{"../util/SyntaxUnit":26,"./Parser":6}],5:[function(require,module,exports){ -"use strict"; - -module.exports = MediaQuery; - -var SyntaxUnit = require("../util/SyntaxUnit"); - -var Parser = require("./Parser"); - -/** - * Represents an individual media query. - * @namespace parserlib.css - * @class MediaQuery - * @extends parserlib.util.SyntaxUnit - * @constructor - * @param {String} modifier The modifier "not" or "only" (or null). - * @param {String} mediaType The type of media (i.e., "print"). - * @param {Array} parts Array of selectors parts making up this selector. - * @param {int} line The line of text on which the unit resides. - * @param {int} col The column of text on which the unit resides. - */ -function MediaQuery(modifier, mediaType, features, line, col) { - - SyntaxUnit.call(this, (modifier ? modifier + " ": "") + (mediaType ? mediaType : "") + (mediaType && features.length > 0 ? " and " : "") + features.join(" and "), line, col, Parser.MEDIA_QUERY_TYPE); - - /** - * The media modifier ("not" or "only") - * @type String - * @property modifier - */ - this.modifier = modifier; - - /** - * The mediaType (i.e., "print") - * @type String - * @property mediaType - */ - this.mediaType = mediaType; - - /** - * The parts that make up the selector. - * @type Array - * @property features - */ - this.features = features; - -} - -MediaQuery.prototype = new SyntaxUnit(); -MediaQuery.prototype.constructor = MediaQuery; - - -},{"../util/SyntaxUnit":26,"./Parser":6}],6:[function(require,module,exports){ -"use strict"; - -module.exports = Parser; - -var EventTarget = require("../util/EventTarget"); -var SyntaxError = require("../util/SyntaxError"); -var SyntaxUnit = require("../util/SyntaxUnit"); - -var Combinator = require("./Combinator"); -var MediaFeature = require("./MediaFeature"); -var MediaQuery = require("./MediaQuery"); -var PropertyName = require("./PropertyName"); -var PropertyValue = require("./PropertyValue"); -var PropertyValuePart = require("./PropertyValuePart"); -var Selector = require("./Selector"); -var SelectorPart = require("./SelectorPart"); -var SelectorSubPart = require("./SelectorSubPart"); -var TokenStream = require("./TokenStream"); -var Tokens = require("./Tokens"); -var Validation = require("./Validation"); - -/** - * A CSS3 parser. - * @namespace parserlib.css - * @class Parser - * @constructor - * @param {Object} options (Optional) Various options for the parser: - * starHack (true|false) to allow IE6 star hack as valid, - * underscoreHack (true|false) to interpret leading underscores - * as IE6-7 targeting for known properties, ieFilters (true|false) - * to indicate that IE < 8 filters should be accepted and not throw - * syntax errors. - */ -function Parser(options) { - - //inherit event functionality - EventTarget.call(this); - - - this.options = options || {}; - - this._tokenStream = null; -} - -//Static constants -Parser.DEFAULT_TYPE = 0; -Parser.COMBINATOR_TYPE = 1; -Parser.MEDIA_FEATURE_TYPE = 2; -Parser.MEDIA_QUERY_TYPE = 3; -Parser.PROPERTY_NAME_TYPE = 4; -Parser.PROPERTY_VALUE_TYPE = 5; -Parser.PROPERTY_VALUE_PART_TYPE = 6; -Parser.SELECTOR_TYPE = 7; -Parser.SELECTOR_PART_TYPE = 8; -Parser.SELECTOR_SUB_PART_TYPE = 9; - -Parser.prototype = function() { - - var proto = new EventTarget(), //new prototype - prop, - additions = { - __proto__: null, - - //restore constructor - constructor: Parser, - - //instance constants - yuck - DEFAULT_TYPE : 0, - COMBINATOR_TYPE : 1, - MEDIA_FEATURE_TYPE : 2, - MEDIA_QUERY_TYPE : 3, - PROPERTY_NAME_TYPE : 4, - PROPERTY_VALUE_TYPE : 5, - PROPERTY_VALUE_PART_TYPE : 6, - SELECTOR_TYPE : 7, - SELECTOR_PART_TYPE : 8, - SELECTOR_SUB_PART_TYPE : 9, - - //----------------------------------------------------------------- - // Grammar - //----------------------------------------------------------------- - - _stylesheet: function() { - - /* - * stylesheet - * : [ CHARSET_SYM S* STRING S* ';' ]? - * [S|CDO|CDC]* [ import [S|CDO|CDC]* ]* - * [ namespace [S|CDO|CDC]* ]* - * [ [ ruleset | media | page | font_face | keyframes_rule | supports_rule ] [S|CDO|CDC]* ]* - * ; - */ - - var tokenStream = this._tokenStream, - count, - token, - tt; - - this.fire("startstylesheet"); - - //try to read character set - this._charset(); - - this._skipCruft(); - - //try to read imports - may be more than one - while (tokenStream.peek() === Tokens.IMPORT_SYM) { - this._import(); - this._skipCruft(); - } - - //try to read namespaces - may be more than one - while (tokenStream.peek() === Tokens.NAMESPACE_SYM) { - this._namespace(); - this._skipCruft(); - } - - //get the next token - tt = tokenStream.peek(); - - //try to read the rest - while (tt > Tokens.EOF) { - - try { - - switch (tt) { - case Tokens.MEDIA_SYM: - this._media(); - this._skipCruft(); - break; - case Tokens.PAGE_SYM: - this._page(); - this._skipCruft(); - break; - case Tokens.FONT_FACE_SYM: - this._font_face(); - this._skipCruft(); - break; - case Tokens.KEYFRAMES_SYM: - this._keyframes(); - this._skipCruft(); - break; - case Tokens.VIEWPORT_SYM: - this._viewport(); - this._skipCruft(); - break; - case Tokens.DOCUMENT_SYM: - this._document(); - this._skipCruft(); - break; - case Tokens.SUPPORTS_SYM: - this._supports(); - this._skipCruft(); - break; - case Tokens.UNKNOWN_SYM: //unknown @ rule - tokenStream.get(); - if (!this.options.strict) { - - //fire error event - this.fire({ - type: "error", - error: null, - message: "Unknown @ rule: " + tokenStream.LT(0).value + ".", - line: tokenStream.LT(0).startLine, - col: tokenStream.LT(0).startCol - }); - - //skip braces - count=0; - while (tokenStream.advance([Tokens.LBRACE, Tokens.RBRACE]) === Tokens.LBRACE) { - count++; //keep track of nesting depth - } - - while (count) { - tokenStream.advance([Tokens.RBRACE]); - count--; - } - - } else { - //not a syntax error, rethrow it - throw new SyntaxError("Unknown @ rule.", tokenStream.LT(0).startLine, tokenStream.LT(0).startCol); - } - break; - case Tokens.S: - this._readWhitespace(); - break; - default: - if (!this._ruleset()) { - - //error handling for known issues - switch (tt) { - case Tokens.CHARSET_SYM: - token = tokenStream.LT(1); - this._charset(false); - throw new SyntaxError("@charset not allowed here.", token.startLine, token.startCol); - case Tokens.IMPORT_SYM: - token = tokenStream.LT(1); - this._import(false); - throw new SyntaxError("@import not allowed here.", token.startLine, token.startCol); - case Tokens.NAMESPACE_SYM: - token = tokenStream.LT(1); - this._namespace(false); - throw new SyntaxError("@namespace not allowed here.", token.startLine, token.startCol); - default: - tokenStream.get(); //get the last token - this._unexpectedToken(tokenStream.token()); - } - - } - } - } catch (ex) { - if (ex instanceof SyntaxError && !this.options.strict) { - this.fire({ - type: "error", - error: ex, - message: ex.message, - line: ex.line, - col: ex.col - }); - } else { - throw ex; - } - } - - tt = tokenStream.peek(); - } - - if (tt !== Tokens.EOF) { - this._unexpectedToken(tokenStream.token()); - } - - this.fire("endstylesheet"); - }, - - _charset: function(emit) { - var tokenStream = this._tokenStream, - charset, - token, - line, - col; - - if (tokenStream.match(Tokens.CHARSET_SYM)) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - tokenStream.mustMatch(Tokens.STRING); - - token = tokenStream.token(); - charset = token.value; - - this._readWhitespace(); - tokenStream.mustMatch(Tokens.SEMICOLON); - - if (emit !== false) { - this.fire({ - type: "charset", - charset:charset, - line: line, - col: col - }); - } - } - }, - - _import: function(emit) { - /* - * import - * : IMPORT_SYM S* - * [STRING|URI] S* media_query_list? ';' S* - */ - - var tokenStream = this._tokenStream, - uri, - importToken, - mediaList = []; - - //read import symbol - tokenStream.mustMatch(Tokens.IMPORT_SYM); - importToken = tokenStream.token(); - this._readWhitespace(); - - tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); - - //grab the URI value - uri = tokenStream.token().value.replace(/^(?:url\()?["']?([^"']+?)["']?\)?$/, "$1"); - - this._readWhitespace(); - - mediaList = this._media_query_list(); - - //must end with a semicolon - tokenStream.mustMatch(Tokens.SEMICOLON); - this._readWhitespace(); - - if (emit !== false) { - this.fire({ - type: "import", - uri: uri, - media: mediaList, - line: importToken.startLine, - col: importToken.startCol - }); - } - - }, - - _namespace: function(emit) { - /* - * namespace - * : NAMESPACE_SYM S* [namespace_prefix S*]? [STRING|URI] S* ';' S* - */ - - var tokenStream = this._tokenStream, - line, - col, - prefix, - uri; - - //read import symbol - tokenStream.mustMatch(Tokens.NAMESPACE_SYM); - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - this._readWhitespace(); - - //it's a namespace prefix - no _namespace_prefix() method because it's just an IDENT - if (tokenStream.match(Tokens.IDENT)) { - prefix = tokenStream.token().value; - this._readWhitespace(); - } - - tokenStream.mustMatch([Tokens.STRING, Tokens.URI]); - /*if (!tokenStream.match(Tokens.STRING)){ - tokenStream.mustMatch(Tokens.URI); - }*/ - - //grab the URI value - uri = tokenStream.token().value.replace(/(?:url\()?["']([^"']+)["']\)?/, "$1"); - - this._readWhitespace(); - - //must end with a semicolon - tokenStream.mustMatch(Tokens.SEMICOLON); - this._readWhitespace(); - - if (emit !== false) { - this.fire({ - type: "namespace", - prefix: prefix, - uri: uri, - line: line, - col: col - }); - } - - }, - - _supports: function(emit) { - /* - * supports_rule - * : SUPPORTS_SYM S* supports_condition S* group_rule_body - * ; - */ - var tokenStream = this._tokenStream, - line, - col; - - if (tokenStream.match(Tokens.SUPPORTS_SYM)) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - this._supports_condition(); - this._readWhitespace(); - - tokenStream.mustMatch(Tokens.LBRACE); - this._readWhitespace(); - - if (emit !== false) { - this.fire({ - type: "startsupports", - line: line, - col: col - }); - } - - while (true) { - if (!this._ruleset()) { - break; - } - } - - tokenStream.mustMatch(Tokens.RBRACE); - this._readWhitespace(); - - this.fire({ - type: "endsupports", - line: line, - col: col - }); - } - }, - - _supports_condition: function() { - /* - * supports_condition - * : supports_negation | supports_conjunction | supports_disjunction | - * supports_condition_in_parens - * ; - */ - var tokenStream = this._tokenStream, - ident; - - if (tokenStream.match(Tokens.IDENT)) { - ident = tokenStream.token().value.toLowerCase(); - - if (ident === "not") { - tokenStream.mustMatch(Tokens.S); - this._supports_condition_in_parens(); - } else { - tokenStream.unget(); - } - } else { - this._supports_condition_in_parens(); - this._readWhitespace(); - - while (tokenStream.peek() === Tokens.IDENT) { - ident = tokenStream.LT(1).value.toLowerCase(); - if (ident === "and" || ident === "or") { - tokenStream.mustMatch(Tokens.IDENT); - this._readWhitespace(); - this._supports_condition_in_parens(); - this._readWhitespace(); - } - } - } - }, - - _supports_condition_in_parens: function() { - /* - * supports_condition_in_parens - * : ( '(' S* supports_condition S* ')' ) | supports_declaration_condition | - * general_enclosed - * ; - */ - var tokenStream = this._tokenStream, - ident; - - if (tokenStream.match(Tokens.LPAREN)) { - this._readWhitespace(); - if (tokenStream.match(Tokens.IDENT)) { - // look ahead for not keyword, if not given, continue with declaration condition. - ident = tokenStream.token().value.toLowerCase(); - if (ident === "not") { - this._readWhitespace(); - this._supports_condition(); - this._readWhitespace(); - tokenStream.mustMatch(Tokens.RPAREN); - } else { - tokenStream.unget(); - this._supports_declaration_condition(false); - } - } else { - this._supports_condition(); - this._readWhitespace(); - tokenStream.mustMatch(Tokens.RPAREN); - } - } else { - this._supports_declaration_condition(); - } - }, - - _supports_declaration_condition: function(requireStartParen) { - /* - * supports_declaration_condition - * : '(' S* declaration ')' - * ; - */ - var tokenStream = this._tokenStream; - - if (requireStartParen !== false) { - tokenStream.mustMatch(Tokens.LPAREN); - } - this._readWhitespace(); - this._declaration(); - tokenStream.mustMatch(Tokens.RPAREN); - }, - - _media: function() { - /* - * media - * : MEDIA_SYM S* media_query_list S* '{' S* ruleset* '}' S* - * ; - */ - var tokenStream = this._tokenStream, - line, - col, - mediaList;// = []; - - //look for @media - tokenStream.mustMatch(Tokens.MEDIA_SYM); - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - - mediaList = this._media_query_list(); - - tokenStream.mustMatch(Tokens.LBRACE); - this._readWhitespace(); - - this.fire({ - type: "startmedia", - media: mediaList, - line: line, - col: col - }); - - while (true) { - if (tokenStream.peek() === Tokens.PAGE_SYM) { - this._page(); - } else if (tokenStream.peek() === Tokens.FONT_FACE_SYM) { - this._font_face(); - } else if (tokenStream.peek() === Tokens.VIEWPORT_SYM) { - this._viewport(); - } else if (tokenStream.peek() === Tokens.DOCUMENT_SYM) { - this._document(); - } else if (tokenStream.peek() === Tokens.SUPPORTS_SYM) { - this._supports(); - } else if (tokenStream.peek() === Tokens.MEDIA_SYM) { - this._media(); - } else if (!this._ruleset()) { - break; - } - } - - tokenStream.mustMatch(Tokens.RBRACE); - this._readWhitespace(); - - this.fire({ - type: "endmedia", - media: mediaList, - line: line, - col: col - }); - }, - - - //CSS3 Media Queries - _media_query_list: function() { - /* - * media_query_list - * : S* [media_query [ ',' S* media_query ]* ]? - * ; - */ - var tokenStream = this._tokenStream, - mediaList = []; - - - this._readWhitespace(); - - if (tokenStream.peek() === Tokens.IDENT || tokenStream.peek() === Tokens.LPAREN) { - mediaList.push(this._media_query()); - } - - while (tokenStream.match(Tokens.COMMA)) { - this._readWhitespace(); - mediaList.push(this._media_query()); - } - - return mediaList; - }, - - /* - * Note: "expression" in the grammar maps to the _media_expression - * method. - - */ - _media_query: function() { - /* - * media_query - * : [ONLY | NOT]? S* media_type S* [ AND S* expression ]* - * | expression [ AND S* expression ]* - * ; - */ - var tokenStream = this._tokenStream, - type = null, - ident = null, - token = null, - expressions = []; - - if (tokenStream.match(Tokens.IDENT)) { - ident = tokenStream.token().value.toLowerCase(); - - //since there's no custom tokens for these, need to manually check - if (ident !== "only" && ident !== "not") { - tokenStream.unget(); - ident = null; - } else { - token = tokenStream.token(); - } - } - - this._readWhitespace(); - - if (tokenStream.peek() === Tokens.IDENT) { - type = this._media_type(); - if (token === null) { - token = tokenStream.token(); - } - } else if (tokenStream.peek() === Tokens.LPAREN) { - if (token === null) { - token = tokenStream.LT(1); - } - expressions.push(this._media_expression()); - } - - if (type === null && expressions.length === 0) { - return null; - } else { - this._readWhitespace(); - while (tokenStream.match(Tokens.IDENT)) { - if (tokenStream.token().value.toLowerCase() !== "and") { - this._unexpectedToken(tokenStream.token()); - } - - this._readWhitespace(); - expressions.push(this._media_expression()); - } - } - - return new MediaQuery(ident, type, expressions, token.startLine, token.startCol); - }, - - //CSS3 Media Queries - _media_type: function() { - /* - * media_type - * : IDENT - * ; - */ - return this._media_feature(); - }, - - /** - * Note: in CSS3 Media Queries, this is called "expression". - * Renamed here to avoid conflict with CSS3 Selectors - * definition of "expression". Also note that "expr" in the - * grammar now maps to "expression" from CSS3 selectors. - * @method _media_expression - * @private - */ - _media_expression: function() { - /* - * expression - * : '(' S* media_feature S* [ ':' S* expr ]? ')' S* - * ; - */ - var tokenStream = this._tokenStream, - feature = null, - token, - expression = null; - - tokenStream.mustMatch(Tokens.LPAREN); - - feature = this._media_feature(); - this._readWhitespace(); - - if (tokenStream.match(Tokens.COLON)) { - this._readWhitespace(); - token = tokenStream.LT(1); - expression = this._expression(); - } - - tokenStream.mustMatch(Tokens.RPAREN); - this._readWhitespace(); - - return new MediaFeature(feature, expression ? new SyntaxUnit(expression, token.startLine, token.startCol) : null); - }, - - //CSS3 Media Queries - _media_feature: function() { - /* - * media_feature - * : IDENT - * ; - */ - var tokenStream = this._tokenStream; - - this._readWhitespace(); - - tokenStream.mustMatch(Tokens.IDENT); - - return SyntaxUnit.fromToken(tokenStream.token()); - }, - - //CSS3 Paged Media - _page: function() { - /* - * page: - * PAGE_SYM S* IDENT? pseudo_page? S* - * '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* - * ; - */ - var tokenStream = this._tokenStream, - line, - col, - identifier = null, - pseudoPage = null; - - //look for @page - tokenStream.mustMatch(Tokens.PAGE_SYM); - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - - if (tokenStream.match(Tokens.IDENT)) { - identifier = tokenStream.token().value; - - //The value 'auto' may not be used as a page name and MUST be treated as a syntax error. - if (identifier.toLowerCase() === "auto") { - this._unexpectedToken(tokenStream.token()); - } - } - - //see if there's a colon upcoming - if (tokenStream.peek() === Tokens.COLON) { - pseudoPage = this._pseudo_page(); - } - - this._readWhitespace(); - - this.fire({ - type: "startpage", - id: identifier, - pseudo: pseudoPage, - line: line, - col: col - }); - - this._readDeclarations(true, true); - - this.fire({ - type: "endpage", - id: identifier, - pseudo: pseudoPage, - line: line, - col: col - }); - - }, - - //CSS3 Paged Media - _margin: function() { - /* - * margin : - * margin_sym S* '{' declaration [ ';' S* declaration? ]* '}' S* - * ; - */ - var tokenStream = this._tokenStream, - line, - col, - marginSym = this._margin_sym(); - - if (marginSym) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this.fire({ - type: "startpagemargin", - margin: marginSym, - line: line, - col: col - }); - - this._readDeclarations(true); - - this.fire({ - type: "endpagemargin", - margin: marginSym, - line: line, - col: col - }); - return true; - } else { - return false; - } - }, - - //CSS3 Paged Media - _margin_sym: function() { - - /* - * margin_sym : - * TOPLEFTCORNER_SYM | - * TOPLEFT_SYM | - * TOPCENTER_SYM | - * TOPRIGHT_SYM | - * TOPRIGHTCORNER_SYM | - * BOTTOMLEFTCORNER_SYM | - * BOTTOMLEFT_SYM | - * BOTTOMCENTER_SYM | - * BOTTOMRIGHT_SYM | - * BOTTOMRIGHTCORNER_SYM | - * LEFTTOP_SYM | - * LEFTMIDDLE_SYM | - * LEFTBOTTOM_SYM | - * RIGHTTOP_SYM | - * RIGHTMIDDLE_SYM | - * RIGHTBOTTOM_SYM - * ; - */ - - var tokenStream = this._tokenStream; - - if (tokenStream.match([Tokens.TOPLEFTCORNER_SYM, Tokens.TOPLEFT_SYM, - Tokens.TOPCENTER_SYM, Tokens.TOPRIGHT_SYM, Tokens.TOPRIGHTCORNER_SYM, - Tokens.BOTTOMLEFTCORNER_SYM, Tokens.BOTTOMLEFT_SYM, - Tokens.BOTTOMCENTER_SYM, Tokens.BOTTOMRIGHT_SYM, - Tokens.BOTTOMRIGHTCORNER_SYM, Tokens.LEFTTOP_SYM, - Tokens.LEFTMIDDLE_SYM, Tokens.LEFTBOTTOM_SYM, Tokens.RIGHTTOP_SYM, - Tokens.RIGHTMIDDLE_SYM, Tokens.RIGHTBOTTOM_SYM])) { - return SyntaxUnit.fromToken(tokenStream.token()); - } else { - return null; - } - - }, - - _pseudo_page: function() { - /* - * pseudo_page - * : ':' IDENT - * ; - */ - - var tokenStream = this._tokenStream; - - tokenStream.mustMatch(Tokens.COLON); - tokenStream.mustMatch(Tokens.IDENT); - - //TODO: CSS3 Paged Media says only "left", "center", and "right" are allowed - - return tokenStream.token().value; - }, - - _font_face: function() { - /* - * font_face - * : FONT_FACE_SYM S* - * '{' S* declaration [ ';' S* declaration ]* '}' S* - * ; - */ - var tokenStream = this._tokenStream, - line, - col; - - //look for @page - tokenStream.mustMatch(Tokens.FONT_FACE_SYM); - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - - this.fire({ - type: "startfontface", - line: line, - col: col - }); - - this._readDeclarations(true); - - this.fire({ - type: "endfontface", - line: line, - col: col - }); - }, - - _viewport: function() { - /* - * viewport - * : VIEWPORT_SYM S* - * '{' S* declaration? [ ';' S* declaration? ]* '}' S* - * ; - */ - var tokenStream = this._tokenStream, - line, - col; - - tokenStream.mustMatch(Tokens.VIEWPORT_SYM); - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - - this._readWhitespace(); - - this.fire({ - type: "startviewport", - line: line, - col: col - }); - - this._readDeclarations(true); - - this.fire({ - type: "endviewport", - line: line, - col: col - }); - - }, - - _document: function() { - /* - * document - * : DOCUMENT_SYM S* - * _document_function [ ',' S* _document_function ]* S* - * '{' S* ruleset* '}' - * ; - */ - - var tokenStream = this._tokenStream, - token, - functions = [], - prefix = ""; - - tokenStream.mustMatch(Tokens.DOCUMENT_SYM); - token = tokenStream.token(); - if (/^@\-([^\-]+)\-/.test(token.value)) { - prefix = RegExp.$1; - } - - this._readWhitespace(); - functions.push(this._document_function()); - - while (tokenStream.match(Tokens.COMMA)) { - this._readWhitespace(); - functions.push(this._document_function()); - } - - tokenStream.mustMatch(Tokens.LBRACE); - this._readWhitespace(); - - this.fire({ - type: "startdocument", - functions: functions, - prefix: prefix, - line: token.startLine, - col: token.startCol - }); - - var ok = true; - while (ok) { - switch (tokenStream.peek()) { - case Tokens.PAGE_SYM: - this._page(); - break; - case Tokens.FONT_FACE_SYM: - this._font_face(); - break; - case Tokens.VIEWPORT_SYM: - this._viewport(); - break; - case Tokens.MEDIA_SYM: - this._media(); - break; - case Tokens.KEYFRAMES_SYM: - this._keyframes(); - break; - case Tokens.DOCUMENT_SYM: - this._document(); - break; - default: - ok = Boolean(this._ruleset()); - } - } - - tokenStream.mustMatch(Tokens.RBRACE); - token = tokenStream.token(); - this._readWhitespace(); - - this.fire({ - type: "enddocument", - functions: functions, - prefix: prefix, - line: token.startLine, - col: token.startCol - }); - }, - - _document_function: function() { - /* - * document_function - * : function | URI S* - * ; - */ - - var tokenStream = this._tokenStream, - value; - - if (tokenStream.match(Tokens.URI)) { - value = tokenStream.token().value; - this._readWhitespace(); - } else { - value = this._function(); - } - - return value; - }, - - _operator: function(inFunction) { - - /* - * operator (outside function) - * : '/' S* | ',' S* | /( empty )/ - * operator (inside function) - * : '/' S* | '+' S* | '*' S* | '-' S* /( empty )/ - * ; - */ - - var tokenStream = this._tokenStream, - token = null; - - if (tokenStream.match([Tokens.SLASH, Tokens.COMMA]) || - (inFunction && tokenStream.match([Tokens.PLUS, Tokens.STAR, Tokens.MINUS]))) { - token = tokenStream.token(); - this._readWhitespace(); - } - return token ? PropertyValuePart.fromToken(token) : null; - - }, - - _combinator: function() { - - /* - * combinator - * : PLUS S* | GREATER S* | TILDE S* | S+ - * ; - */ - - var tokenStream = this._tokenStream, - value = null, - token; - - if (tokenStream.match([Tokens.PLUS, Tokens.GREATER, Tokens.TILDE])) { - token = tokenStream.token(); - value = new Combinator(token.value, token.startLine, token.startCol); - this._readWhitespace(); - } - - return value; - }, - - _unary_operator: function() { - - /* - * unary_operator - * : '-' | '+' - * ; - */ - - var tokenStream = this._tokenStream; - - if (tokenStream.match([Tokens.MINUS, Tokens.PLUS])) { - return tokenStream.token().value; - } else { - return null; - } - }, - - _property: function() { - - /* - * property - * : IDENT S* - * ; - */ - - var tokenStream = this._tokenStream, - value = null, - hack = null, - tokenValue, - token, - line, - col; - - //check for star hack - throws error if not allowed - if (tokenStream.peek() === Tokens.STAR && this.options.starHack) { - tokenStream.get(); - token = tokenStream.token(); - hack = token.value; - line = token.startLine; - col = token.startCol; - } - - if (tokenStream.match(Tokens.IDENT)) { - token = tokenStream.token(); - tokenValue = token.value; - - //check for underscore hack - no error if not allowed because it's valid CSS syntax - if (tokenValue.charAt(0) === "_" && this.options.underscoreHack) { - hack = "_"; - tokenValue = tokenValue.substring(1); - } - - value = new PropertyName(tokenValue, hack, (line||token.startLine), (col||token.startCol)); - this._readWhitespace(); - } - - return value; - }, - - //Augmented with CSS3 Selectors - _ruleset: function() { - /* - * ruleset - * : selectors_group - * '{' S* declaration? [ ';' S* declaration? ]* '}' S* - * ; - */ - - var tokenStream = this._tokenStream, - tt, - selectors; - - - /* - * Error Recovery: If even a single selector fails to parse, - * then the entire ruleset should be thrown away. - */ - try { - selectors = this._selectors_group(); - } catch (ex) { - if (ex instanceof SyntaxError && !this.options.strict) { - - //fire error event - this.fire({ - type: "error", - error: ex, - message: ex.message, - line: ex.line, - col: ex.col - }); - - //skip over everything until closing brace - tt = tokenStream.advance([Tokens.RBRACE]); - if (tt === Tokens.RBRACE) { - //if there's a right brace, the rule is finished so don't do anything - } else { - //otherwise, rethrow the error because it wasn't handled properly - throw ex; - } - - } else { - //not a syntax error, rethrow it - throw ex; - } - - //trigger parser to continue - return true; - } - - //if it got here, all selectors parsed - if (selectors) { - - this.fire({ - type: "startrule", - selectors: selectors, - line: selectors[0].line, - col: selectors[0].col - }); - - this._readDeclarations(true); - - this.fire({ - type: "endrule", - selectors: selectors, - line: selectors[0].line, - col: selectors[0].col - }); - - } - - return selectors; - - }, - - //CSS3 Selectors - _selectors_group: function() { - - /* - * selectors_group - * : selector [ COMMA S* selector ]* - * ; - */ - var tokenStream = this._tokenStream, - selectors = [], - selector; - - selector = this._selector(); - if (selector !== null) { - - selectors.push(selector); - while (tokenStream.match(Tokens.COMMA)) { - this._readWhitespace(); - selector = this._selector(); - if (selector !== null) { - selectors.push(selector); - } else { - this._unexpectedToken(tokenStream.LT(1)); - } - } - } - - return selectors.length ? selectors : null; - }, - - //CSS3 Selectors - _selector: function() { - /* - * selector - * : simple_selector_sequence [ combinator simple_selector_sequence ]* - * ; - */ - - var tokenStream = this._tokenStream, - selector = [], - nextSelector = null, - combinator = null, - ws = null; - - //if there's no simple selector, then there's no selector - nextSelector = this._simple_selector_sequence(); - if (nextSelector === null) { - return null; - } - - selector.push(nextSelector); - - do { - - //look for a combinator - combinator = this._combinator(); - - if (combinator !== null) { - selector.push(combinator); - nextSelector = this._simple_selector_sequence(); - - //there must be a next selector - if (nextSelector === null) { - this._unexpectedToken(tokenStream.LT(1)); - } else { - - //nextSelector is an instance of SelectorPart - selector.push(nextSelector); - } - } else { - - //if there's not whitespace, we're done - if (this._readWhitespace()) { - - //add whitespace separator - ws = new Combinator(tokenStream.token().value, tokenStream.token().startLine, tokenStream.token().startCol); - - //combinator is not required - combinator = this._combinator(); - - //selector is required if there's a combinator - nextSelector = this._simple_selector_sequence(); - if (nextSelector === null) { - if (combinator !== null) { - this._unexpectedToken(tokenStream.LT(1)); - } - } else { - - if (combinator !== null) { - selector.push(combinator); - } else { - selector.push(ws); - } - - selector.push(nextSelector); - } - } else { - break; - } - - } - } while (true); - - return new Selector(selector, selector[0].line, selector[0].col); - }, - - //CSS3 Selectors - _simple_selector_sequence: function() { - /* - * simple_selector_sequence - * : [ type_selector | universal ] - * [ HASH | class | attrib | pseudo | negation ]* - * | [ HASH | class | attrib | pseudo | negation ]+ - * ; - */ - - var tokenStream = this._tokenStream, - - //parts of a simple selector - elementName = null, - modifiers = [], - - //complete selector text - selectorText= "", - - //the different parts after the element name to search for - components = [ - //HASH - function() { - return tokenStream.match(Tokens.HASH) ? - new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) : - null; - }, - this._class, - this._attrib, - this._pseudo, - this._negation - ], - i = 0, - len = components.length, - component = null, - line, - col; - - - //get starting line and column for the selector - line = tokenStream.LT(1).startLine; - col = tokenStream.LT(1).startCol; - - elementName = this._type_selector(); - if (!elementName) { - elementName = this._universal(); - } - - if (elementName !== null) { - selectorText += elementName; - } - - while (true) { - - //whitespace means we're done - if (tokenStream.peek() === Tokens.S) { - break; - } - - //check for each component - while (i < len && component === null) { - component = components[i++].call(this); - } - - if (component === null) { - - //we don't have a selector - if (selectorText === "") { - return null; - } else { - break; - } - } else { - i = 0; - modifiers.push(component); - selectorText += component.toString(); - component = null; - } - } - - - return selectorText !== "" ? - new SelectorPart(elementName, modifiers, selectorText, line, col) : - null; - }, - - //CSS3 Selectors - _type_selector: function() { - /* - * type_selector - * : [ namespace_prefix ]? element_name - * ; - */ - - var tokenStream = this._tokenStream, - ns = this._namespace_prefix(), - elementName = this._element_name(); - - if (!elementName) { - /* - * Need to back out the namespace that was read due to both - * type_selector and universal reading namespace_prefix - * first. Kind of hacky, but only way I can figure out - * right now how to not change the grammar. - */ - if (ns) { - tokenStream.unget(); - if (ns.length > 1) { - tokenStream.unget(); - } - } - - return null; - } else { - if (ns) { - elementName.text = ns + elementName.text; - elementName.col -= ns.length; - } - return elementName; - } - }, - - //CSS3 Selectors - _class: function() { - /* - * class - * : '.' IDENT - * ; - */ - - var tokenStream = this._tokenStream, - token; - - if (tokenStream.match(Tokens.DOT)) { - tokenStream.mustMatch(Tokens.IDENT); - token = tokenStream.token(); - return new SelectorSubPart("." + token.value, "class", token.startLine, token.startCol - 1); - } else { - return null; - } - - }, - - //CSS3 Selectors - _element_name: function() { - /* - * element_name - * : IDENT - * ; - */ - - var tokenStream = this._tokenStream, - token; - - if (tokenStream.match(Tokens.IDENT)) { - token = tokenStream.token(); - return new SelectorSubPart(token.value, "elementName", token.startLine, token.startCol); - - } else { - return null; - } - }, - - //CSS3 Selectors - _namespace_prefix: function() { - /* - * namespace_prefix - * : [ IDENT | '*' ]? '|' - * ; - */ - var tokenStream = this._tokenStream, - value = ""; - - //verify that this is a namespace prefix - if (tokenStream.LA(1) === Tokens.PIPE || tokenStream.LA(2) === Tokens.PIPE) { - - if (tokenStream.match([Tokens.IDENT, Tokens.STAR])) { - value += tokenStream.token().value; - } - - tokenStream.mustMatch(Tokens.PIPE); - value += "|"; - - } - - return value.length ? value : null; - }, - - //CSS3 Selectors - _universal: function() { - /* - * universal - * : [ namespace_prefix ]? '*' - * ; - */ - var tokenStream = this._tokenStream, - value = "", - ns; - - ns = this._namespace_prefix(); - if (ns) { - value += ns; - } - - if (tokenStream.match(Tokens.STAR)) { - value += "*"; - } - - return value.length ? value : null; - - }, - - //CSS3 Selectors - _attrib: function() { - /* - * attrib - * : '[' S* [ namespace_prefix ]? IDENT S* - * [ [ PREFIXMATCH | - * SUFFIXMATCH | - * SUBSTRINGMATCH | - * '=' | - * INCLUDES | - * DASHMATCH ] S* [ IDENT | STRING ] S* - * ]? ']' - * ; - */ - - var tokenStream = this._tokenStream, - value = null, - ns, - token; - - if (tokenStream.match(Tokens.LBRACKET)) { - token = tokenStream.token(); - value = token.value; - value += this._readWhitespace(); - - ns = this._namespace_prefix(); - - if (ns) { - value += ns; - } - - tokenStream.mustMatch(Tokens.IDENT); - value += tokenStream.token().value; - value += this._readWhitespace(); - - if (tokenStream.match([Tokens.PREFIXMATCH, Tokens.SUFFIXMATCH, Tokens.SUBSTRINGMATCH, - Tokens.EQUALS, Tokens.INCLUDES, Tokens.DASHMATCH])) { - - value += tokenStream.token().value; - value += this._readWhitespace(); - - tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); - value += tokenStream.token().value; - value += this._readWhitespace(); - } - - tokenStream.mustMatch(Tokens.RBRACKET); - - return new SelectorSubPart(value + "]", "attribute", token.startLine, token.startCol); - } else { - return null; - } - }, - - //CSS3 Selectors - _pseudo: function() { - - /* - * pseudo - * : ':' ':'? [ IDENT | functional_pseudo ] - * ; - */ - - var tokenStream = this._tokenStream, - pseudo = null, - colons = ":", - line, - col; - - if (tokenStream.match(Tokens.COLON)) { - - if (tokenStream.match(Tokens.COLON)) { - colons += ":"; - } - - if (tokenStream.match(Tokens.IDENT)) { - pseudo = tokenStream.token().value; - line = tokenStream.token().startLine; - col = tokenStream.token().startCol - colons.length; - } else if (tokenStream.peek() === Tokens.FUNCTION) { - line = tokenStream.LT(1).startLine; - col = tokenStream.LT(1).startCol - colons.length; - pseudo = this._functional_pseudo(); - } - - if (pseudo) { - pseudo = new SelectorSubPart(colons + pseudo, "pseudo", line, col); - } else { - var startLine = tokenStream.LT(1).startLine, - startCol = tokenStream.LT(0).startCol; - throw new SyntaxError("Expected a `FUNCTION` or `IDENT` after colon at line " + startLine + ", col " + startCol + ".", startLine, startCol); - } - } - - return pseudo; - }, - - //CSS3 Selectors - _functional_pseudo: function() { - /* - * functional_pseudo - * : FUNCTION S* expression ')' - * ; - */ - - var tokenStream = this._tokenStream, - value = null; - - if (tokenStream.match(Tokens.FUNCTION)) { - value = tokenStream.token().value; - value += this._readWhitespace(); - value += this._expression(); - tokenStream.mustMatch(Tokens.RPAREN); - value += ")"; - } - - return value; - }, - - //CSS3 Selectors - _expression: function() { - /* - * expression - * : [ [ PLUS | '-' | DIMENSION | NUMBER | STRING | IDENT ] S* ]+ - * ; - */ - - var tokenStream = this._tokenStream, - value = ""; - - while (tokenStream.match([Tokens.PLUS, Tokens.MINUS, Tokens.DIMENSION, - Tokens.NUMBER, Tokens.STRING, Tokens.IDENT, Tokens.LENGTH, - Tokens.FREQ, Tokens.ANGLE, Tokens.TIME, - Tokens.RESOLUTION, Tokens.SLASH])) { - - value += tokenStream.token().value; - value += this._readWhitespace(); - } - - return value.length ? value : null; - - }, - - //CSS3 Selectors - _negation: function() { - /* - * negation - * : NOT S* negation_arg S* ')' - * ; - */ - - var tokenStream = this._tokenStream, - line, - col, - value = "", - arg, - subpart = null; - - if (tokenStream.match(Tokens.NOT)) { - value = tokenStream.token().value; - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - value += this._readWhitespace(); - arg = this._negation_arg(); - value += arg; - value += this._readWhitespace(); - tokenStream.match(Tokens.RPAREN); - value += tokenStream.token().value; - - subpart = new SelectorSubPart(value, "not", line, col); - subpart.args.push(arg); - } - - return subpart; - }, - - //CSS3 Selectors - _negation_arg: function() { - /* - * negation_arg - * : type_selector | universal | HASH | class | attrib | pseudo - * ; - */ - - var tokenStream = this._tokenStream, - args = [ - this._type_selector, - this._universal, - function() { - return tokenStream.match(Tokens.HASH) ? - new SelectorSubPart(tokenStream.token().value, "id", tokenStream.token().startLine, tokenStream.token().startCol) : - null; - }, - this._class, - this._attrib, - this._pseudo - ], - arg = null, - i = 0, - len = args.length, - line, - col, - part; - - line = tokenStream.LT(1).startLine; - col = tokenStream.LT(1).startCol; - - while (i < len && arg === null) { - - arg = args[i].call(this); - i++; - } - - //must be a negation arg - if (arg === null) { - this._unexpectedToken(tokenStream.LT(1)); - } - - //it's an element name - if (arg.type === "elementName") { - part = new SelectorPart(arg, [], arg.toString(), line, col); - } else { - part = new SelectorPart(null, [arg], arg.toString(), line, col); - } - - return part; - }, - - _declaration: function() { - - /* - * declaration - * : property ':' S* expr prio? - * | /( empty )/ - * ; - */ - - var tokenStream = this._tokenStream, - property = null, - expr = null, - prio = null, - invalid = null, - propertyName= ""; - - property = this._property(); - if (property !== null) { - - tokenStream.mustMatch(Tokens.COLON); - this._readWhitespace(); - - expr = this._expr(); - - //if there's no parts for the value, it's an error - if (!expr || expr.length === 0) { - this._unexpectedToken(tokenStream.LT(1)); - } - - prio = this._prio(); - - /* - * If hacks should be allowed, then only check the root - * property. If hacks should not be allowed, treat - * _property or *property as invalid properties. - */ - propertyName = property.toString(); - if (this.options.starHack && property.hack === "*" || - this.options.underscoreHack && property.hack === "_") { - - propertyName = property.text; - } - - try { - this._validateProperty(propertyName, expr); - } catch (ex) { - invalid = ex; - } - - this.fire({ - type: "property", - property: property, - value: expr, - important: prio, - line: property.line, - col: property.col, - invalid: invalid - }); - - return true; - } else { - return false; - } - }, - - _prio: function() { - /* - * prio - * : IMPORTANT_SYM S* - * ; - */ - - var tokenStream = this._tokenStream, - result = tokenStream.match(Tokens.IMPORTANT_SYM); - - this._readWhitespace(); - return result; - }, - - _expr: function(inFunction) { - /* - * expr - * : term [ operator term ]* - * ; - */ - - var values = [], - //valueParts = [], - value = null, - operator = null; - - value = this._term(inFunction); - if (value !== null) { - - values.push(value); - - do { - operator = this._operator(inFunction); - - //if there's an operator, keep building up the value parts - if (operator) { - values.push(operator); - } /*else { - //if there's not an operator, you have a full value - values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); - valueParts = []; - }*/ - - value = this._term(inFunction); - - if (value === null) { - break; - } else { - values.push(value); - } - } while (true); - } - - //cleanup - /*if (valueParts.length) { - values.push(new PropertyValue(valueParts, valueParts[0].line, valueParts[0].col)); - }*/ - - return values.length > 0 ? new PropertyValue(values, values[0].line, values[0].col) : null; - }, - - _term: function(inFunction) { - - /* - * term - * : unary_operator? - * [ NUMBER S* | PERCENTAGE S* | LENGTH S* | ANGLE S* | - * TIME S* | FREQ S* | function | ie_function ] - * | STRING S* | IDENT S* | URI S* | UNICODERANGE S* | hexcolor - * ; - */ - - var tokenStream = this._tokenStream, - unary = null, - value = null, - endChar = null, - part = null, - token, - line, - col; - - //returns the operator or null - unary = this._unary_operator(); - if (unary !== null) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - } - - //exception for IE filters - if (tokenStream.peek() === Tokens.IE_FUNCTION && this.options.ieFilters) { - - value = this._ie_function(); - if (unary === null) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - } - - //see if it's a simple block - } else if (inFunction && tokenStream.match([Tokens.LPAREN, Tokens.LBRACE, Tokens.LBRACKET])) { - - token = tokenStream.token(); - endChar = token.endChar; - value = token.value + this._expr(inFunction).text; - if (unary === null) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - } - tokenStream.mustMatch(Tokens.type(endChar)); - value += endChar; - this._readWhitespace(); - - //see if there's a simple match - } else if (tokenStream.match([Tokens.NUMBER, Tokens.PERCENTAGE, Tokens.LENGTH, - Tokens.ANGLE, Tokens.TIME, - Tokens.FREQ, Tokens.STRING, Tokens.IDENT, Tokens.URI, Tokens.UNICODE_RANGE])) { - - value = tokenStream.token().value; - if (unary === null) { - line = tokenStream.token().startLine; - col = tokenStream.token().startCol; - // Correct potentially-inaccurate IDENT parsing in - // PropertyValuePart constructor. - part = PropertyValuePart.fromToken(tokenStream.token()); - } - this._readWhitespace(); - } else { - - //see if it's a color - token = this._hexcolor(); - if (token === null) { - - //if there's no unary, get the start of the next token for line/col info - if (unary === null) { - line = tokenStream.LT(1).startLine; - col = tokenStream.LT(1).startCol; - } - - //has to be a function - if (value === null) { - - /* - * This checks for alpha(opacity=0) style of IE - * functions. IE_FUNCTION only presents progid: style. - */ - if (tokenStream.LA(3) === Tokens.EQUALS && this.options.ieFilters) { - value = this._ie_function(); - } else { - value = this._function(); - } - } - - /*if (value === null) { - return null; - //throw new Error("Expected identifier at line " + tokenStream.token().startLine + ", character " + tokenStream.token().startCol + "."); - }*/ - - } else { - value = token.value; - if (unary === null) { - line = token.startLine; - col = token.startCol; - } - } - - } - - return part !== null ? part : value !== null ? - new PropertyValuePart(unary !== null ? unary + value : value, line, col) : - null; - - }, - - _function: function() { - - /* - * function - * : FUNCTION S* expr ')' S* - * ; - */ - - var tokenStream = this._tokenStream, - functionText = null, - expr = null, - lt; - - if (tokenStream.match(Tokens.FUNCTION)) { - functionText = tokenStream.token().value; - this._readWhitespace(); - expr = this._expr(true); - functionText += expr; - - //START: Horrible hack in case it's an IE filter - if (this.options.ieFilters && tokenStream.peek() === Tokens.EQUALS) { - do { - - if (this._readWhitespace()) { - functionText += tokenStream.token().value; - } - - //might be second time in the loop - if (tokenStream.LA(0) === Tokens.COMMA) { - functionText += tokenStream.token().value; - } - - tokenStream.match(Tokens.IDENT); - functionText += tokenStream.token().value; - - tokenStream.match(Tokens.EQUALS); - functionText += tokenStream.token().value; - - //functionText += this._term(); - lt = tokenStream.peek(); - while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { - tokenStream.get(); - functionText += tokenStream.token().value; - lt = tokenStream.peek(); - } - } while (tokenStream.match([Tokens.COMMA, Tokens.S])); - } - - //END: Horrible Hack - - tokenStream.match(Tokens.RPAREN); - functionText += ")"; - this._readWhitespace(); - } - - return functionText; - }, - - _ie_function: function() { - - /* (My own extension) - * ie_function - * : IE_FUNCTION S* IDENT '=' term [S* ','? IDENT '=' term]+ ')' S* - * ; - */ - - var tokenStream = this._tokenStream, - functionText = null, - lt; - - //IE function can begin like a regular function, too - if (tokenStream.match([Tokens.IE_FUNCTION, Tokens.FUNCTION])) { - functionText = tokenStream.token().value; - - do { - - if (this._readWhitespace()) { - functionText += tokenStream.token().value; - } - - //might be second time in the loop - if (tokenStream.LA(0) === Tokens.COMMA) { - functionText += tokenStream.token().value; - } - - tokenStream.match(Tokens.IDENT); - functionText += tokenStream.token().value; - - tokenStream.match(Tokens.EQUALS); - functionText += tokenStream.token().value; - - //functionText += this._term(); - lt = tokenStream.peek(); - while (lt !== Tokens.COMMA && lt !== Tokens.S && lt !== Tokens.RPAREN) { - tokenStream.get(); - functionText += tokenStream.token().value; - lt = tokenStream.peek(); - } - } while (tokenStream.match([Tokens.COMMA, Tokens.S])); - - tokenStream.match(Tokens.RPAREN); - functionText += ")"; - this._readWhitespace(); - } - - return functionText; - }, - - _hexcolor: function() { - /* - * There is a constraint on the color that it must - * have either 3 or 6 hex-digits (i.e., [0-9a-fA-F]) - * after the "#"; e.g., "#000" is OK, but "#abcd" is not. - * - * hexcolor - * : HASH S* - * ; - */ - - var tokenStream = this._tokenStream, - token = null, - color; - - if (tokenStream.match(Tokens.HASH)) { - - //need to do some validation here - - token = tokenStream.token(); - color = token.value; - if (!/#[a-f0-9]{3,6}/i.test(color)) { - throw new SyntaxError("Expected a hex color but found '" + color + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); - } - this._readWhitespace(); - } - - return token; - }, - - //----------------------------------------------------------------- - // Animations methods - //----------------------------------------------------------------- - - _keyframes: function() { - - /* - * keyframes: - * : KEYFRAMES_SYM S* keyframe_name S* '{' S* keyframe_rule* '}' { - * ; - */ - var tokenStream = this._tokenStream, - token, - tt, - name, - prefix = ""; - - tokenStream.mustMatch(Tokens.KEYFRAMES_SYM); - token = tokenStream.token(); - if (/^@\-([^\-]+)\-/.test(token.value)) { - prefix = RegExp.$1; - } - - this._readWhitespace(); - name = this._keyframe_name(); - - this._readWhitespace(); - tokenStream.mustMatch(Tokens.LBRACE); - - this.fire({ - type: "startkeyframes", - name: name, - prefix: prefix, - line: token.startLine, - col: token.startCol - }); - - this._readWhitespace(); - tt = tokenStream.peek(); - - //check for key - while (tt === Tokens.IDENT || tt === Tokens.PERCENTAGE) { - this._keyframe_rule(); - this._readWhitespace(); - tt = tokenStream.peek(); - } - - this.fire({ - type: "endkeyframes", - name: name, - prefix: prefix, - line: token.startLine, - col: token.startCol - }); - - this._readWhitespace(); - tokenStream.mustMatch(Tokens.RBRACE); - this._readWhitespace(); - - }, - - _keyframe_name: function() { - - /* - * keyframe_name: - * : IDENT - * | STRING - * ; - */ - var tokenStream = this._tokenStream; - - tokenStream.mustMatch([Tokens.IDENT, Tokens.STRING]); - return SyntaxUnit.fromToken(tokenStream.token()); - }, - - _keyframe_rule: function() { - - /* - * keyframe_rule: - * : key_list S* - * '{' S* declaration [ ';' S* declaration ]* '}' S* - * ; - */ - var keyList = this._key_list(); - - this.fire({ - type: "startkeyframerule", - keys: keyList, - line: keyList[0].line, - col: keyList[0].col - }); - - this._readDeclarations(true); - - this.fire({ - type: "endkeyframerule", - keys: keyList, - line: keyList[0].line, - col: keyList[0].col - }); - - }, - - _key_list: function() { - - /* - * key_list: - * : key [ S* ',' S* key]* - * ; - */ - var tokenStream = this._tokenStream, - keyList = []; - - //must be least one key - keyList.push(this._key()); - - this._readWhitespace(); - - while (tokenStream.match(Tokens.COMMA)) { - this._readWhitespace(); - keyList.push(this._key()); - this._readWhitespace(); - } - - return keyList; - }, - - _key: function() { - /* - * There is a restriction that IDENT can be only "from" or "to". - * - * key - * : PERCENTAGE - * | IDENT - * ; - */ - - var tokenStream = this._tokenStream, - token; - - if (tokenStream.match(Tokens.PERCENTAGE)) { - return SyntaxUnit.fromToken(tokenStream.token()); - } else if (tokenStream.match(Tokens.IDENT)) { - token = tokenStream.token(); - - if (/from|to/i.test(token.value)) { - return SyntaxUnit.fromToken(token); - } - - tokenStream.unget(); - } - - //if it gets here, there wasn't a valid token, so time to explode - this._unexpectedToken(tokenStream.LT(1)); - }, - - //----------------------------------------------------------------- - // Helper methods - //----------------------------------------------------------------- - - /** - * Not part of CSS grammar, but useful for skipping over - * combination of white space and HTML-style comments. - * @return {void} - * @method _skipCruft - * @private - */ - _skipCruft: function() { - while (this._tokenStream.match([Tokens.S, Tokens.CDO, Tokens.CDC])) { - //noop - } - }, - - /** - * Not part of CSS grammar, but this pattern occurs frequently - * in the official CSS grammar. Split out here to eliminate - * duplicate code. - * @param {Boolean} checkStart Indicates if the rule should check - * for the left brace at the beginning. - * @param {Boolean} readMargins Indicates if the rule should check - * for margin patterns. - * @return {void} - * @method _readDeclarations - * @private - */ - _readDeclarations: function(checkStart, readMargins) { - /* - * Reads the pattern - * S* '{' S* declaration [ ';' S* declaration ]* '}' S* - * or - * S* '{' S* [ declaration | margin ]? [ ';' S* [ declaration | margin ]? ]* '}' S* - * Note that this is how it is described in CSS3 Paged Media, but is actually incorrect. - * A semicolon is only necessary following a declaration if there's another declaration - * or margin afterwards. - */ - var tokenStream = this._tokenStream, - tt; - - - this._readWhitespace(); - - if (checkStart) { - tokenStream.mustMatch(Tokens.LBRACE); - } - - this._readWhitespace(); - - try { - - while (true) { - - if (tokenStream.match(Tokens.SEMICOLON) || (readMargins && this._margin())) { - //noop - } else if (this._declaration()) { - if (!tokenStream.match(Tokens.SEMICOLON)) { - break; - } - } else { - break; - } - - //if ((!this._margin() && !this._declaration()) || !tokenStream.match(Tokens.SEMICOLON)){ - // break; - //} - this._readWhitespace(); - } - - tokenStream.mustMatch(Tokens.RBRACE); - this._readWhitespace(); - - } catch (ex) { - if (ex instanceof SyntaxError && !this.options.strict) { - - //fire error event - this.fire({ - type: "error", - error: ex, - message: ex.message, - line: ex.line, - col: ex.col - }); - - //see if there's another declaration - tt = tokenStream.advance([Tokens.SEMICOLON, Tokens.RBRACE]); - if (tt === Tokens.SEMICOLON) { - //if there's a semicolon, then there might be another declaration - this._readDeclarations(false, readMargins); - } else if (tt !== Tokens.RBRACE) { - //if there's a right brace, the rule is finished so don't do anything - //otherwise, rethrow the error because it wasn't handled properly - throw ex; - } - - } else { - //not a syntax error, rethrow it - throw ex; - } - } - - }, - - /** - * In some cases, you can end up with two white space tokens in a - * row. Instead of making a change in every function that looks for - * white space, this function is used to match as much white space - * as necessary. - * @method _readWhitespace - * @return {String} The white space if found, empty string if not. - * @private - */ - _readWhitespace: function() { - - var tokenStream = this._tokenStream, - ws = ""; - - while (tokenStream.match(Tokens.S)) { - ws += tokenStream.token().value; - } - - return ws; - }, - - - /** - * Throws an error when an unexpected token is found. - * @param {Object} token The token that was found. - * @method _unexpectedToken - * @return {void} - * @private - */ - _unexpectedToken: function(token) { - throw new SyntaxError("Unexpected token '" + token.value + "' at line " + token.startLine + ", col " + token.startCol + ".", token.startLine, token.startCol); - }, - - /** - * Helper method used for parsing subparts of a style sheet. - * @return {void} - * @method _verifyEnd - * @private - */ - _verifyEnd: function() { - if (this._tokenStream.LA(1) !== Tokens.EOF) { - this._unexpectedToken(this._tokenStream.LT(1)); - } - }, - - //----------------------------------------------------------------- - // Validation methods - //----------------------------------------------------------------- - _validateProperty: function(property, value) { - Validation.validate(property, value); - }, - - //----------------------------------------------------------------- - // Parsing methods - //----------------------------------------------------------------- - - parse: function(input) { - this._tokenStream = new TokenStream(input, Tokens); - this._stylesheet(); - }, - - parseStyleSheet: function(input) { - //just passthrough - return this.parse(input); - }, - - parseMediaQuery: function(input) { - this._tokenStream = new TokenStream(input, Tokens); - var result = this._media_query(); - - //if there's anything more, then it's an invalid selector - this._verifyEnd(); - - //otherwise return result - return result; - }, - - /** - * Parses a property value (everything after the semicolon). - * @return {parserlib.css.PropertyValue} The property value. - * @throws parserlib.util.SyntaxError If an unexpected token is found. - * @method parserPropertyValue - */ - parsePropertyValue: function(input) { - - this._tokenStream = new TokenStream(input, Tokens); - this._readWhitespace(); - - var result = this._expr(); - - //okay to have a trailing white space - this._readWhitespace(); - - //if there's anything more, then it's an invalid selector - this._verifyEnd(); - - //otherwise return result - return result; - }, - - /** - * Parses a complete CSS rule, including selectors and - * properties. - * @param {String} input The text to parser. - * @return {Boolean} True if the parse completed successfully, false if not. - * @method parseRule - */ - parseRule: function(input) { - this._tokenStream = new TokenStream(input, Tokens); - - //skip any leading white space - this._readWhitespace(); - - var result = this._ruleset(); - - //skip any trailing white space - this._readWhitespace(); - - //if there's anything more, then it's an invalid selector - this._verifyEnd(); - - //otherwise return result - return result; - }, - - /** - * Parses a single CSS selector (no comma) - * @param {String} input The text to parse as a CSS selector. - * @return {Selector} An object representing the selector. - * @throws parserlib.util.SyntaxError If an unexpected token is found. - * @method parseSelector - */ - parseSelector: function(input) { - - this._tokenStream = new TokenStream(input, Tokens); - - //skip any leading white space - this._readWhitespace(); - - var result = this._selector(); - - //skip any trailing white space - this._readWhitespace(); - - //if there's anything more, then it's an invalid selector - this._verifyEnd(); - - //otherwise return result - return result; - }, - - /** - * Parses an HTML style attribute: a set of CSS declarations - * separated by semicolons. - * @param {String} input The text to parse as a style attribute - * @return {void} - * @method parseStyleAttribute - */ - parseStyleAttribute: function(input) { - input += "}"; // for error recovery in _readDeclarations() - this._tokenStream = new TokenStream(input, Tokens); - this._readDeclarations(); - } - }; - - //copy over onto prototype - for (prop in additions) { - if (Object.prototype.hasOwnProperty.call(additions, prop)) { - proto[prop] = additions[prop]; - } - } - - return proto; -}(); - - -/* -nth - : S* [ ['-'|'+']? INTEGER? {N} [ S* ['-'|'+'] S* INTEGER ]? | - ['-'|'+']? INTEGER | {O}{D}{D} | {E}{V}{E}{N} ] S* - ; -*/ - -},{"../util/EventTarget":23,"../util/SyntaxError":25,"../util/SyntaxUnit":26,"./Combinator":2,"./MediaFeature":4,"./MediaQuery":5,"./PropertyName":8,"./PropertyValue":9,"./PropertyValuePart":11,"./Selector":13,"./SelectorPart":14,"./SelectorSubPart":15,"./TokenStream":17,"./Tokens":18,"./Validation":19}],7:[function(require,module,exports){ -"use strict"; - -/* exported Properties */ - -var Properties = module.exports = { - __proto__: null, - - //A - "align-items" : "flex-start | flex-end | center | baseline | stretch", - "align-content" : "flex-start | flex-end | center | space-between | space-around | stretch", - "align-self" : "auto | flex-start | flex-end | center | baseline | stretch", - "all" : "initial | inherit | unset", - "-webkit-align-items" : "flex-start | flex-end | center | baseline | stretch", - "-webkit-align-content" : "flex-start | flex-end | center | space-between | space-around | stretch", - "-webkit-align-self" : "auto | flex-start | flex-end | center | baseline | stretch", - "alignment-adjust" : "auto | baseline | before-edge | text-before-edge | middle | central | after-edge | text-after-edge | ideographic | alphabetic | hanging | mathematical | | ", - "alignment-baseline" : "auto | baseline | use-script | before-edge | text-before-edge | after-edge | text-after-edge | central | middle | ideographic | alphabetic | hanging | mathematical", - "animation" : 1, - "animation-delay" : "

    ' . __( 'For more information:' ) . '

    ' . - '

    ' . __( 'Documentation on Dashboard' ) . '

    ' . - '

    ' . __( 'Support' ) . '

    ' . + '

    ' . __( 'Documentation on Dashboard' ) . '

    ' . + '

    ' . __( 'Support forums' ) . '

    ' . '

    ' . $wp_version_text . '

    ' ); @@ -154,20 +154,21 @@ // Only show the dashboard notice if it's been less than a minute since the message was postponed. if ( $time_passed < MINUTE_IN_SECONDS ) : - ?> -
    -

    - -

    -
    - - + $message = sprintf( + /* translators: %s: Human-readable time interval. */ + __( 'The admin email verification page will reappear after %s.' ), + human_time_diff( time() + $remind_interval ) + ); + wp_admin_notice( + $message, + array( + 'type' => 'success', + 'dismissible' => true, + ) + ); + endif; + endif; + ?> " aria-label=""> @@ -22,237 +41,243 @@

    - - +

    - -
    - -
    - -
    -
    +
    -

    - -

    -
    - -
    -
    - -
    -
    -

    - -

    -

    - Widgets dev note.' ), - 'https://make.wordpress.org/core/2021/06/29/block-based-widgets-editor-in-wordpress-5-8/' - ); - ?> -

    +
    +

    +

    -
    -
    -

    - -

    +
    +
    +

    - +
    +

    -
    - +
    +
    + +
    -
    -
    - +
    +
    +
    + +
    -
    -

    - -

    +
    +

    - +
    +

    -
    - -
    -

    - -

    -
    - -
    -
    - -
    -
    -

    - -

    +
    +
    +

    - +
    +

    -
    - -
    -
    -

    - -

    -

    - -

    -
    -
    - +
    +
    + +
    -
    -
    - +
    +
    +
    + +
    -
    -

    - -

    +
    +

    - +
    +

    -
    - -
    -

    - -

    -
    - - - - + + +
    +
    +
    + +
    +

    +

    +
    +
    +
    + +
    +

    +

    -
    + + +
    +

    -

    - -

    check out this dev note.' ), - 'https://make.wordpress.org/core/2021/06/25/introducing-theme-json-in-wordpress-5-8/' + /* translators: %s: Version number. */ + __( 'For a comprehensive overview of all the new features and enhancements in WordPress %s, please visit the feature-showcase website.' ), + $display_major_version ); ?>

    +
    +
    + +
    +
    +
    +
    -
    +
    + +
    +

    - -

    -

    switch to a more modern browser.' ), - 'https://browsehappy.com/' + /* translators: %s: Version number. */ + __( 'Learn more about WordPress %s' ), + $display_major_version ); ?> -

    -
    -
    -

    -

    Learn WordPress is a free resource for new and experienced WordPress users. Learn is stocked with how-to videos on using various features in WordPress, interactive workshops for exploring topics in-depth, and lesson plans for diving deep into specific areas of WordPress.' ), + 'https://learn.wordpress.org/', + 'https://learn.wordpress.org/online-workshops/' + ); ?>

    -
    -

    - -

    +
    + +
    +
    +
    + +
    +

    + + + +

    5.6 and 5.7, WordPress 5.8 introduces several new block support flags and new options to customize your registered blocks. More information is available in the block supports dev note.' ), - 'https://make.wordpress.org/core/2020/11/18/block-supports-in-wordpress-5-6/', - 'https://make.wordpress.org/core/2021/02/24/changes-to-block-editor-components-and-blocks/', - 'https://make.wordpress.org/core/2021/06/25/block-supports-api-updates-for-wordpress-5-8/' + /* translators: %s: WordPress version number. */ + __( 'Read the WordPress %s Release Notes for information on installation, enhancements, fixed issues, release contributors, learning resources, and the list of file changes.' ), + $display_major_version ); ?>

    -
    - -
    - -
    -
    -

    +
    +
    + +
    +

    + + + +

    WordPress 5.8 Field Guide.' ), - 'https://make.wordpress.org/core/2021/07/03/wordpress-5-8-field-guide/' + /* translators: %s: WordPress version number. */ + __( 'Explore the WordPress %s Field Guide. Learn about the changes in this release with detailed developer notes to help you build with WordPress.' ), + $display_major_version ); ?>

    -
    +
    - - - - | - - + %2$s | ', + esc_url( self_admin_url( 'update-core.php' ) ), + is_multisite() ? __( 'Go to Updates' ) : __( 'Go to Dashboard → Updates' ) + ); + } + + printf( + '%2$s', + esc_url( self_admin_url() ), + is_blog_admin() ? __( 'Go to Dashboard → Home' ) : __( 'Go to Dashboard' ) + ); + ?>
    @@ -297,3 +322,15 @@ /* translators: %s: Documentation URL. */ __( 'For more information, see the release notes.' ); + +/* translators: 1: WordPress version number, 2: Link to update WordPress */ +__( 'Important! Your version of WordPress (%1$s) is no longer supported, you will not receive any security updates for your website. To keep your site secure, please update to the latest version of WordPress.' ); + +/* translators: 1: WordPress version number, 2: Link to update WordPress */ +__( 'Important! Your version of WordPress (%1$s) will stop receiving security updates in the near future. To keep your site secure, please update to the latest version of WordPress.' ); + +/* translators: %s: The major version of WordPress for this branch. */ +__( 'This is the final release of WordPress %s' ); + +/* translators: The localized WordPress download URL. */ +__( 'https://wordpress.org/download/' ); diff --git a/src/wp-admin/admin-ajax.php b/src/wp-admin/admin-ajax.php index 087a11993d4b3..3ad60f95766e3 100644 --- a/src/wp-admin/admin-ajax.php +++ b/src/wp-admin/admin-ajax.php @@ -5,7 +5,7 @@ * @package WordPress * @subpackage Administration * - * @link https://codex.wordpress.org/AJAX_in_Plugins + * @link https://developer.wordpress.org/plugins/javascript/ajax */ /** @@ -27,8 +27,8 @@ header( 'Content-Type: text/html; charset=' . get_option( 'blog_charset' ) ); header( 'X-Robots-Tag: noindex' ); -// Require an action parameter. -if ( empty( $_REQUEST['action'] ) ) { +// Require a valid action parameter. +if ( empty( $_REQUEST['action'] ) || ! is_scalar( $_REQUEST['action'] ) ) { wp_die( '0', 400 ); } @@ -117,6 +117,7 @@ 'parse-media-shortcode', 'destroy-sessions', 'install-plugin', + 'activate-plugin', 'update-plugin', 'crop-image', 'generate-password', @@ -153,7 +154,8 @@ 'health-check-background-updates', 'health-check-loopback-requests', ); -$core_actions_post = array_merge( $core_actions_post, $core_actions_post_deprecated ); + +$core_actions_post = array_merge( $core_actions_post, $core_actions_post_deprecated ); // Register core Ajax calls. if ( ! empty( $_GET['action'] ) && in_array( $_GET['action'], $core_actions_get, true ) ) { @@ -168,7 +170,10 @@ add_action( 'wp_ajax_nopriv_heartbeat', 'wp_ajax_nopriv_heartbeat', 1 ); -$action = ( isset( $_REQUEST['action'] ) ) ? $_REQUEST['action'] : ''; +// Register Plugin Dependencies Ajax calls. +add_action( 'wp_ajax_check_plugin_dependencies', array( 'WP_Plugin_Dependencies', 'check_plugin_dependencies_during_ajax' ) ); + +$action = $_REQUEST['action']; if ( is_user_logged_in() ) { // If no action is registered, return a Bad Request response. @@ -201,5 +206,6 @@ */ do_action( "wp_ajax_nopriv_{$action}" ); } + // Default status. wp_die( '0' ); diff --git a/src/wp-admin/admin-footer.php b/src/wp-admin/admin-footer.php index 9e683d0b117ba..abb020e046459 100644 --- a/src/wp-admin/admin-footer.php +++ b/src/wp-admin/admin-footer.php @@ -35,7 +35,7 @@ $text = sprintf( /* translators: %s: https://wordpress.org/ */ __( 'Thank you for creating with WordPress.' ), - __( 'https://wordpress.org/' ) + esc_url( __( 'https://wordpress.org/' ) ) ); /** @@ -114,6 +114,6 @@ ?>
    - + diff --git a/src/wp-admin/admin-functions.php b/src/wp-admin/admin-functions.php index a9ff3f44b99f7..6ce4e06c47007 100644 --- a/src/wp-admin/admin-functions.php +++ b/src/wp-admin/admin-functions.php @@ -9,6 +9,11 @@ * @subpackage Administration */ +// Don't load directly. +if ( ! defined( 'ABSPATH' ) ) { + die( '-1' ); +} + _deprecated_file( basename( __FILE__ ), '2.5.0', 'wp-admin/includes/admin.php' ); /** WordPress Administration API: Includes all Administration functions. */ diff --git a/src/wp-admin/admin-header.php b/src/wp-admin/admin-header.php index 767276b9742f2..e1e9ba0f6562b 100644 --- a/src/wp-admin/admin-header.php +++ b/src/wp-admin/admin-header.php @@ -6,6 +6,11 @@ * @subpackage Administration */ +// Don't load directly. +if ( ! defined( 'ABSPATH' ) ) { + die( '-1' ); +} + header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); if ( ! defined( 'WP_ADMIN' ) ) { require_once __DIR__ . '/admin.php'; @@ -14,15 +19,15 @@ /** * In case admin-header.php is included in a function. * - * @global string $title + * @global string $title The title of the current screen. * @global string $hook_suffix * @global WP_Screen $current_screen WordPress current screen object. * @global WP_Locale $wp_locale WordPress date and time locale object. - * @global string $pagenow + * @global string $pagenow The filename of the current screen. * @global string $update_title * @global int $total_update_count * @global string $parent_file - * @global string $typenow + * @global string $typenow The post type of the current screen. */ global $title, $hook_suffix, $current_screen, $wp_locale, $pagenow, $update_title, $total_update_count, $parent_file, $typenow; @@ -49,8 +54,23 @@ /* translators: Admin screen title. %s: Admin screen name. */ $admin_title = sprintf( __( '%s — WordPress' ), $title ); } else { + $screen_title = $title; + + if ( 'post' === $current_screen->base && 'add' !== $current_screen->action ) { + $post_title = get_the_title(); + if ( ! empty( $post_title ) ) { + $post_type_obj = get_post_type_object( $typenow ); + $screen_title = sprintf( + /* translators: Editor admin screen title. 1: "Edit item" text for the post type, 2: Post title. */ + __( '%1$s “%2$s”' ), + $post_type_obj->labels->edit_item, + $post_title + ); + } + } + /* translators: Admin screen title. 1: Admin screen name, 2: Network or site name. */ - $admin_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $title, $admin_title ); + $admin_title = sprintf( __( '%1$s ‹ %2$s — WordPress' ), $screen_title, $admin_title ); } if ( wp_is_recovery_mode() ) { @@ -58,15 +78,6 @@ $admin_title = sprintf( __( 'Recovery Mode — %s' ), $admin_title ); } -if ( 'post' === $current_screen->base && 'add' !== $current_screen->action ) { - $post_title = get_the_title(); - if ( ! empty( $post_title ) ) { - $obj = get_post_type_object( $typenow ); - /* translators: Editor admin screen title. 1: "Edit item" text for the post type, 2: Post title. */ - $admin_title = sprintf( __( '%1$s “%2$s”' ), $obj->labels->edit_item, $post_title ); - } -} - /** * Filters the title tag content for an admin page. * @@ -90,8 +101,8 @@ $admin_body_class = preg_replace( '/[^a-z0-9_-]+/i', '-', $hook_suffix ); ?> - diff --git a/src/wp-admin/admin-post.php b/src/wp-admin/admin-post.php index 803a00652c03b..f880c41ac26a0 100644 --- a/src/wp-admin/admin-post.php +++ b/src/wp-admin/admin-post.php @@ -13,11 +13,8 @@ define( 'WP_ADMIN', true ); } -if ( defined( 'ABSPATH' ) ) { - require_once ABSPATH . 'wp-load.php'; -} else { - require_once dirname( __DIR__ ) . '/wp-load.php'; -} +/** Load WordPress Bootstrap */ +require_once dirname( __DIR__ ) . '/wp-load.php'; /** Allow for cross-domain requests (from the front end). */ send_origin_headers(); @@ -29,7 +26,12 @@ /** This action is documented in wp-admin/admin.php */ do_action( 'admin_init' ); -$action = empty( $_REQUEST['action'] ) ? '' : $_REQUEST['action']; +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; + +// Reject invalid parameters. +if ( ! is_scalar( $action ) ) { + wp_die( '', 400 ); +} if ( ! is_user_logged_in() ) { if ( empty( $action ) ) { @@ -40,6 +42,11 @@ */ do_action( 'admin_post_nopriv' ); } else { + // If no action is registered, return a Bad Request response. + if ( ! has_action( "admin_post_nopriv_{$action}" ) ) { + wp_die( '', 400 ); + } + /** * Fires on a non-authenticated admin post request for the given action. * @@ -59,6 +66,11 @@ */ do_action( 'admin_post' ); } else { + // If no action is registered, return a Bad Request response. + if ( ! has_action( "admin_post_{$action}" ) ) { + wp_die( '', 400 ); + } + /** * Fires on an authenticated admin post request for the given action. * diff --git a/src/wp-admin/admin.php b/src/wp-admin/admin.php index d06890259e0ff..82ab6b93ac99e 100644 --- a/src/wp-admin/admin.php +++ b/src/wp-admin/admin.php @@ -31,6 +31,7 @@ define( 'WP_LOAD_IMPORTERS', true ); } +/** Load WordPress Bootstrap */ require_once dirname( __DIR__ ) . '/wp-load.php'; nocache_headers(); @@ -38,7 +39,7 @@ if ( get_option( 'db_upgraded' ) ) { flush_rewrite_rules(); - update_option( 'db_upgraded', false ); + update_option( 'db_upgraded', false, true ); /** * Fires on the next page load after a successful DB upgrade. @@ -71,14 +72,15 @@ * @param bool $do_mu_upgrade Whether to perform the Multisite upgrade routine. Default true. */ if ( apply_filters( 'do_mu_upgrade', true ) ) { - $c = get_blog_count(); + $blog_count = get_blog_count(); /* * If there are 50 or fewer sites, run every time. Otherwise, throttle to reduce load: * attempt to do no more than threshold value, with some +/- allowed. */ - if ( $c <= 50 || ( $c > 50 && mt_rand( 0, (int) ( $c / 50 ) ) === 1 ) ) { + if ( $blog_count <= 50 || ( $blog_count > 50 && mt_rand( 0, (int) ( $blog_count / 50 ) ) === 1 ) ) { require_once ABSPATH . WPINC . '/http.php'; + $response = wp_remote_get( admin_url( 'upgrade.php?step=1' ), array( @@ -86,11 +88,14 @@ 'httpversion' => '1.1', ) ); + /** This action is documented in wp-admin/network/upgrade.php */ do_action( 'after_mu_upgrade', $response ); + unset( $response ); } - unset( $c ); + + unset( $blog_count ); } } @@ -116,16 +121,16 @@ wp_enqueue_script( 'common' ); /** - * $pagenow is set in vars.php - * $wp_importers is sometimes set in wp-admin/includes/import.php - * The remaining variables are imported as globals elsewhere, declared as globals here + * $pagenow is set in vars.php. + * $wp_importers is sometimes set in wp-admin/includes/import.php. + * The remaining variables are imported as globals elsewhere, declared as globals here. * - * @global string $pagenow + * @global string $pagenow The filename of the current screen. * @global array $wp_importers * @global string $hook_suffix * @global string $plugin_page - * @global string $typenow - * @global string $taxnow + * @global string $typenow The post type of the current screen. + * @global string $taxnow The taxonomy of the current screen. */ global $pagenow, $wp_importers, $hook_suffix, $plugin_page, $typenow, $taxnow; @@ -348,7 +353,7 @@ define( 'WP_IMPORTING', true ); /** - * Whether to filter imported data through kses on import. + * Filters whether to filter imported data through kses on import. * * Multisite uses this hook to filter all data through kses by default, * as a super administrator may be assisting an untrusted user. @@ -376,7 +381,7 @@ * The load-* hook fires in a number of contexts. This hook is for core screens. * * The dynamic portion of the hook name, `$pagenow`, is a global variable - * referring to the filename of the current page, such as 'admin.php', + * referring to the filename of the current screen, such as 'admin.php', * 'post-new.php' etc. A complete hook for the latter would be * 'load-post-new.php'. * @@ -390,17 +395,22 @@ */ if ( 'page' === $typenow ) { if ( 'post-new.php' === $pagenow ) { + /** This action is documented in wp-admin/admin.php */ do_action( 'load-page-new.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } elseif ( 'post.php' === $pagenow ) { + /** This action is documented in wp-admin/admin.php */ do_action( 'load-page.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } } elseif ( 'edit-tags.php' === $pagenow ) { if ( 'category' === $taxnow ) { + /** This action is documented in wp-admin/admin.php */ do_action( 'load-categories.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } elseif ( 'link_category' === $taxnow ) { + /** This action is documented in wp-admin/admin.php */ do_action( 'load-edit-link-categories.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } } elseif ( 'term.php' === $pagenow ) { + /** This action is documented in wp-admin/admin.php */ do_action( 'load-edit-tags.php' ); // phpcs:ignore WordPress.NamingConventions.ValidHookName.UseUnderscores } } diff --git a/src/wp-admin/async-upload.php b/src/wp-admin/async-upload.php index d152673f69f52..5fbdd370c5c1a 100644 --- a/src/wp-admin/async-upload.php +++ b/src/wp-admin/async-upload.php @@ -14,11 +14,8 @@ define( 'WP_ADMIN', true ); } -if ( defined( 'ABSPATH' ) ) { - require_once ABSPATH . 'wp-load.php'; -} else { - require_once dirname( __DIR__ ) . '/wp-load.php'; -} +/** Load WordPress Bootstrap */ +require_once dirname( __DIR__ ) . '/wp-load.php'; require_once ABSPATH . 'wp-admin/admin.php'; @@ -63,23 +60,25 @@ $title = $post->post_title ? $post->post_title : wp_basename( $file ); ?>
    - - + + +
    + ' . _x( 'Edit', 'media item' ) . ''; + } else { + echo '' . _x( 'Success', 'media item' ) . ''; + } + ?> + + + + +
    -
    - - - - - ' . _x( 'Edit', 'media item' ) . ''; - } else { - echo '' . _x( 'Success', 'media item' ) . ''; - } - ?> -
    %s %s
    %s
    ', + $button_unique_id = uniqid( 'dismiss-' ); + $error_description_id = uniqid( 'error-description-' ); + $message = sprintf( + '%s %s
    %s', sprintf( - '', + '', + esc_attr( $button_unique_id ), + esc_attr( $error_description_id ), __( 'Dismiss' ) ), sprintf( @@ -126,6 +129,23 @@ ), esc_html( $id->get_error_message() ) ); + + wp_admin_notice( + $message, + array( + 'id' => $error_description_id, + 'additional_classes' => array( 'error-div', 'error' ), + 'paragraph_wrap' => false, + ) + ); + + $speak_message = sprintf( + /* translators: %s: Name of the file that failed to upload. */ + __( '%s has failed to upload.' ), + $_FILES['async-upload']['name'] + ); + + echo '\n"; exit; } diff --git a/src/wp-admin/authorize-application.php b/src/wp-admin/authorize-application.php index d4104297ef120..8d931f46666a2 100644 --- a/src/wp-admin/authorize-application.php +++ b/src/wp-admin/authorize-application.php @@ -137,9 +137,16 @@

    - -

    get_error_message(); ?>

    - + get_error_message(), + array( + 'type' => 'error', + ) + ); + } + ?>

    @@ -161,17 +168,29 @@ if ( is_multisite() ) { $blogs = get_blogs_of_user( $user->ID, true ); $blogs_count = count( $blogs ); + if ( $blogs_count > 1 ) { ?>

    the %2$s site in this installation that you have permissions on.', + 'This will grant access to all %2$s sites in this installation that you have permissions on.', + $blogs_count + ); + + if ( is_super_admin() ) { /* translators: 1: URL to my-sites.php, 2: Number of sites the user has. */ - _n( - 'This will grant access to the %2$s site in this installation that you have permissions on.', - 'This will grant access to all %2$s sites in this installation that you have permissions on.', + $message = _n( + 'This will grant access to the %2$s site on the network as you have Super Admin rights.', + 'This will grant access to all %2$s sites on the network as you have Super Admin rights.', $blogs_count - ), + ); + } + + printf( + $message, admin_url( 'my-sites.php' ), number_format_i18n( $blogs_count ) ); @@ -182,24 +201,25 @@ } ?> - -

    -

    - - -

    -

    -
    + + + +

    +

    ' . __( 'Be sure to save this in a safe location. You will not be able to retrieve it.' ) . '

    '; + $args = array( + 'type' => 'success', + 'additional_classes' => array( 'notice-alt', 'below-h2' ), + 'paragraph_wrap' => false, + ); + wp_admin_notice( $message, $args ); - -
    @@ -238,8 +258,8 @@ * The array of request data. All arguments are optional and may be empty. * * @type string $app_name The suggested name of the application. - * @type string $success_url The url the user will be redirected to after approving the application. - * @type string $reject_url The url the user will be redirected to after rejecting the application. + * @type string $success_url The URL the user will be redirected to after approving the application. + * @type string $reject_url The URL the user will be redirected to after rejecting the application. * } * @param WP_User $user The user authorizing the application. */ diff --git a/src/wp-admin/comment.php b/src/wp-admin/comment.php index 4c09a6085edb6..b834f7b594062 100644 --- a/src/wp-admin/comment.php +++ b/src/wp-admin/comment.php @@ -16,7 +16,8 @@ * @global string $action */ global $action; -wp_reset_vars( array( 'action' ) ); + +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; if ( isset( $_POST['deletecomment'] ) ) { $action = 'deletecomment'; @@ -43,11 +44,12 @@ // Prevent actions on a comment associated with a trashed post. if ( $comment && 'trash' === get_post_status( $comment->comment_post_ID ) ) { wp_die( - __( 'You can’t edit this comment because the associated post is in the Trash. Please restore the post first, then try again.' ) + __( 'You cannot edit this comment because the associated post is in the Trash. Please restore the post first, then try again.' ) ); } } else { - $comment = null; + $comment_id = 0; + $comment = null; } switch ( $action ) { @@ -68,8 +70,8 @@ get_current_screen()->set_help_sidebar( '

    ' . __( 'For more information:' ) . '

    ' . - '

    ' . __( 'Documentation on Comments' ) . '

    ' . - '

    ' . __( 'Support' ) . '

    ' + '

    ' . __( 'Documentation on Comments' ) . '

    ' . + '

    ' . __( 'Support forums' ) . '

    ' ); wp_enqueue_script( 'comment' ); @@ -161,11 +163,23 @@ break; } if ( $message ) { - echo '

    ' . $message . '

    '; + wp_admin_notice( + $message, + array( + 'type' => 'info', + 'id' => 'message', + ) + ); } } + wp_admin_notice( + '' . __( 'Caution:' ) . ' ' . $caution_msg, + array( + 'type' => 'warning', + 'id' => 'message', + ) + ); ?> -

    @@ -282,7 +296,7 @@ comment_footer_die( __( 'Sorry, you are not allowed to edit comments on this post.' ) ); } - if ( wp_get_referer() && ! $noredir && false === strpos( wp_get_referer(), 'comment.php' ) ) { + if ( wp_get_referer() && ! $noredir && ! str_contains( wp_get_referer(), 'comment.php' ) ) { $redir = wp_get_referer(); } elseif ( wp_get_original_referer() && ! $noredir ) { $redir = wp_get_original_referer(); diff --git a/src/wp-admin/contribute.php b/src/wp-admin/contribute.php new file mode 100644 index 0000000000000..e77628d4e8084 --- /dev/null +++ b/src/wp-admin/contribute.php @@ -0,0 +1,118 @@ + +
    + +
    +
    + <?php echo esc_attr( $header_alt_text ); ?> +
    + +
    +

    + +

    +
    + +
    + +
    +
    + + + +
    +
    + +
    +
    +

    +

    + +
      +
    • +
    • +
    • +
    +
    +
    + +
    +
    +

    +

    +
      +
    • Share your knowledge in the WordPress support forums.' ); ?>
    • +
    • Write or improve documentation for WordPress.' ); ?>
    • +
    • Translate WordPress into your local language.' ); ?>
    • +
    • Create and improve WordPress educational materials.' ); ?>
    • +
    • Promote the WordPress project to your community.' ); ?>
    • +
    • Curate submissions or take photos for the Photo Directory.' ); ?>
    • +
    • Organize or participate in local Meetups and WordCamps.' ); ?>
    • +
    • Lend your creative imagination to the WordPress UI design.' ); ?>
    • +
    • Edit videos and add captions to WordPress.tv.' ); ?>
    • +
    • Explore ways to reduce the environmental impact of websites.' ); ?>
    • +
    +
    +
    + +
    +
    +
    +
    + +
    +
    +

    +

    +
      +
    • Find and report bugs in the WordPress core software.' ); ?>
    • +
    • Test new releases and proposed features for the Block Editor.' ); ?>
    • +
    • Write and submit patches to fix bugs or help build new features.' ); ?>
    • +
    • Contribute to the code, improve the UX, and test the WordPress app.' ); ?>
    • +
    +

    +
      +
    • +
    • +
    +
    +
    + +
    +
    +

    +

    +

    +
    +
    + +
    +
    +
    + <?php echo esc_attr( $header_alt_text ); ?> +
    +

    @@ -29,18 +38,19 @@

    - +
    - -
    -
    + + +
    @@ -53,7 +63,7 @@ ); ?>
    - +

    @@ -61,7 +71,7 @@


    - +

    @@ -76,7 +86,7 @@ } ?> -
    +
    @@ -132,5 +142,19 @@ __( 'Release Lead' ); __( 'Release Design Lead' ); __( 'Release Deputy' ); +__( 'Release Coordination' ); +__( 'Minor Release Lead' ); __( 'Core Developer' ); +__( 'Core Tech Lead' ); +__( 'Core Triage Lead' ); +__( 'Editor Tech Lead' ); +__( 'Editor Triage Lead' ); +__( 'Documentation Lead' ); +__( 'Test Lead' ); +__( 'Design Lead' ); +__( 'Performance Lead' ); +__( 'Default Theme Design Lead' ); +__( 'Default Theme Development Lead' ); +__( 'Tech Lead' ); +__( 'Triage Lead' ); __( 'External Libraries' ); diff --git a/src/wp-admin/css/about.css b/src/wp-admin/css/about.css index e57ded61c7ad8..721a98078f0d6 100644 --- a/src/wp-admin/css/about.css +++ b/src/wp-admin/css/about.css @@ -1,12 +1,13 @@ /*------------------------------------------------------------------------------ 22.0 - About Pages - 1.0 Global: About, Credits, Freedoms, Privacy + 1.0 Global: About, Credits, Freedoms, Privacy, Get Involved 1.1 Layout 1.2 Typography & Elements 1.3 Header 2.0 Credits Page 3.0 Freedoms Page + 4.0 Privacy Page x.2.0 Legacy About Styles: Global x.2.1 Typography x.2.2 Structure @@ -20,16 +21,20 @@ .about__container { /* Section backgrounds */ - --background: transparent; - --subtle-background: #def; + --background: #ebe8e5; + --subtle-background: #ebe8e5; /* Main text color */ - --text: #000; + --text: #1e1e1e; --text-light: #fff; /* Accent colors: used in header, on special classes. */ - --accent-1: #3858e9; /* Accent background, link color */ - --accent-2: #2d46ba; /* Header background */ + --accent-1: #3858e9; /* Link color */ + --accent-2: #183ad6; /* Accent background */ + --accent-3: #ececec; /* hr background */ + + /* Header background on small screens */ + --accent-gradient: linear-gradient(90deg, #000000 4.7%, var(--accent-1) 83.84%)/*rtl:linear-gradient(-90deg, #000000 4.7%, var(--accent-1) 83.84%)*/; /* Navigation colors. */ --nav-background: #fff; @@ -37,25 +42,29 @@ --nav-color: var(--text); --nav-current: var(--accent-1); + --border-radius: 0.5rem; + --gap: 2rem; } /*------------------------------------------------------------------------------ - 1.0 - Global: About, Credits, Freedoms, Privacy + 1.0 - Global: About, Credits, Freedoms, Privacy, Get Involved ------------------------------------------------------------------------------*/ .about-php, .credits-php, .freedoms-php, -.privacy-php { - background: #f0f7ff; +.privacy-php, +.contribute-php { + background: #fff; } .about-php #wpcontent, .credits-php #wpcontent, .freedoms-php #wpcontent, -.privacy-php #wpcontent { - background: linear-gradient(180deg, #fff 50%, #f0f7ff 100%); +.privacy-php #wpcontent, +.contribute-php #wpcontent { + background: #fff; padding: 0 24px; } @@ -63,7 +72,8 @@ .about-php.auto-fold #wpcontent, .credits-php.auto-fold #wpcontent, .freedoms-php.auto-fold #wpcontent, - .privacy-php.auto-fold #wpcontent { + .privacy-php.auto-fold #wpcontent, + .contribute-php.auto-fold #wpcontent { padding-left: 24px; } } @@ -99,17 +109,13 @@ } .about__section { - background: var(--background); + background: transparent; clear: both; } .about__container .has-accent-background-color { - background-color: var(--accent-1); - color: var(--text-light); -} - -.about__container .has-accent-background-color a { color: var(--text-light); + background-color: var(--accent-2); } .about__container .has-transparent-background-color { @@ -117,7 +123,7 @@ } .about__container .has-accent-color { - color: var(--accent-1); + color: var(--accent-2); } .about__container .has-border { @@ -126,6 +132,7 @@ .about__container .has-subtle-background-color { background-color: var(--subtle-background); + border-radius: var(--border-radius); } .about__container .has-background-image { @@ -137,28 +144,30 @@ /* 1.1 - Layout */ .about__section { - margin: 0 0 var(--gap); + margin: 0; } -.about__section .column { +.about__section .column:not(.is-edge-to-edge) { padding: var(--gap); } -.about__section + .about__section .column { - padding-top: 0; +.about__section .column.is-left-padding-zero { + padding-left: 0; +} + +.about__section .column.is-right-padding-zero { + padding-right: 0; } .about__section + .about__section .is-section-header { padding-bottom: var(--gap); } -.about__section .column[class*="background-color"], -.about__section .column.has-border { +.about__section .column[class*="background-color"]:not(.is-edge-to-edge), +.about__section:where([class*="background-color"]) .column:not(.is-edge-to-edge), +.about__section .column.has-border:not(.is-edge-to-edge) { padding-top: var(--gap); -} - -.about__section .column.is-edge-to-edge { - padding: 0; + padding-bottom: var(--gap); } .about__section .column p:first-of-type { @@ -214,7 +223,8 @@ } .about__section.has-gutters { - gap: calc(var(--gap) / 2); + gap: var(--gap); + margin-bottom: var(--gap); } .about__section.has-2-columns { @@ -222,83 +232,26 @@ } .about__section.has-2-columns.is-wider-right { - grid-template-columns: 1fr 2fr; + grid-template-columns: 2fr 3fr; } .about__section.has-2-columns.is-wider-left { - grid-template-columns: 2fr 1fr; -} - -.about__section.has-2-columns .is-section-header { - grid-column-start: 1; - -ms-grid-column-span: 2; - grid-column-end: span 2; + grid-template-columns: 3fr 2fr; } -.about__section.has-2-columns .column:nth-of-type(2n+1) { +.about__section .is-section-header { grid-column-start: 1; -} - -.about__section.has-2-columns .column:nth-of-type(2n) { - grid-column-start: 2; + grid-column-end: -1; } .about__section.has-3-columns { grid-template-columns: repeat(3, 1fr); } -.about__section.has-3-columns .is-section-header { - grid-column-start: 1; - -ms-grid-column-span: 3; - grid-column-end: span 3; -} - -.about__section.has-3-columns .column:nth-of-type(3n+1) { - grid-column-start: 1; -} - -.about__section.has-3-columns .column:nth-of-type(3n+2) { - grid-column-start: 2; -} - -.about__section.has-3-columns .column:nth-of-type(3n) { - grid-column-start: 3; -} - .about__section.has-4-columns { grid-template-columns: repeat(4, 1fr); } -.about__section.has-4-columns .is-section-header { - grid-column-start: 1; - -ms-grid-column-span: 4; - grid-column-end: span 4; -} - -.about__section.has-4-columns .column:nth-of-type(4n+1) { - grid-column-start: 1; -} - -.about__section.has-4-columns .column:nth-of-type(4n+2) { - grid-column-start: 2; -} - -.about__section.has-4-columns .column:nth-of-type(4n+3) { - grid-column-start: 3; -} - -.about__section.has-4-columns .column:nth-of-type(4n) { - grid-column-start: 4; -} - -/* Any columns following a section header need to be expicitly put into the second row, for IE support. */ -.about__section.has-2-columns .is-section-header ~ .column, -.about__section.has-3-columns .is-section-header ~ .column, -.about__section.has-4-columns .is-section-header ~ .column, -.about__section.has-overlap-style .is-section-header ~ .column { - grid-row-start: 2; -} - .about__section.has-overlap-style { grid-template-columns: repeat(7, 1fr); } @@ -309,13 +262,11 @@ .about__section.has-overlap-style .column:nth-of-type(2n+1) { grid-column-start: 2; - -ms-grid-column-span: 3; grid-column-end: span 3; } .about__section.has-overlap-style .column:nth-of-type(2n) { grid-column-start: 4; - -ms-grid-column-span: 3; grid-column-end: span 3; } @@ -328,13 +279,24 @@ .about__section.has-2-columns.is-wider-left, .about__section.has-3-columns { display: block; - padding-bottom: calc(var(--gap) / 2); + margin-bottom: calc(var(--gap) / 2); + } + + .about__section .column:not(.is-edge-to-edge) { + padding-top: var(--gap); + padding-bottom: var(--gap); + } + + .about__section.has-2-columns.has-gutters.is-wider-right, + .about__section.has-2-columns.has-gutters.is-wider-left, + .about__section.has-3-columns.has-gutters { + margin-bottom: calc(var(--gap) * 2); } .about__section.has-2-columns.has-gutters .column, .about__section.has-2-columns.has-gutters .column, .about__section.has-3-columns.has-gutters .column { - margin-bottom: calc(var(--gap) / 2); + margin-bottom: var(--gap); } .about__section.has-2-columns.has-gutters .column:last-child, @@ -352,29 +314,6 @@ grid-template-columns: repeat(2, 1fr); } - .about__section.has-4-columns .column:nth-of-type(2n+1) { - grid-column-start: 1; - } - - .about__section.has-4-columns .column:nth-of-type(2n) { - grid-column-start: 2; - } - - .about__section.has-4-columns .column:nth-of-type(4n+3), - .about__section.has-4-columns .column:nth-of-type(4n) { - grid-row-start: 2; - } - - .about__section.has-4-columns .is-section-header { - -ms-grid-column-span: 2; - grid-column-end: span 2; - } - - .about__section.has-4-columns .is-section-header ~ .column:nth-of-type(4n+3), - .about__section.has-4-columns .is-section-header ~ .column:nth-of-type(4n) { - grid-row-start: 3; - } - .about__section.has-overlap-style { grid-template-columns: 1fr; } @@ -382,10 +321,8 @@ /* At this size, the two columns fully overlap */ .about__section.has-overlap-style .column.column { grid-column-start: 1; - -ms-grid-column-span: 1; grid-column-end: 2; grid-row-start: 1; - -ms-grid-row-span: 1; grid-row-end: 2; } } @@ -393,25 +330,38 @@ @media screen and (max-width: 600px) { .about__section.has-2-columns { display: block; + margin-bottom: var(--gap); + } + + .about__section.has-2-columns:not(.has-gutters) .column:nth-of-type(n) { + padding-top: calc(var(--gap) / 2); padding-bottom: calc(var(--gap) / 2); } + .about__section.has-2-columns.has-gutters { + margin-bottom: calc(var(--gap) * 2); + } + .about__section.has-2-columns.has-gutters .column { - margin-bottom: calc(var(--gap) / 2); + margin-bottom: var(--gap); } .about__section.has-2-columns.has-gutters .column:last-child { margin-bottom: 0; } - .about__section.has-2-columns .column:nth-of-type(n) { - padding-top: calc(var(--gap) / 2); - padding-bottom: calc(var(--gap) / 2); - } + .about__section .column.is-left-padding-zero { + padding-right: 0; + } + + .about__section .column.is-right-padding-zero { + padding-left: 0; + } } @media screen and (max-width: 480px) { - .about__section.is-feature .column { + .about__section.is-feature .column, + .about__section .is-section-header { padding: 0; } @@ -443,26 +393,50 @@ .about__container h1 { padding: 0; - color: inherit; } .about__container h1, .about__container h2, .about__container h3.is-larger-heading { margin-top: 0; - margin-bottom: 0.5em; - font-size: 2em; - line-height: 1.2; + margin-bottom: calc(0.5 * var(--gap)); + font-size: 2rem; font-weight: 700; + line-height: 1.16; } .about__container h3, .about__container h1.is-smaller-heading, .about__container h2.is-smaller-heading { margin-top: 0; - font-size: 1.6em; - line-height: 1.3; - font-weight: 400; + margin-bottom: calc(0.5 * var(--gap)); + font-size: 1.625rem; + font-weight: 700; + line-height: 1.4; +} + +.about__container h4, +.about__container h3.is-smaller-heading { + margin-top: 0; + font-size: 1.125rem; + font-weight: 600; + line-height: 1.6; +} + +.about__container h1, +.about__container h2, +.about__container h3, +.about__container h4 { + text-wrap: pretty; + color: inherit; +} + +.about__container :is(h1, h2, h3, h4, .about__header-text):lang(en) { + text-wrap: balance; +} + +.about__container p { + text-wrap: pretty; } .about__container p { @@ -470,6 +444,14 @@ line-height: inherit; } +.about__container p.is-subheading { + margin-top: 0; + margin-bottom: 1rem; + font-size: 1.5rem; + font-weight: 300; + line-height: 160%; +} + .about__section a { color: var(--accent-1); text-decoration: underline; @@ -492,11 +474,24 @@ text-decoration: underline; } +.about__section a.button.button-hero { + padding-top: 1.1875rem; + padding-bottom: 1.1875rem; + font-size: 1.5rem; + line-height: 1.4; + white-space: normal; + text-wrap: pretty; +} + .about__container ul { list-style: disc; margin-left: calc(var(--gap) / 2); } +.about__container li { + margin-bottom: 0.5rem; +} + .about__container img { margin: 0; max-width: 100%; @@ -511,6 +506,7 @@ max-width: 100%; width: 100%; height: auto; + border-radius: var(--border-radius); } .about__container .about__image figcaption { @@ -523,63 +519,32 @@ margin-right: auto; } -.about__container .about__image-comparison { - position: relative; - display: inline-block; - max-width: 100%; -} - -.about__container .about__image-comparison img { - -webkit-user-select: none; - user-select: none; - width: auto; - max-width: none; - max-height: 100%; -} - -.about__container .about__image-comparison > img { - max-width: 100%; -} - -.about__container .about__image-comparison-resize { - position: absolute !important; /* Needed to override inline style on ResizableBox */ - top: 0; - bottom: 0; - left: 0; - width: 50%; - max-width: 100%; -} - -.about__container .about__image-comparison.no-js .about__image-comparison-resize { - overflow: hidden; - border-right: 2px solid var(--wp-admin-theme-color); -} - -.about__container .about__image-comparison-resize .components-resizable-box__side-handle::before { - width: 4px; - right: calc(50% - 2px); - transition: none; - animation: none; - opacity: 1; +.about__container .about__image svg { + vertical-align: middle; } .about__container .about__image + h3 { - margin-top: 1.5em; + margin-top: calc(0.75 * var(--gap)); } .about__container hr { - margin: 0; - height: var(--gap); + margin: calc(var(--gap) / 2) var(--gap); + height: 0; border: none; + border-top: 4px solid var(--accent-3); } .about__container hr.is-small { - height: calc(var(--gap) / 4); + margin-top: 0; + margin-bottom: 0; } .about__container hr.is-large { - height: calc(var(--gap) * 2); - margin: calc(var(--gap) / 2) auto; + margin: var(--gap) auto; +} + +.about__container hr.is-invisible { + border: none; } .about__container div.updated, @@ -588,14 +553,24 @@ display: none !important; } +.about__container code { + font-size: inherit; +} + .about__section { - font-size: 1.2em; + font-size: 1.125rem; + line-height: 1.55; } .about__section.is-feature { font-size: 1.6em; } +.about__section.has-3-columns, +.about__section.has-4-columns { + font-size: 1rem; +} + @media screen and (max-width: 480px) { .about__section.is-feature { font-size: 1.4em; @@ -611,62 +586,116 @@ /* 1.3 - Header */ .about__header { - margin-bottom: var(--gap); - padding-top: 0; - background-position: center; + position: relative; + display: flex; + flex-direction: column; + align-items: flex-start; + justify-content: flex-end; + box-sizing: border-box; + padding: calc(var(--gap) * 1.5); + padding-right: 26rem; /* Space for the background image. */ + min-height: clamp(10rem, 25vw, 18.75rem); + border-radius: var(--border-radius); + background-image: url( "../images/about-header-default.webp?ver=20260514" ); background-repeat: no-repeat; + background-position: right center; background-size: cover; - background-image: url('../images/about-header-about.svg'); - background-color: var(--accent-2); + background-color: var(--background); color: var(--text-light); } -.credits-php .about__header { - background-image: url('../images/about-header-credits.svg'); +.credits-php .about__header { + background-image: url( "../images/about-header-credits.webp?ver=20260514" ); +} + +.freedoms-php .about__header { + background-image: url( "../images/about-header-freedoms.webp?ver=20260514" ); +} + +.privacy-php .about__header { + background-image: url( "../images/about-header-privacy.webp?ver=20260514" ); +} + +.contribute-php .about__header { + background-image: url( "../images/about-header-get-involved.webp?ver=20260514" ); +} + +[dir="rtl"] .about__header { + background-image: url( "../images/about-header-default-rtl.webp?ver=20260514" ); +} + +[dir="rtl"] .credits-php .about__header { + background-image: url( "../images/about-header-credits-rtl.webp?ver=20260514" ); } -.freedoms-php .about__header { - background-image: url('../images/about-header-freedoms.svg'); +[dir="rtl"] .freedoms-php .about__header { + background-image: url( "../images/about-header-freedoms-rtl.webp?ver=20260514" ); } -.privacy-php .about__header { - background-image: url('../images/about-header-privacy.svg'); +[dir="rtl"] .privacy-php .about__header { + background-image: url( "../images/about-header-privacy-rtl.webp?ver=20260514" ); +} + +[dir="rtl"] .contribute-php .about__header { + background-image: url( "../images/about-header-get-involved-rtl.webp?ver=20260514" ); } .about__header-image { - margin: 0 var(--gap) 3em; + margin: 0 0 calc(var(--gap) * 1.5); } .about__header-title { - padding: 2rem 0 0; - margin: 0 2rem; + box-sizing: border-box; + margin: 0; + padding: 0; } .about__header-title h1 { - margin: 0 0 0.5rem; + margin: 0; padding: 0; - font-size: 4.5rem; + /* Fluid font size scales on browser size 960px - 1200px. */ + font-size: clamp(2rem, 20vw - 9rem, 4rem); line-height: 1; - font-weight: 400; + font-weight: 600; + color: var(--text); +} + +.about-php .about__header-title h1, +.credits-php .about__header-title h1, +.freedoms-php .about__header-title h1, +.privacy-php .about__header-title h1, +.contribute-php .about__header-title h1 { + /* Fluid font size scales on browser size 960px - 1200px. */ + font-size: clamp(2rem, 20vw - 9rem, 4rem); } .about__header-text { - max-width: 42rem; - margin: 0 0 5em; - padding: 0 2rem; - font-size: 2rem; + box-sizing: border-box; + max-width: 26em; + margin: 1rem 0 0; + padding: 0; + font-size: 1.6rem; line-height: 1.15; + color: var(--text); } .about__header-navigation { + position: relative; + z-index: 1; display: flex; - justify-content: center; + flex-wrap: wrap; + justify-content: space-between; padding-top: 0; + margin-bottom: var(--gap); background: var(--nav-background); color: var(--nav-color); border-bottom: 3px solid var(--nav-border); } +.about__header-navigation::after { + display: none; +} + .about__header-navigation .nav-tab { margin-left: 0; padding: calc(var(--gap) * 0.75) var(--gap); @@ -684,6 +713,7 @@ .about__header-navigation .nav-tab:active { background-color: var(--nav-current); color: var(--text-light); + border-radius: var(--border-radius); } .about__header-navigation .nav-tab-active { @@ -698,11 +728,25 @@ background-color: var(--nav-current); color: var(--text-light); border-color: var(--nav-current); + border-radius: var(--border-radius); } -@media screen and (max-width: 960px){ - .about__header-title h1 { - font-size: 4.8em; +@media screen and (max-width: 960px) { + .about__header { + padding-right: 21rem; + } + + .about-php .about__header-title h1, + .credits-php .about__header-title h1, + .freedoms-php .about__header-title h1, + .privacy-php .about__header-title h1, + .contribute-php .about__header-title h1 { + /* Fluid font size scales on browser size 600px - 960px. */ + font-size: clamp(2rem, 20vw - 9rem, 4rem); + } + + .about__header-navigation .nav-tab { + padding: calc(var(--gap) * 0.75) calc(var(--gap) * 0.5); } } @@ -715,28 +759,26 @@ display: block; } - .about__header-title, - .about__header-image { - margin-left: calc(var(--gap) / 2); - margin-right: calc(var(--gap) / 2); + .about__header { + padding: var(--gap); + padding-right: 17rem; + } + + .about__header-text { + margin-top: 0.5rem; } - .about__header-text, .about__header-navigation .nav-tab { margin-top: 0; margin-right: 0; - padding-left: calc(var(--gap) / 2); - padding-right: calc(var(--gap) / 2); + font-size: 1.2em; } } -@media screen and (max-width: 480px) { - .about__header-title p { - font-size: 2.4em; - } - - .about__header-text { - margin-bottom: 1em; +@media screen and (max-width: 600px) { + .about__header { + min-height: auto; + padding-right: var(--gap); } .about__header-navigation { @@ -763,9 +805,8 @@ ------------------------------------------------------------------------------*/ .about__section .wp-people-group-title { - margin-bottom: calc(var(--gap) * 2); + margin-bottom: calc(var(--gap) * 2 - 10px); text-align: center; - } .about__section .wp-people-group { @@ -778,7 +819,7 @@ display: inline-block; vertical-align: top; box-sizing: border-box; - margin-bottom: var(--gap); + margin-bottom: calc(var(--gap) - 10px); width: 25%; text-align: center; } @@ -795,14 +836,12 @@ height: 140px; border-radius: 100%; overflow: hidden; - background: var(--accent-1); } .about__section .wp-person .gravatar { width: 140px; height: 140px; filter: grayscale(100%); - mix-blend-mode: screen; } .about__section .compact .wp-person-avatar, @@ -812,8 +851,10 @@ } .about__section .wp-person .web { + display: block; font-size: 1.4em; font-weight: 600; + padding: 10px 10px 0; text-decoration: none; } @@ -881,7 +922,19 @@ .about__section .column .freedom-image { margin-bottom: var(--gap); - max-height: 140px; + max-height: 180px; +} + + +/*------------------------------------------------------------------------------ + 4.0 - Privacy Page +------------------------------------------------------------------------------*/ + +.about__section .column .privacy-image { + display: block; + margin-left: auto; + margin-right: auto; + max-width: 25rem; } @@ -1140,7 +1193,7 @@ } .about-wrap .is-vertically-aligned-top { - align-self: start; + align-self: flex-start; } .about-wrap .is-vertically-aligned-center { @@ -1307,24 +1360,6 @@ margin: 0.6em 0; } -.freedoms-php .column .freedoms-image { - background-image: url('../images/freedoms.png'); - background-size: 100%; - padding-top: 100%; -} - -.freedoms-php .column:nth-of-type(2) .freedoms-image { - background-position: 0 34%; -} - -.freedoms-php .column:nth-of-type(3) .freedoms-image { - background-position: 0 66%; -} - -.freedoms-php .column:nth-of-type(4) .freedoms-image { - background-position: 0 100%; -} - /*------------------------------------------------------------------------------ x.5.0 - Legacy About Styles: Media Queries ------------------------------------------------------------------------------*/ diff --git a/src/wp-admin/css/admin-menu.css b/src/wp-admin/css/admin-menu.css index 243dde7daa3b0..c4b32ac4b9e87 100644 --- a/src/wp-admin/css/admin-menu.css +++ b/src/wp-admin/css/admin-menu.css @@ -11,6 +11,9 @@ top: 0; bottom: -120px; z-index: 1; /* positive z-index to avoid elastic scrolling woes in Safari */ + + /* Only visible in Windows High Contrast mode */ + outline: 1px solid transparent; } .php-error #adminmenuback { @@ -36,108 +39,21 @@ width: 36px; } -.icon16 { - height: 18px; - width: 18px; - padding: 6px; - margin: -6px 0 0 -8px; - float: left; -} - /* New Menu icons */ -.icon16:before { - color: #8c8f94; /* same as new icons */ - font: normal 20px/1 dashicons; - speak: never; - padding: 6px 0; - height: 34px; - width: 20px; - display: inline-block; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - transition: all .1s ease-in-out; -} - -.icon16.icon-dashboard:before { - content: "\f226"; -} - -.icon16.icon-post:before { - content: "\f109"; -} - -.icon16.icon-media:before { - content: "\f104"; -} - -.icon16.icon-links:before { - content: "\f103"; -} - -.icon16.icon-page:before { - content: "\f105"; -} - -.icon16.icon-comments:before { - content: "\f101"; - margin-top: 1px; -} - -.icon16.icon-appearance:before { - content: "\f100"; -} - -.icon16.icon-plugins:before { - content: "\f106"; -} - -.icon16.icon-users:before { - content: "\f110"; -} - -.icon16.icon-tools:before { - content: "\f107"; -} - -.icon16.icon-settings:before { - content: "\f108"; -} - -.icon16.icon-site:before { - content: "\f541"; -} - -.icon16.icon-generic:before { - content: "\f111"; -} - /* hide background-image for icons above */ -.icon16.icon-dashboard, .menu-icon-dashboard div.wp-menu-image, -.icon16.icon-post, .menu-icon-post div.wp-menu-image, -.icon16.icon-media, .menu-icon-media div.wp-menu-image, -.icon16.icon-links, .menu-icon-links div.wp-menu-image, -.icon16.icon-page, .menu-icon-page div.wp-menu-image, -.icon16.icon-comments, .menu-icon-comments div.wp-menu-image, -.icon16.icon-appearance, .menu-icon-appearance div.wp-menu-image, -.icon16.icon-plugins, .menu-icon-plugins div.wp-menu-image, -.icon16.icon-users, .menu-icon-users div.wp-menu-image, -.icon16.icon-tools, .menu-icon-tools div.wp-menu-image, -.icon16.icon-settings, .menu-icon-settings div.wp-menu-image, -.icon16.icon-site, .menu-icon-site div.wp-menu-image, -.icon16.icon-generic, .menu-icon-generic div.wp-menu-image { background-image: none !important; } @@ -196,6 +112,7 @@ .folded #adminmenu .wp-submenu-head:hover { box-shadow: inset 4px 0 0 0 currentColor; transition: box-shadow .1s linear; + border-radius: 0; } #adminmenu li.menu-top { @@ -211,7 +128,7 @@ left: 160px; overflow: visible; word-wrap: break-word; - padding: 7px 0 8px; + padding: 6px 0; z-index: 9999; background-color: #2c3338; box-shadow: 0 3px 5px rgba(0, 0, 0, 0.2); @@ -264,9 +181,7 @@ #adminmenu li.wp-has-current-submenu a.wp-has-current-submenu, #adminmenu li.current a.menu-top, -#adminmenu .wp-menu-arrow, -#adminmenu .wp-has-current-submenu .wp-submenu .wp-submenu-head, -#adminmenu .wp-menu-arrow div { +#adminmenu .wp-has-current-submenu .wp-submenu .wp-submenu-head { background: #2271b1; color: #fff; } @@ -292,7 +207,8 @@ .folded #adminmenu .wp-has-current-submenu .wp-submenu { min-width: 160px; width: auto; - border-left: 5px solid transparent; + border: 1px solid transparent; + border-left-width: 5px; } #adminmenu .wp-submenu li.current, @@ -355,7 +271,6 @@ word-wrap: break-word; -ms-word-break: break-all; word-break: break-word; - -webkit-hyphens: auto; hyphens: auto; } @@ -417,12 +332,6 @@ div.wp-menu-image:before { position: fixed; } -/* A new arrow */ - -.wp-menu-arrow { - display: none !important; -} - ul#adminmenu a.wp-has-current-submenu { position: relative; } @@ -502,8 +411,8 @@ ul#adminmenu > li.current > a.current:after { font-weight: 400; font-size: 14px; padding: 5px 4px 5px 11px; - margin: -7px 0 4px -5px; - border-width: 3px 0 3px 5px; + margin: -8px -1px 4px -5px; + border-width: 3px 1px 3px 5px; border-style: solid; border-color: transparent; } @@ -514,6 +423,7 @@ ul#adminmenu > li.current > a.current:after { } /* @todo: consider to use a single rule for these counters and the list table comments counters. */ +#adminmenu .menu-counter, #adminmenu .awaiting-mod, #adminmenu .update-plugins { display: inline-block; @@ -595,12 +505,12 @@ ul#adminmenu > li.current > a.current:after { #collapse-button .collapse-button-icon:after { content: "\f148"; + content: "\f148" / ''; display: block; position: relative; top: 7px; text-align: center; font: normal 20px/1 dashicons !important; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -662,13 +572,15 @@ li#wp-admin-bar-menu-toggle { position: absolute; top: -1000em; margin-right: -1px; - padding: 7px 0 8px; + padding: 6px 0; z-index: 9999; } .auto-fold #adminmenu .wp-has-current-submenu .wp-submenu { - min-width: 150px; + min-width: 160px; width: auto; + border: 1px solid transparent; + border-left-width: 5px; } .auto-fold #adminmenu .wp-has-current-submenu li > a { @@ -857,8 +769,9 @@ li#wp-admin-bar-menu-toggle { } #adminmenu .wp-not-current-submenu .wp-submenu, - .folded #adminmenu .wp-has-current-submenu .wp-submenu { - border-left: none; + .folded #adminmenu .wp-has-current-submenu .wp-submenu, + .auto-fold #adminmenu .wp-has-current-submenu .wp-submenu { + border: none; } /* Remove submenu headers and adjust sub meu*/ diff --git a/src/wp-admin/css/color-picker.css b/src/wp-admin/css/color-picker.css index 89513949579b6..8264432dd39cc 100644 --- a/src/wp-admin/css/color-picker.css +++ b/src/wp-admin/css/color-picker.css @@ -8,9 +8,9 @@ display: none; } -/* Needs higher specificiity. */ +/* Needs higher specificity to override `.wp-core-ui .button`. */ .wp-picker-container .wp-color-result.button { - min-height: 30px; + min-height: 32px; margin: 0 6px 6px 0; padding: 0 0 0 30px; font-size: 11px; @@ -22,7 +22,7 @@ border-left: 1px solid #c3c4c7; color: #50575e; display: block; - line-height: 2.54545455; /* 28px */ + line-height: 2.72727273; /* 30px */ padding: 0 6px; text-align: center; } @@ -76,8 +76,8 @@ .wp-customizer .wp-picker-input-wrap .button.wp-picker-clear { margin-left: 6px; padding: 0 8px; - line-height: 2.54545455; /* 28px */ - min-height: 30px; + line-height: 2.72727273; /* 30px */ + min-height: 32px; } .wp-picker-container .iris-square-slider .ui-slider-handle:focus { @@ -94,11 +94,10 @@ width: 4rem; font-size: 12px; font-family: monospace; - line-height: 2.33333333; /* 28px */ margin: 0; padding: 0 5px; vertical-align: top; - min-height: 30px; + min-height: 32px; } .wp-color-picker::-webkit-input-placeholder { @@ -107,11 +106,6 @@ .wp-color-picker::-moz-placeholder { color: #646970; - opacity: 1; -} - -.wp-color-picker:-ms-input-placeholder { - color: #646970; } .wp-picker-container input[type="text"].iris-error { @@ -122,14 +116,14 @@ .iris-picker .ui-square-handle:focus, .iris-picker .iris-strip .ui-slider-handle:focus { - border-color: #3582c4; + border-color: var(--wp-admin-theme-color, #3858e9); border-style: solid; - box-shadow: 0 0 0 1px #3582c4; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); outline: 2px solid transparent; } .iris-picker .iris-palette:focus { - box-shadow: 0 0 0 2px #3582c4; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); } @media screen and (max-width: 782px) { diff --git a/src/wp-admin/css/colors/_admin.scss b/src/wp-admin/css/colors/_admin.scss index 449b0a8e279f8..313666e3ded73 100644 --- a/src/wp-admin/css/colors/_admin.scss +++ b/src/wp-admin/css/colors/_admin.scss @@ -1,25 +1,32 @@ - -@import 'variables'; -@import 'mixins'; - +@use 'sass:color'; +@use 'sass:string'; +@forward 'variables' show $scheme-name, $base-color, $body-background, $button-color, $custom-welcome-panel, $dashboard-accent-1, $dashboard-accent-2, $dashboard-icon-background, $form-checked, $highlight-color, $icon-color, $link, $link-focus, $low-contrast-theme, $menu-bubble-text, $menu-collapse-focus-icon, $menu-collapse-text, $menu-highlight-background, $menu-highlight-icon, $menu-highlight-text, $menu-submenu-text, $menu-submenu-focus-text, $menu-submenu-background, $notification-color, $text-color; +@use 'variables'; +@use 'mixins'; +@use 'tokens'; + +/** + * This function name uses British English to maintain backward compatibility, as developers + * may use the function in their own admin CSS files. See #56811. + */ @function url-friendly-colour( $color ) { - @return '%23' + str-slice( '#{ $color }', 2, -1 ); + @return '%23' + string.slice( '#{ $color }', 2, -1 ); } body { - background: $body-background; + background: variables.$body-background; } /* Links */ a { - color: $link; + color: variables.$link; &:hover, &:active, &:focus { - color: $link-focus; + color: variables.$link-focus; } } @@ -31,13 +38,27 @@ span.wp-media-buttons-icon:before { color: currentColor; } -.wp-core-ui .button-link { - color: $link; +/* Link button - appears as text link, no border or background */ +/* Matches Gutenberg's .is-link button variant */ +.wp-core-ui .button-link, +.wp-core-ui .button.button-link { + color: var(--wp-admin-theme-color); &:hover, - &:active, + &:active { + color: var(--wp-admin-theme-color-darker-20); + } + &:focus { - color: $link-focus; + color: var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:disabled, + &[aria-disabled="true"] { + color: tokens.$gray-600; } } @@ -45,7 +66,7 @@ span.wp-media-buttons-icon:before { .media-modal .trash-attachment, .media-modal .untrash-attachment, .wp-core-ui .button-link-delete { - color: #a00; + color: tokens.$alert-red; } .media-modal .delete-attachment:hover, @@ -56,24 +77,29 @@ span.wp-media-buttons-icon:before { .media-modal .untrash-attachment:focus, .wp-core-ui .button-link-delete:hover, .wp-core-ui .button-link-delete:focus { - color: #dc3232; + color: color.adjust(tokens.$alert-red, $lightness: 10%); } /* Forms */ -input[type=checkbox]:checked::before { - content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27#{url-friendly-colour($form-checked)}%27%2F%3E%3C%2Fsvg%3E"); +// Checkbox checked state - uses theme color +input[type="checkbox"]:checked { + background: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); } -input[type=radio]:checked::before { - background: $form-checked; +// Radio checked state - uses theme color +input[type="radio"]:checked { + background: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); } .wp-core-ui input[type="reset"]:hover, .wp-core-ui input[type="reset"]:active { - color: $link-focus; + color: variables.$link-focus; } +// Text input focus - outset focus ring matching button focus style input[type="text"]:focus, input[type="password"]:focus, input[type="color"]:focus, @@ -85,159 +111,139 @@ input[type="month"]:focus, input[type="number"]:focus, input[type="search"]:focus, input[type="tel"]:focus, -input[type="text"]:focus, input[type="time"]:focus, input[type="url"]:focus, input[type="week"]:focus, -input[type="checkbox"]:focus, -input[type="radio"]:focus, select:focus, textarea:focus { - border-color: $highlight-color; - box-shadow: 0 0 0 1px $highlight-color; + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); } +// Checkbox/Radio focus - Gutenberg-style outset focus ring +input[type="checkbox"]:focus, +input[type="radio"]:focus { + border-color: tokens.$gray-900; + box-shadow: 0 0 0 2px tokens.$white, 0 0 0 4px var(--wp-admin-theme-color); + outline: 2px solid transparent; +} -/* Core UI */ +// Select focus (wp-core-ui styled selects) +.wp-core-ui select:focus { + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); +} -.wp-core-ui { +// Autocomplete focus state +.wp-tags-autocomplete .ui-state-focus, +.wp-tags-autocomplete [aria-selected="true"] { + background-color: var(--wp-admin-theme-color); +} - .button { - border-color: #7e8993; - color: #32373c; - } +// Password field focus +#pass1:focus, +#pass1-text:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); +} - .button.hover, - .button:hover, - .button.focus, - .button:focus { - border-color: darken( #7e8993, 5% ); - color: darken( #32373c, 5% ); - } +// Password toggle button focus +.mailserver-pass-wrap .button.wp-hide-pw:focus { + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); +} - .button.focus, - .button:focus { - border-color: #7e8993; - color: darken( #32373c, 5% ); - box-shadow: 0 0 0 1px #32373c; - } - .button:active { - border-color: #7e8993; - color: darken( #32373c, 5% ); - box-shadow: none; - } +/* Core UI */ - .button.active, - .button.active:focus, - .button.active:hover { - border-color: $button-color; - color: darken( #32373c, 5% ); - box-shadow: inset 0 2px 5px -3px $button-color; - } +.wp-core-ui { - .button.active:focus { - box-shadow: 0 0 0 1px #32373c; + /* Default button - theme color border and text (matches secondary) */ + .button { + @include mixins.button-secondary(); } - @if ( $low-contrast-theme != "true" ) { - .button, - .button-secondary { - color: $highlight-color; - border-color: $highlight-color; - } - - .button.hover, - .button:hover, - .button-secondary:hover{ - border-color: darken($highlight-color, 10); - color: darken($highlight-color, 10); - } - - .button.focus, - .button:focus, - .button-secondary:focus { - border-color: lighten($highlight-color, 10); - color: darken($highlight-color, 20);; - box-shadow: 0 0 0 1px lighten($highlight-color, 10); - } - - .button-primary { - &:hover { - color: #fff; - } - } + /* Secondary button - same as default */ + .button-secondary { + @include mixins.button-secondary(); } + /* Primary button - theme color background */ .button-primary { - @include button( $button-color ); + @include mixins.button(); } .button-group > .button.active { - border-color: $button-color; + border-color: var(--wp-admin-theme-color); + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); } .wp-ui-primary { - color: $text-color; - background-color: $base-color; + color: variables.$text-color; + background-color: variables.$base-color; } .wp-ui-text-primary { - color: $base-color; + color: variables.$base-color; } .wp-ui-highlight { - color: $menu-highlight-text; - background-color: $menu-highlight-background; + color: variables.$menu-highlight-text; + background-color: variables.$menu-highlight-background; } .wp-ui-text-highlight { - color: $menu-highlight-background; + color: variables.$menu-highlight-background; } .wp-ui-notification { - color: $menu-bubble-text; - background-color: $menu-bubble-background; + color: variables.$menu-bubble-text; + background-color: variables.$menu-bubble-background; } .wp-ui-text-notification { - color: $menu-bubble-background; + color: variables.$menu-bubble-background; } .wp-ui-text-icon { - color: $menu-icon; + color: variables.$menu-icon; } } /* List tables */ -@if $low-contrast-theme == "true" { - .wrap .page-title-action:hover { - color: $menu-text; - background-color: $menu-background; - } -} @else { - .wrap .page-title-action, - .wrap .page-title-action:active { - border: 1px solid $highlight-color; - color: $highlight-color; - } - .wrap .page-title-action:hover { - color: darken($highlight-color, 10); - border-color: darken($highlight-color, 10); - } +// .page-title-action uses secondary button styling +.wrap .page-title-action { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); +} - .wrap .page-title-action:focus { - border-color: lighten($highlight-color, 10); - color: darken($highlight-color, 20);; - box-shadow: 0 0 0 1px lighten($highlight-color, 10); - } +.wrap .page-title-action:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); +} + +.wrap .page-title-action:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + outline: 1px solid transparent; +} + +.wrap .page-title-action:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; } .view-switch a.current:before { - color: $menu-background; + color: variables.$menu-background; } .view-switch a:hover:before { - color: $menu-bubble-background; + color: variables.$menu-bubble-background; } @@ -246,28 +252,28 @@ textarea:focus { #adminmenuback, #adminmenuwrap, #adminmenu { - background: $menu-background; + background: variables.$menu-background; } #adminmenu a { - color: $menu-text; + color: variables.$menu-text; } #adminmenu div.wp-menu-image:before { - color: $menu-icon; + color: variables.$menu-icon; } #adminmenu a:hover, #adminmenu li.menu-top:hover, #adminmenu li.opensub > a.menu-top, #adminmenu li > a.menu-top:focus { - color: $menu-highlight-text; - background-color: $menu-highlight-background; + color: variables.$menu-highlight-text; + background-color: variables.$menu-highlight-background; } #adminmenu li.menu-top:hover div.wp-menu-image:before, #adminmenu li.opensub > a.menu-top div.wp-menu-image:before { - color: $menu-highlight-icon; + color: variables.$menu-highlight-icon; } @@ -276,8 +282,8 @@ textarea:focus { .about-wrap .nav-tab-active, .nav-tab-active, .nav-tab-active:hover { - background-color: $body-background; - border-bottom-color: $body-background; + background-color: variables.$body-background; + border-bottom-color: variables.$body-background; } @@ -287,26 +293,26 @@ textarea:focus { #adminmenu .wp-has-current-submenu .wp-submenu, #adminmenu .wp-has-current-submenu.opensub .wp-submenu, #adminmenu a.wp-has-current-submenu:focus + .wp-submenu { - background: $menu-submenu-background; + background: variables.$menu-submenu-background; } #adminmenu li.wp-has-submenu.wp-not-current-submenu.opensub:hover:after, #adminmenu li.wp-has-submenu.wp-not-current-submenu:focus-within:after { - border-right-color: $menu-submenu-background; + border-right-color: variables.$menu-submenu-background; } #adminmenu .wp-submenu .wp-submenu-head { - color: $menu-submenu-text; + color: variables.$menu-submenu-text; } #adminmenu .wp-submenu a, #adminmenu .wp-has-current-submenu .wp-submenu a, #adminmenu a.wp-has-current-submenu:focus + .wp-submenu a, #adminmenu .wp-has-current-submenu.opensub .wp-submenu a { - color: $menu-submenu-text; + color: variables.$menu-submenu-text; &:focus, &:hover { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } } @@ -316,24 +322,24 @@ textarea:focus { #adminmenu .wp-submenu li.current a, #adminmenu a.wp-has-current-submenu:focus + .wp-submenu li.current a, #adminmenu .wp-has-current-submenu.opensub .wp-submenu li.current a { - color: $menu-submenu-current-text; + color: variables.$menu-submenu-current-text; &:hover, &:focus { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } } ul#adminmenu a.wp-has-current-submenu:after, ul#adminmenu > li.current > a.current:after { - border-right-color: $body-background; + border-right-color: variables.$body-background; } #adminmenu li.current a.menu-top, #adminmenu li.wp-has-current-submenu a.wp-has-current-submenu, #adminmenu li.wp-has-current-submenu .wp-submenu .wp-submenu-head, .folded #adminmenu li.current.menu-top { - color: $menu-current-text; - background: $menu-current-background; + color: variables.$menu-current-text; + background: variables.$menu-current-background; } #adminmenu li.wp-has-current-submenu div.wp-menu-image:before, @@ -344,57 +350,58 @@ ul#adminmenu > li.current > a.current:after { #adminmenu li:hover div.wp-menu-image:before, #adminmenu li a:focus div.wp-menu-image:before, #adminmenu li.opensub div.wp-menu-image:before { - color: $menu-current-icon; + color: variables.$menu-current-icon; } /* Admin Menu: bubble */ +#adminmenu .menu-counter, #adminmenu .awaiting-mod, #adminmenu .update-plugins { - color: $menu-bubble-text; - background: $menu-bubble-background; + color: variables.$menu-bubble-text; + background: variables.$menu-bubble-background; } #adminmenu li.current a .awaiting-mod, #adminmenu li a.wp-has-current-submenu .update-plugins, #adminmenu li:hover a .awaiting-mod, #adminmenu li.menu-top:hover > a .update-plugins { - color: $menu-bubble-current-text; - background: $menu-bubble-current-background; + color: variables.$menu-bubble-current-text; + background: variables.$menu-bubble-current-background; } /* Admin Menu: collapse button */ #collapse-button { - color: $menu-collapse-text; + color: variables.$menu-collapse-text; } #collapse-button:hover, #collapse-button:focus { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } /* Admin Bar */ #wpadminbar { - color: $menu-text; - background: $menu-background; + color: variables.$menu-text; + background: variables.$menu-background; } #wpadminbar .ab-item, #wpadminbar a.ab-item, #wpadminbar > #wp-toolbar span.ab-label, #wpadminbar > #wp-toolbar span.noticon { - color: $menu-text; + color: variables.$menu-text; } #wpadminbar .ab-icon, #wpadminbar .ab-icon:before, #wpadminbar .ab-item:before, #wpadminbar .ab-item:after { - color: $menu-icon; + color: variables.$menu-icon; } #wpadminbar:not(.mobile) .ab-top-menu > li:hover > .ab-item, @@ -402,45 +409,45 @@ ul#adminmenu > li.current > a.current:after { #wpadminbar.nojq .quicklinks .ab-top-menu > li > .ab-item:focus, #wpadminbar.nojs .ab-top-menu > li.menupop:hover > .ab-item, #wpadminbar .ab-top-menu > li.menupop.hover > .ab-item { - color: $menu-submenu-focus-text; - background: $menu-submenu-background; + color: variables.$menu-submenu-focus-text; + background: variables.$menu-submenu-background; } #wpadminbar:not(.mobile) > #wp-toolbar li:hover span.ab-label, #wpadminbar:not(.mobile) > #wp-toolbar li.hover span.ab-label, #wpadminbar:not(.mobile) > #wp-toolbar a:focus span.ab-label { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } #wpadminbar:not(.mobile) li:hover .ab-icon:before, #wpadminbar:not(.mobile) li:hover .ab-item:before, #wpadminbar:not(.mobile) li:hover .ab-item:after, #wpadminbar:not(.mobile) li:hover #adminbarsearch:before { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } /* Admin Bar: submenu */ #wpadminbar .menupop .ab-sub-wrapper { - background: $menu-submenu-background; + background: variables.$menu-submenu-background; } #wpadminbar .quicklinks .menupop ul.ab-sub-secondary, #wpadminbar .quicklinks .menupop ul.ab-sub-secondary .ab-submenu { - background: $menu-submenu-background-alt; + background: variables.$menu-submenu-background-alt; } #wpadminbar .ab-submenu .ab-item, #wpadminbar .quicklinks .menupop ul li a, #wpadminbar .quicklinks .menupop.hover ul li a, #wpadminbar.nojs .quicklinks .menupop:hover ul li a { - color: $menu-submenu-text; + color: variables.$menu-submenu-text; } #wpadminbar .quicklinks li .blavatar, #wpadminbar .menupop .menupop > .ab-item:before { - color: $menu-icon; + color: variables.$menu-icon; } #wpadminbar .quicklinks .menupop ul li a:hover, @@ -461,91 +468,91 @@ ul#adminmenu > li.current > a.current:after { #wpadminbar li.hover .ab-item:before, #wpadminbar li:hover #adminbarsearch:before, #wpadminbar li #adminbarsearch.adminbar-focused:before { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } #wpadminbar .quicklinks li a:hover .blavatar, #wpadminbar .quicklinks li a:focus .blavatar, #wpadminbar .quicklinks .ab-sub-wrapper .menupop.hover > a .blavatar, #wpadminbar .menupop .menupop > .ab-item:hover:before, -#wpadminbar.mobile .quicklinks .ab-icon:before, -#wpadminbar.mobile .quicklinks .ab-item:before { - color: $menu-submenu-focus-text; -} - #wpadminbar.mobile .quicklinks .hover .ab-icon:before, #wpadminbar.mobile .quicklinks .hover .ab-item:before { - color: $menu-icon; + color: variables.$menu-submenu-focus-text; +} + +#wpadminbar.mobile .quicklinks .ab-icon:before, +#wpadminbar.mobile .quicklinks .ab-item:before { + color: variables.$menu-icon; } /* Admin Bar: search */ #wpadminbar #adminbarsearch:before { - color: $menu-icon; + color: variables.$menu-icon; } #wpadminbar > #wp-toolbar > #wp-admin-bar-top-secondary > #wp-admin-bar-search #adminbarsearch input.adminbar-input:focus { - color: $menu-text; - background: $adminbar-input-background; + color: variables.$menu-text; + background: variables.$adminbar-input-background; } /* Admin Bar: recovery mode */ #wpadminbar #wp-admin-bar-recovery-mode { - color: $adminbar-recovery-exit-text; - background-color: $adminbar-recovery-exit-background; + color: variables.$adminbar-recovery-exit-text; + background-color: variables.$adminbar-recovery-exit-background; } #wpadminbar #wp-admin-bar-recovery-mode .ab-item, #wpadminbar #wp-admin-bar-recovery-mode a.ab-item { - color: $adminbar-recovery-exit-text; + color: variables.$adminbar-recovery-exit-text; } #wpadminbar .ab-top-menu > #wp-admin-bar-recovery-mode.hover >.ab-item, #wpadminbar.nojq .quicklinks .ab-top-menu > #wp-admin-bar-recovery-mode > .ab-item:focus, #wpadminbar:not(.mobile) .ab-top-menu > #wp-admin-bar-recovery-mode:hover > .ab-item, #wpadminbar:not(.mobile) .ab-top-menu > #wp-admin-bar-recovery-mode > .ab-item:focus { - color: $adminbar-recovery-exit-text; - background-color: $adminbar-recovery-exit-background-alt; + color: variables.$adminbar-recovery-exit-text; + background-color: variables.$adminbar-recovery-exit-background-alt; } /* Admin Bar: my account */ #wpadminbar .quicklinks li#wp-admin-bar-my-account.with-avatar > a img { - border-color: $adminbar-avatar-frame; - background-color: $adminbar-avatar-frame; + border-color: variables.$adminbar-avatar-frame; + background-color: variables.$adminbar-avatar-frame; } #wpadminbar #wp-admin-bar-user-info .display-name { - color: $menu-text; + color: variables.$menu-text; } #wpadminbar #wp-admin-bar-user-info a:hover .display-name { - color: $menu-submenu-focus-text; + color: variables.$menu-submenu-focus-text; } #wpadminbar #wp-admin-bar-user-info .username { - color: $menu-submenu-text; + color: variables.$menu-submenu-text; } /* Pointers */ .wp-pointer .wp-pointer-content h3 { - background-color: $highlight-color; - border-color: darken( $highlight-color, 5% ); + background-color: variables.$highlight-color; + border-color: color.adjust(variables.$highlight-color, $lightness: -5%); } .wp-pointer .wp-pointer-content h3:before { - color: $highlight-color; + color: variables.$highlight-color; } .wp-pointer.wp-pointer-top .wp-pointer-arrow, .wp-pointer.wp-pointer-top .wp-pointer-arrow-inner, .wp-pointer.wp-pointer-undefined .wp-pointer-arrow, .wp-pointer.wp-pointer-undefined .wp-pointer-arrow-inner { - border-bottom-color: $highlight-color; + border-bottom-color: variables.$highlight-color; } @@ -553,22 +560,22 @@ ul#adminmenu > li.current > a.current:after { .media-item .bar, .media-progress-bar div { - background-color: $highlight-color; + background-color: variables.$highlight-color; } .details.attachment { box-shadow: inset 0 0 0 3px #fff, - inset 0 0 0 7px $highlight-color; + inset 0 0 0 7px variables.$highlight-color; } .attachment.details .check { - background-color: $highlight-color; - box-shadow: 0 0 0 1px #fff, 0 0 0 2px $highlight-color; + background-color: variables.$highlight-color; + box-shadow: 0 0 0 1px #fff, 0 0 0 2px variables.$highlight-color; } .media-selection .attachment.selection.details .thumbnail { - box-shadow: 0 0 0 1px #fff, 0 0 0 3px $highlight-color; + box-shadow: 0 0 0 1px #fff, 0 0 0 3px variables.$highlight-color; } @@ -577,49 +584,49 @@ ul#adminmenu > li.current > a.current:after { .theme-browser .theme.active .theme-name, .theme-browser .theme.add-new-theme a:hover:after, .theme-browser .theme.add-new-theme a:focus:after { - background: $highlight-color; + background: variables.$highlight-color; } .theme-browser .theme.add-new-theme a:hover span:after, .theme-browser .theme.add-new-theme a:focus span:after { - color: $highlight-color; + color: variables.$highlight-color; } .theme-section.current, .theme-filter.current { - border-bottom-color: $menu-background; + border-bottom-color: variables.$menu-background; } body.more-filters-opened .more-filters { - color: $menu-text; - background-color: $menu-background; + color: variables.$menu-text; + background-color: variables.$menu-background; } body.more-filters-opened .more-filters:before { - color: $menu-text; + color: variables.$menu-text; } body.more-filters-opened .more-filters:hover, body.more-filters-opened .more-filters:focus { - background-color: $menu-highlight-background; - color: $menu-highlight-text; + background-color: variables.$menu-highlight-background; + color: variables.$menu-highlight-text; } body.more-filters-opened .more-filters:hover:before, body.more-filters-opened .more-filters:focus:before { - color: $menu-highlight-text; + color: variables.$menu-highlight-text; } /* Widgets */ .widgets-chooser li.widgets-chooser-selected { - background-color: $menu-highlight-background; - color: $menu-highlight-text; + background-color: variables.$menu-highlight-background; + color: variables.$menu-highlight-text; } .widgets-chooser li.widgets-chooser-selected:before, .widgets-chooser li.widgets-chooser-selected:focus:before { - color: $menu-highlight-text; + color: variables.$menu-highlight-text; } @@ -627,29 +634,29 @@ body.more-filters-opened .more-filters:focus:before { .nav-menus-php .item-edit:focus:before { box-shadow: - 0 0 0 1px lighten($button-color, 10), - 0 0 2px 1px $button-color; + 0 0 0 1px color.adjust(variables.$button-color, $lightness: 10%), + 0 0 2px 1px variables.$button-color; } /* Responsive Component */ div#wp-responsive-toggle a:before { - color: $menu-icon; + color: variables.$menu-icon; } .wp-responsive-open div#wp-responsive-toggle a { // ToDo: make inset border border-color: transparent; - background: $menu-highlight-background; + background: variables.$menu-highlight-background; } .wp-responsive-open #wpadminbar #wp-admin-bar-menu-toggle a { - background: $menu-submenu-background; + background: variables.$menu-submenu-background; } .wp-responsive-open #wpadminbar #wp-admin-bar-menu-toggle .ab-icon:before { - color: $menu-icon; + color: variables.$menu-icon; } /* TinyMCE */ @@ -659,7 +666,7 @@ div#wp-responsive-toggle a:before { .mce-container.mce-menu .mce-menu-item:focus, .mce-container.mce-menu .mce-menu-item-normal.mce-active, .mce-container.mce-menu .mce-menu-item-preview.mce-active { - background: $highlight-color; + background: variables.$highlight-color; } /* Customizer */ @@ -668,24 +675,24 @@ div#wp-responsive-toggle a:before { #customize-controls .control-section .accordion-section-title:hover, #customize-controls .control-section.open .accordion-section-title, #customize-controls .control-section .accordion-section-title:focus { - color: $link; - border-left-color: $button-color; + color: variables.$link; + border-left-color: variables.$button-color; } .customize-controls-close:focus, .customize-controls-close:hover, .customize-controls-preview-toggle:focus, .customize-controls-preview-toggle:hover { - color: $link; - border-top-color: $button-color; + color: variables.$link; + border-top-color: variables.$button-color; } .customize-panel-back:hover, .customize-panel-back:focus, .customize-section-back:hover, .customize-section-back:focus { - color: $link; - border-left-color: $button-color; + color: variables.$link; + border-left-color: variables.$button-color; } .customize-screen-options-toggle:hover, @@ -695,7 +702,7 @@ div#wp-responsive-toggle a:before { #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:hover, #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:active, #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:focus { - color: $link; + color: variables.$link; } .customize-screen-options-toggle:focus:before, @@ -706,28 +713,28 @@ div#wp-responsive-toggle a:before { #customize-save-button-wrapper .save:focus, #publish-settings:focus { box-shadow: - 0 0 0 1px lighten($button-color, 10), - 0 0 2px 1px $button-color; + 0 0 0 1px color.adjust(variables.$button-color, $lightness: 10%), + 0 0 2px 1px variables.$button-color; } #customize-controls .customize-info.open .customize-help-toggle, #customize-controls .customize-info .customize-help-toggle:focus, #customize-controls .customize-info .customize-help-toggle:hover { - color: $link; + color: variables.$link; } .control-panel-themes .customize-themes-section-title:focus, .control-panel-themes .customize-themes-section-title:hover { - border-left-color: $button-color; - color: $link; + border-left-color: variables.$button-color; + color: variables.$link; } .control-panel-themes .theme-section .customize-themes-section-title.selected:after { - background: $button-color; + background: variables.$button-color; } .control-panel-themes .customize-themes-section-title.selected { - color: $link; + color: variables.$link; } #customize-theme-controls .control-section:hover > .accordion-section-title:after, @@ -738,37 +745,37 @@ div#wp-responsive-toggle a:before { #customize-outer-theme-controls .control-section .accordion-section-title:hover:after, #customize-outer-theme-controls .control-section.open .accordion-section-title:after, #customize-outer-theme-controls .control-section .accordion-section-title:focus:after { - color: $link; + color: variables.$link; } .customize-control .attachment-media-view .button-add-media:focus { background-color: #fbfbfc; - border-color: $button-color; + border-color: variables.$button-color; border-style: solid; - box-shadow: 0 0 0 1px $button-color; + box-shadow: 0 0 0 1px variables.$button-color; outline: 2px solid transparent; } .wp-full-overlay-footer .devices button:focus, .wp-full-overlay-footer .devices button.active:hover { - border-bottom-color: $button-color; + border-bottom-color: variables.$button-color; } .wp-full-overlay-footer .devices button:hover:before, .wp-full-overlay-footer .devices button:focus:before { - color: $button-color; + color: variables.$button-color; } .wp-full-overlay .collapse-sidebar:hover, .wp-full-overlay .collapse-sidebar:focus { - color: $button-color; + color: variables.$button-color; } .wp-full-overlay .collapse-sidebar:hover .collapse-sidebar-arrow, .wp-full-overlay .collapse-sidebar:focus .collapse-sidebar-arrow { box-shadow: - 0 0 0 1px lighten($button-color, 10), - 0 0 2px 1px $button-color; + 0 0 0 1px color.adjust(variables.$button-color, $lightness: 10%), + 0 0 2px 1px variables.$button-color; } &.wp-customizer .theme-overlay .theme-header .close:focus, @@ -777,7 +784,7 @@ div#wp-responsive-toggle a:before { &.wp-customizer .theme-overlay .theme-header .right:hover, &.wp-customizer .theme-overlay .theme-header .left:focus, &.wp-customizer .theme-overlay .theme-header .left:hover { - border-bottom-color: $button-color; - color: $link; + border-bottom-color: variables.$button-color; + color: variables.$link; } } diff --git a/src/wp-admin/css/colors/_mixins.scss b/src/wp-admin/css/colors/_mixins.scss index 9744a20a0e321..9575f1f2d623b 100644 --- a/src/wp-admin/css/colors/_mixins.scss +++ b/src/wp-admin/css/colors/_mixins.scss @@ -1,37 +1,132 @@ +@use 'sass:color'; +@use 'tokens'; + /* - * Button mixin- creates a button effect with correct - * highlights/shadows, based on a base color. + * Button mixin - creates a primary button effect. + * Uses CSS custom properties for theme color support across color schemes. */ -@mixin button( $button-color, $button-text-color: #fff ) { - background: $button-color; - border-color: $button-color; +@mixin button( $button-text-color: #fff ) { + background: var(--wp-admin-theme-color); + border-color: transparent; + border-radius: tokens.$radius-s; color: $button-text-color; - &:hover, - &:focus { - background: lighten( $button-color, 3% ); - border-color: darken( $button-color, 3% ); + &:hover { + background: var(--wp-admin-theme-color-darker-10); + border-color: transparent; color: $button-text-color; } &:focus { + background: var(--wp-admin-theme-color); + border-color: transparent; + color: $button-text-color; + /* Gutenberg-style focus ring: outer theme color + inset white for contrast */ box-shadow: - 0 0 0 1px #fff, - 0 0 0 3px $button-color; + 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color), + inset 0 0 0 1px tokens.$white; + /* Visible in Windows High Contrast mode */ + outline: 1px solid transparent; } &:active { - background: darken( $button-color, 5% ); - border-color: darken( $button-color, 5% ); + background: var(--wp-admin-theme-color-darker-20); + border-color: transparent; color: $button-text-color; } + &:disabled, + &.disabled { + background: tokens.$gray-100; + border-color: transparent; + color: tokens.$gray-600; + cursor: not-allowed; + } + &.active, &.active:focus, &.active:hover { - background: $button-color; + background: var(--wp-admin-theme-color-darker-10); color: $button-text-color; - border-color: darken( $button-color, 15% ); - box-shadow: inset 0 2px 5px -3px darken( $button-color, 50% ); + border-color: transparent; + box-shadow: none; + } +} + +/* + * Secondary button mixin - outlined style with theme color. + * Matches Gutenberg's .is-secondary button variant. + */ +@mixin button-secondary() { + background: transparent; + border: 1px solid var(--wp-admin-theme-color); + border-radius: tokens.$radius-s; + color: var(--wp-admin-theme-color); + + &:hover { + background: rgba(var(--wp-admin-theme-color--rgb), 0.04); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-color: var(--wp-admin-theme-color-darker-20); + color: var(--wp-admin-theme-color-darker-20); + box-shadow: none; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-300; + color: tokens.$gray-600; + cursor: not-allowed; + } +} + +/* + * Tertiary button mixin - transparent background, gray text. + */ +@mixin button-tertiary() { + background: transparent; + border: 1px solid tokens.$gray-600; + border-radius: tokens.$radius-s; + color: tokens.$gray-900; + + &:hover { + background: rgba(0, 0, 0, 0.05); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + color: tokens.$gray-900; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + outline: 1px solid transparent; + } + + &:active { + background: rgba(0, 0, 0, 0.1); + border-color: tokens.$gray-700; + color: tokens.$gray-900; + } + + &:disabled, + &.disabled { + background: transparent; + border-color: tokens.$gray-400; + color: tokens.$gray-600; + cursor: not-allowed; } } diff --git a/src/wp-admin/css/colors/_tokens.scss b/src/wp-admin/css/colors/_tokens.scss new file mode 100644 index 0000000000000..785f1f47f5076 --- /dev/null +++ b/src/wp-admin/css/colors/_tokens.scss @@ -0,0 +1,212 @@ +// ========================================================================== +// WordPress Design System Tokens +// ========================================================================== +// +// These tokens are derived from the WordPress Design System in Figma: +// https://www.figma.com/design/804HN2REV2iap2ytjRQ055/WordPress-Design-System +// +// IMPORTANT: Do NOT expose these as CSS custom properties. +// Use these Sass variables to compile to static CSS values. +// The only CSS custom properties available are those in wp-base-styles: +// - --wp-admin-theme-color +// - --wp-admin-theme-color--rgb +// - --wp-admin-theme-color-darker-10 +// - --wp-admin-theme-color-darker-20 +// - --wp-admin-border-width-focus +// +// ========================================================================== + + +// -------------------------------------------------------------------------- +// Grid Units (Spacing) +// -------------------------------------------------------------------------- +// Based on 4px base unit. Use for padding, margin, and gap values. + +$grid-unit-05: 4px; // Scales/grid unit 05 +$grid-unit-10: 8px; // Scales/grid unit 10 +$grid-unit-15: 12px; // Scales/grid unit 15 +$grid-unit-20: 16px; // Scales/grid unit 20 +$grid-unit-30: 24px; // Scales/grid unit 30 +$grid-unit-40: 32px; // Scales/grid unit 40 +$grid-unit-50: 40px; // Scales/grid unit 50 +$grid-unit-60: 48px; // Scales/grid unit 60 +$grid-unit-70: 56px; // Scales/grid unit 70 + + +// -------------------------------------------------------------------------- +// Border Radius +// -------------------------------------------------------------------------- + +$radius-xs: 1px; // radius-xs +$radius-s: 2px; // radius-s - Buttons, inputs +$radius-m: 4px; // radius-m - Focus rings +$radius-l: 8px; // radius-l - Cards, dashboard widgets +$radius-30: 12px; // Radius 30 +$radius-full: 9999px; // radius-full - Pills, avatars, circles + + +// -------------------------------------------------------------------------- +// Gray Scale +// -------------------------------------------------------------------------- +// Neutral colors for backgrounds, borders, and text. + +$gray-100: #f0f0f0; // Scales/Grays/gray-100 - Page background, disabled inputs +$gray-200: #e0e0e0; // Scales/Grays/gray-200 +$gray-300: #dddddd; // Scales/Grays/gray-300 +$gray-400: #cccccc; // Scales/Grays/gray-400 - Disabled borders +$gray-600: #949494; // Scales/Grays/gray-600 - Input borders, disabled text +$gray-700: #757575; // Scales/Grays/gray-700 +$gray-800: #2f2f2f; // Scales/Grays/gray-800 +$gray-900: #1e1e1e; // Scales/Grays/gray-900 - Primary text + +$white: #ffffff; // Scales/Black & White/white + + +// -------------------------------------------------------------------------- +// Theme Colors (Static reference values) +// -------------------------------------------------------------------------- +// For actual theme color usage, use var(--wp-admin-theme-color) instead. +// These are provided for reference and for contexts where CSS vars aren't available. + +$theme-reference: #3858e9; // Scales/Theme/theme (modern scheme) +$theme-darker-10-reference: #2145e6; // Scales/Theme/theme-darker-10 +$theme-darker-20-reference: #183ad6; // Scales/Theme/theme-darker-20 +$theme-alpha-04: rgba(56, 88, 233, 0.04); // Scales/Theme/theme-alpha-04 (4% opacity) +$theme-alpha-08: rgba(56, 88, 233, 0.08); // Scales/Theme/theme-alpha-08 (8% opacity) + +$brand-9: #4465db; // Scales/brand-9 - Focus ring color (static, not theme-dependent) + + +// -------------------------------------------------------------------------- +// Semantic Colors +// -------------------------------------------------------------------------- +// Use these for notices, alerts, and status indicators. +// These are intentionally NOT theme-dependent for consistency. + +$alert-yellow: #f0b849; // Scales/Yellow/alert-yellow - Warnings +$alert-green: #4ab866; // Scales/Green/alert-green - Success +$alert-red: #cc1818; // Scales/Red/alert-red - Errors +$alert-blue: #3858e9; // Info notices (matches modern theme) + +// Background tints for notices +$alert-yellow-bg: #fef8ee; // Warning notice background +$alert-green-bg: #eff9f1; // Success notice background +$alert-red-bg: #fcf0f0; // Error notice background + +$synced-color: #7a00df; // Scales/Purple/--wp-block-synced-color + + +// -------------------------------------------------------------------------- +// Text Colors +// -------------------------------------------------------------------------- + +$text-primary: $gray-900; // Primary text color +$text-secondary: $gray-700; // Secondary text +$text-tertiary: #5d5d5d; // Alias/text/text-tertiary - Placeholder, hints +$text-disabled: $gray-600; // Disabled text + + +// -------------------------------------------------------------------------- +// Component Tokens +// -------------------------------------------------------------------------- + +// Inputs +$input-bg: $white; // Alias/bg/bg-input +$input-border-color: $gray-600; // Default input border +$input-border-color-disabled: $gray-400; +$input-bg-disabled: $gray-100; +$input-border-width-default: 1px; // Input/Default +$input-border-width-focus: 1.5px; // Input/Focus +$field-spacing-horizontal: 8px; // Alias/field-spacing-horizontal + +// Checkboxes and Radios +$checkbox-size: 16px; // Alias/checkbox +$radio-size: 16px; // Alias/radio + +// Toggles +$toggle-width: 32px; // Alias/toggle-width +$toggle-height: 16px; // Alias/toggle-height + +// Buttons +// Note: Gutenberg is transitioning to 40px as the default button size. +// The "compact" size (32px) is available for space-constrained contexts. +$button-height-default: 40px; // Default button height (next-default-40px) +$button-height-compact: 32px; // Compact button height +$button-height-small: 24px; // Small button height + +// Cards and Surfaces +$card-bg: $white; +$card-border-color: rgba(0, 0, 0, 0.1); +$card-border-width: 1px; +$card-border-radius: $radius-l; // 8px for dashboard widgets +$card-border-radius-metabox: 0; // 0 for post editor metaboxes +$card-divider-color: rgba(0, 0, 0, 0.1); + +// Card Padding Sizes +$card-padding-xs: $grid-unit-10; // 8px - xSmall cards +$card-padding-sm: $grid-unit-20; // 16px - Small cards (metaboxes, dashboard widgets) +$card-padding-md-h: $grid-unit-30; // 24px - Medium cards horizontal +$card-padding-md-v: $grid-unit-20; // 16px - Medium cards vertical +$card-padding-lg-h: $grid-unit-40; // 32px - Large cards horizontal +$card-padding-lg-v: $grid-unit-30; // 24px - Large cards vertical + +// Page Layout +$page-padding-large: 48px; // Alias/page-large +$page-padding-small: 24px; // Alias/page-small + + +// -------------------------------------------------------------------------- +// Typography Scale +// -------------------------------------------------------------------------- + +// Font Sizes +$font-size-xs: 11px; // xs - Small labels, button small +$font-size-s: 12px; // s - Body small +$font-size-m: 13px; // m - Base body text, buttons +$font-size-l: 15px; // l - Body large, heading large +$font-size-xl: 20px; // xl - Heading XL + +// Line Heights +$line-height-xs: 16px; // xs +$line-height-s: 20px; // s - Most UI elements +$line-height-m: 24px; // m - Body large + +// Font Weights +$font-weight-regular: 400; // Regular - Body text +$font-weight-medium: 500; // Medium - Headings, buttons + + +// -------------------------------------------------------------------------- +// Elevation (Box Shadows) +// -------------------------------------------------------------------------- + +$elevation-xs: + 0 4px 4px rgba(0, 0, 0, 0.01), + 0 3px 3px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.02), + 0 1px 1px rgba(0, 0, 0, 0.03); + +$elevation-s: + 0 8px 8px rgba(0, 0, 0, 0.02), + 0 1px 2px rgba(0, 0, 0, 0.05); + +$elevation-m: + 0 16px 16px rgba(0, 0, 0, 0.02), + 0 4px 5px rgba(0, 0, 0, 0.03), + 0 2px 3px rgba(0, 0, 0, 0.05); + +$elevation-l: + 0 50px 43px rgba(0, 0, 0, 0.02), + 0 30px 36px rgba(0, 0, 0, 0.04), + 0 15px 27px rgba(0, 0, 0, 0.07), + 0 5px 15px rgba(0, 0, 0, 0.08); + + +// -------------------------------------------------------------------------- +// Layout +// -------------------------------------------------------------------------- + +$modal-width-small: 384px; // Layout/Modal small +$modal-width-medium: 512px; // Layout/Modal medium +$modal-width-large: 840px; // Layout/Modal large + diff --git a/src/wp-admin/css/colors/_variables.scss b/src/wp-admin/css/colors/_variables.scss index 8287a2320282c..d37c2b1392f00 100644 --- a/src/wp-admin/css/colors/_variables.scss +++ b/src/wp-admin/css/colors/_variables.scss @@ -1,21 +1,27 @@ +@use "sass:color"; + +// Import design system tokens +@use "tokens" as *; + // assign default value to all undefined variables +$scheme-name: "default" !default; // core variables $text-color: #fff !default; $base-color: #23282d !default; -$icon-color: hsl( hue( $base-color ), 7%, 95% ) !default; +$icon-color: hsl(color.channel($base-color, "hue", $space: hsl), 7%, 95%) !default; $highlight-color: #0073aa !default; $notification-color: #d54e21 !default; // global -$body-background: #f1f1f1 !default; +$body-background: $gray-100 !default; $link: #0073aa !default; -$link-focus: lighten( $link, 10% ) !default; +$link-focus: color.adjust($link, $lightness: 10%) !default; $button-color: $highlight-color !default; $button-text-color: $text-color !default; @@ -36,9 +42,9 @@ $menu-current-text: $menu-highlight-text !default; $menu-current-icon: $menu-highlight-icon !default; $menu-current-background: $menu-highlight-background !default; -$menu-submenu-text: mix( $base-color, $text-color, 30% ) !default; -$menu-submenu-background: darken( $base-color, 7% ) !default; -$menu-submenu-background-alt: desaturate( lighten( $menu-background, 7% ), 7% ) !default; +$menu-submenu-text: color.mix( $base-color, $text-color, 30% ) !default; +$menu-submenu-background: color.adjust($base-color, $lightness: -7%) !default; +$menu-submenu-background-alt: color.adjust(color.adjust($menu-background, $lightness: 7%), $saturation: -7%) !default; $menu-submenu-focus-text: $highlight-color !default; $menu-submenu-current-text: $text-color !default; @@ -53,13 +59,20 @@ $menu-collapse-icon: $menu-icon !default; $menu-collapse-focus-text: $text-color !default; $menu-collapse-focus-icon: $menu-highlight-icon !default; -$adminbar-avatar-frame: lighten( $menu-background, 7% ) !default; -$adminbar-input-background: lighten( $menu-background, 7% ) !default; +$adminbar-avatar-frame: color.adjust($menu-background, $lightness: 7%) !default; +$adminbar-input-background: color.adjust($menu-background, $lightness: 7%) !default; $adminbar-recovery-exit-text: $menu-bubble-text !default; $adminbar-recovery-exit-background: $menu-bubble-background !default; -$adminbar-recovery-exit-background-alt: mix(black, $adminbar-recovery-exit-background, 10%) !default; +$adminbar-recovery-exit-background-alt: color.mix(black, $adminbar-recovery-exit-background, 10%) !default; + +$menu-customizer-text: color.mix( $base-color, $text-color, 40% ) !default; + +// Dashboard Colors -$menu-customizer-text: mix( $base-color, $text-color, 40% ) !default; +$custom-welcome-panel: "true" !default; +$dashboard-accent-1: $menu-submenu-background !default; +$dashboard-accent-2: $menu-background !default; +$dashboard-icon-background: $dashboard-accent-2 !default; $low-contrast-theme: "false" !default; diff --git a/src/wp-admin/css/colors/blue/colors.scss b/src/wp-admin/css/colors/blue/colors.scss index b254d73a8fa51..1fca84c12bbc3 100644 --- a/src/wp-admin/css/colors/blue/colors.scss +++ b/src/wp-admin/css/colors/blue/colors.scss @@ -1,11 +1,16 @@ -$base-color: #52accc; -$icon-color: #e5f8ff; $highlight-color: #096484; -$notification-color: #e1a948; -$button-color: #e1a948; -$menu-submenu-text: #e2ecf1; -$menu-submenu-focus-text: #fff; -$menu-submenu-background: #4796b3; +@use "../_admin.scss" with ( + $scheme-name: "blue", + $base-color: #52accc, + $icon-color: #e5f8ff, + $highlight-color: $highlight-color, + $notification-color: #e1a948, + $button-color: #e1a948, -@import "../_admin.scss"; + $menu-submenu-text: #e2ecf1, + $menu-submenu-focus-text: #fff, + $menu-submenu-background: #4796b3, + + $dashboard-icon-background: $highlight-color +); diff --git a/src/wp-admin/css/colors/coffee/colors.scss b/src/wp-admin/css/colors/coffee/colors.scss index 64de62e4918de..80f846ae67195 100644 --- a/src/wp-admin/css/colors/coffee/colors.scss +++ b/src/wp-admin/css/colors/coffee/colors.scss @@ -1,8 +1,11 @@ $base-color: #59524c; -$highlight-color: #c7a589; -$notification-color: #9ea476; -$low-contrast-theme: "true"; -$form-checked: $base-color; +@use "../_admin.scss" with ( + $scheme-name: "coffee", + $base-color: $base-color, + $highlight-color: #c7a589, + $notification-color: #9ea476, + $form-checked: $base-color, -@import "../_admin.scss"; + $low-contrast-theme: "true" +); diff --git a/src/wp-admin/css/colors/ectoplasm/colors.scss b/src/wp-admin/css/colors/ectoplasm/colors.scss index 8d145707705f9..a38736a9a24d5 100644 --- a/src/wp-admin/css/colors/ectoplasm/colors.scss +++ b/src/wp-admin/css/colors/ectoplasm/colors.scss @@ -1,8 +1,11 @@ $base-color: #523f6d; -$icon-color: #ece6f6; -$highlight-color: #a3b745; -$notification-color: #d46f15; -$form-checked: $base-color; +@use "../_admin.scss" with ( + $scheme-name: "ectoplasm", + $base-color: $base-color, + $icon-color: #ece6f6, + $highlight-color: #a3b745, + $notification-color: #d46f15, -@import "../_admin.scss"; + $form-checked: $base-color, +); diff --git a/src/wp-admin/css/colors/light/colors.scss b/src/wp-admin/css/colors/light/colors.scss index 5a75889a55fd6..e9fde92e32a7b 100644 --- a/src/wp-admin/css/colors/light/colors.scss +++ b/src/wp-admin/css/colors/light/colors.scss @@ -1,26 +1,36 @@ -$base-color: #e5e5e5; -$icon-color: #999; -$text-color: #333; +@use "sass:color"; + $highlight-color: #04a4cc; -$notification-color: #d64e07; +$text-color: #333; +$menu-avatar-frame: #aaa; -$body-background: #f5f5f5; +@use "../_admin.scss" with ( + $scheme-name: "light", + $base-color: #e5e5e5, + $icon-color: #999, + $text-color: $text-color, + $highlight-color: $highlight-color, + $notification-color: #d64e07, -$menu-highlight-text: #fff; -$menu-highlight-icon: #ccc; -$menu-highlight-background: #888; + $body-background: #f5f5f5, -$menu-bubble-text: #fff; -$menu-avatar-frame: #aaa; -$menu-submenu-background: #fff; + $menu-highlight-text: #fff, + $menu-highlight-icon: #ccc, + $menu-highlight-background: #888, + + $menu-bubble-text: #fff, + $menu-submenu-background: #fff, -$menu-collapse-text: #777; -$menu-collapse-focus-icon: #555; + $menu-collapse-text: #777, + $menu-collapse-focus-icon: #555, -@import "../_admin.scss"; + $dashboard-accent-1: $highlight-color, + $dashboard-accent-2: color.adjust(color.adjust($highlight-color, $lightness: 7%), $saturation: -15%), + $dashboard-icon-background: $text-color +); /* Override the theme filter highlight color for this scheme */ .theme-section.current, .theme-filter.current { - border-bottom-color: $highlight-color; + border-bottom-color: admin.$highlight-color; } diff --git a/src/wp-admin/css/colors/midnight/colors.scss b/src/wp-admin/css/colors/midnight/colors.scss index 591232b70810b..21e9f2364020a 100644 --- a/src/wp-admin/css/colors/midnight/colors.scss +++ b/src/wp-admin/css/colors/midnight/colors.scss @@ -1,5 +1,14 @@ +@use "sass:color"; + $base-color: #363b3f; $highlight-color: #e14d43; $notification-color: #69a8bb; -@import "../_admin.scss"; +@use "../_admin.scss" with ( + $scheme-name: "midnight", + $base-color: $base-color, + $highlight-color: $highlight-color, + $notification-color: $notification-color, + + $dashboard-accent-2: color.mix($base-color, $notification-color, 90%), +); diff --git a/src/wp-admin/css/colors/modern/colors.scss b/src/wp-admin/css/colors/modern/colors.scss index dc32bf0e4d525..45c750c0f583a 100644 --- a/src/wp-admin/css/colors/modern/colors.scss +++ b/src/wp-admin/css/colors/modern/colors.scss @@ -1,9 +1,16 @@ -$base-color: #1e1e1e; +@use "sass:color"; + $highlight-color: #3858e9; -$menu-submenu-focus-text: #33f078; -$notification-color: $highlight-color; -$link: $highlight-color; -$link-focus: darken($highlight-color, 10%); +@use "../_admin.scss" with ( + $scheme-name: "modern", + $base-color: #1e1e1e, + $highlight-color: #3858e9, + $menu-submenu-focus-text: #7b90ff, + $notification-color: $highlight-color, + + $link: $highlight-color, + $link-focus: color.adjust($highlight-color, $lightness: -10%), -@import "../_admin.scss"; + $custom-welcome-panel: "false" +); diff --git a/src/wp-admin/css/colors/ocean/colors.scss b/src/wp-admin/css/colors/ocean/colors.scss index 807d98dccf0ad..d0ad861c94524 100644 --- a/src/wp-admin/css/colors/ocean/colors.scss +++ b/src/wp-admin/css/colors/ocean/colors.scss @@ -1,9 +1,12 @@ $base-color: #738e96; -$icon-color: #f2fcff; -$highlight-color: #9ebaa0; -$notification-color: #aa9d88; -$low-contrast-theme: "true"; -$form-checked: $base-color; +@use "../_admin.scss" with ( + $scheme-name: "ocean", + $base-color: $base-color, + $icon-color: #f2fcff, + $highlight-color: #9ebaa0, + $notification-color: #aa9d88, + $form-checked: $base-color, -@import "../_admin.scss"; + $low-contrast-theme: "true" +); diff --git a/src/wp-admin/css/colors/sunrise/colors.scss b/src/wp-admin/css/colors/sunrise/colors.scss index 5dd8d82fc1e58..146fd1196028b 100644 --- a/src/wp-admin/css/colors/sunrise/colors.scss +++ b/src/wp-admin/css/colors/sunrise/colors.scss @@ -1,6 +1,11 @@ -$base-color: #cf4944; +@use "sass:color"; + $highlight-color: #dd823b; -$notification-color: #ccaf0b; -$menu-submenu-focus-text: lighten( $highlight-color, 35% ); -@import "../_admin.scss"; +@use "../_admin.scss" with ( + $scheme-name: "sunrise", + $base-color: #cf4944, + $highlight-color: $highlight-color, + $notification-color: #ccaf0b, + $menu-submenu-focus-text: color.adjust($highlight-color, $lightness: 35%) +); diff --git a/src/wp-admin/css/common.css b/src/wp-admin/css/common.css index f75d9b55e1ef6..55b721e7f12da 100644 --- a/src/wp-admin/css/common.css +++ b/src/wp-admin/css/common.css @@ -122,8 +122,6 @@ .screen-reader-text span, .ui-helper-hidden-accessible { border: 0; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; margin: -1px; @@ -131,32 +129,43 @@ padding: 0; position: absolute; width: 1px; - word-wrap: normal !important; /* many screen reader and browser combinations announce broken words as they would appear visually */ + /* Many screen reader and browser combinations announce broken words as they would appear visually. */ + word-wrap: normal !important; + word-break: normal !important; } .button .screen-reader-text { height: auto; /* Fixes a Safari+VoiceOver bug, see ticket #42006 */ } +.screen-reader-text + .dashicons-external { + margin-top: -1px; + margin-left: 2px; + text-decoration: none; +} + .screen-reader-shortcut { position: absolute; top: -1000em; -} - -.screen-reader-shortcut:focus { left: 6px; - top: -25px; height: auto; width: auto; display: block; font-size: 14px; font-weight: 600; padding: 15px 23px 14px; - background: #f0f0f1; - color: #2271b1; + /* Background and color set to prevent false positives in automated accessibility tests. */ + background: #ffffff; + color: var(--wp-admin-theme-color, #3858e9); z-index: 100000; line-height: normal; - box-shadow: 0 0 2px 2px rgba(0, 0, 0, 0.6); +} + +.screen-reader-shortcut:focus { + top: -25px; + /* Overrides a:focus in the admin. See ticket #56789. */ + color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); text-decoration: none; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; @@ -188,7 +197,6 @@ p.popular-tags, .wp-editor-container, .popular-tags, .feature-filter, -.imgedit-group, .comment-ays { border: 1px solid #c3c4c7; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); @@ -201,7 +209,6 @@ p.popular-tags, .widgets-holder-wrap, .popular-tags, .feature-filter, -.imgedit-group, .comment-ays { background: #fff; } @@ -270,13 +277,13 @@ a:active { a:focus, a:focus .media-icon img, +a:focus .plugin-icon, .wp-person a:focus .gravatar { color: #043959; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); - /* Only visible in Windows High Contrast mode */ - outline: 1px solid transparent; + border-radius: 2px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } #adminmenu a:focus { @@ -551,6 +558,15 @@ code { margin-left: 0; } +.js-update-details-toggle .dashicons { + text-decoration: none; +} + +.js-update-details-toggle[aria-expanded="true"] .dashicons::before { + content: "\f142"; + content: "\f142" / ''; +} + .no-js .widefat thead .check-column input, .no-js .widefat tfoot .check-column input { display: none; @@ -571,10 +587,6 @@ code { margin: 10px 20px 0 2px; } -.wrap.block-editor-no-js { - padding-left: 20px; -} - .wrap > h2:first-child, /* Back-compat for pre-4.4 */ .wrap [class$="icon32"] + h2, /* Back-compat for pre-4.4 */ .postbox .inside h2, /* Back-compat for pre-4.4 */ @@ -613,20 +625,26 @@ code { .wrap .add-new-h2:active, /* deprecated */ .wrap .page-title-action, .wrap .page-title-action:active { - margin-left: 4px; - padding: 4px 8px; + display: inline-block; position: relative; - top: -3px; + box-sizing: border-box; + cursor: pointer; + white-space: nowrap; text-decoration: none; + text-shadow: none; + top: -3px; + margin-left: 4px; border: 1px solid #2271b1; border-radius: 2px; - text-shadow: none; - font-weight: 600; + background: transparent; font-size: 13px; - line-height: normal; /* IE8-IE11 need this for buttons */ + font-weight: 500; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height */ color: #2271b1; /* use the standard color used for buttons */ - background: #f6f7f7; - cursor: pointer; + padding: 0 12px; + -webkit-appearance: none; + } .wrap .wp-heading-inline + .page-title-action { @@ -635,7 +653,6 @@ code { .wrap .add-new-h2:hover, /* deprecated */ .wrap .page-title-action:hover { - background: #f0f0f1; border-color: #0a4b78; color: #0a4b78; } @@ -692,13 +709,13 @@ ul.striped > :nth-child(odd), .bar { background-color: #f0f0f1; - border-right-color: #4f94d4; + border-right-color: var(--wp-admin-theme-color); } /* Helper classes for plugins to leverage the active WordPress color scheme */ .highlight { - background-color: #f0f6fc; + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); color: #3c434a; } @@ -788,17 +805,17 @@ img.emoji { /* @todo can we combine these into a class or use an existing dashicon one? */ .welcome-panel .welcome-panel-close:before, .tagchecklist .ntdelbutton .remove-tag-icon:before, -#bulk-titles div a:before, +#bulk-titles .ntdelbutton:before, .notice-dismiss:before { background: none; - color: #787c82; - content: "\f153"; + color: #1e1e1e; + content: "\f335"; + content: "\f335" / ''; display: block; - font: normal 16px/20px dashicons; - speak: never; - height: 20px; + font: normal 20px/1 dashicons; + height: 1em; text-align: center; - width: 20px; + width: 1em; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -807,35 +824,29 @@ img.emoji { margin: 0; } -#bulk-titles div a:before { - margin: 1px 0; -} - .tagchecklist .ntdelbutton .remove-tag-icon:before { margin-left: 2px; border-radius: 50%; - color: #2271b1; + color: var(--wp-admin-theme-color, #3858e9); /* vertically center the icon cross browsers */ - line-height: 1.28; + line-height: 1.1; } .tagchecklist .ntdelbutton:focus { outline: 0; } -.welcome-panel .welcome-panel-close:hover:before, -.welcome-panel .welcome-panel-close:focus:before, .tagchecklist .ntdelbutton:hover .remove-tag-icon:before, .tagchecklist .ntdelbutton:focus .remove-tag-icon:before, -#bulk-titles div a:hover:before, -#bulk-titles div a:focus:before { +#bulk-titles .ntdelbutton:hover:before, +#bulk-titles .ntdelbutton:focus:before { color: #d63638; } .tagchecklist .ntdelbutton:focus .remove-tag-icon:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .key-labels label { @@ -908,6 +919,11 @@ a#remove-post-thumbnail:hover, border: none; } +.application-password-display .success { + color: #007017; + margin-left: 0.5rem; +} + /*------------------------------------------------------------------------------ 3.0 - Actions ------------------------------------------------------------------------------*/ @@ -917,33 +933,27 @@ a#remove-post-thumbnail:hover, clear: both; border-top: 1px solid #dcdcde; background: #f6f7f7; + display: flex; + align-items: center; + justify-content: space-between; } #delete-action { - float: left; line-height: 2.30769231; /* 30px */ } -#delete-link { - line-height: 2.30769231; /* 30px */ - vertical-align: middle; - text-align: left; - margin-left: 8px; -} - #delete-link a { text-decoration: none; } #publishing-action { text-align: right; - float: right; + margin-left: auto; line-height: 1.9; } #publishing-action .spinner { float: none; - margin-top: 5px; } #misc-publishing-actions { @@ -954,6 +964,7 @@ a#remove-post-thumbnail:hover, padding: 6px 10px 8px; } +.word-wrap-break-word, .misc-pub-filename { word-wrap: break-word; } @@ -1067,7 +1078,7 @@ th.action-links { .filter-links .current { box-shadow: none; - border-bottom: 4px solid #646970; + border-bottom: 4px solid var(--wp-admin-theme-color); color: #1d2327; } @@ -1075,16 +1086,17 @@ th.action-links { .filter-links li > a:focus, .show-filters .filter-links a.current:hover, .show-filters .filter-links a.current:focus { - color: #135e96; + color: var(--wp-admin-theme-color); } .wp-filter .search-form { float: right; - margin: 10px 0; + display: flex; + align-items: center; + column-gap: .5rem; } .wp-filter .search-form input[type="search"] { - margin: 1px 0; width: 280px; max-width: 100%; } @@ -1093,23 +1105,37 @@ th.action-links { margin: 0; } -/* Use flexbox only on the plugins install page. The `filter-links` and search form children will become flex items. */ -.plugin-install-php .wp-filter { +.wp-filter .search-form input[type="search"] { + min-height: 32px; + padding: 0 8px; +} + +.wp-filter .search-form select, +.wp-filter .filter-items select { + min-height: 32px; + line-height: 2.14285714; /* 30px for 32px height with 14px font */ + padding: 0 24px 0 8px; +} + +.wp-filter .button { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height with 13px font */ + padding: 0 12px; +} + +/* Use flexbox only on the plugins install page and upload page. The `filter-links` and search form children will become flex items. */ +.plugin-install-php .wp-filter, +.upload-php .wp-filter { display: flex; flex-wrap: wrap; justify-content: space-between; align-items: center; } -.wp-filter .search-form.search-plugins { - /* This element is a flex item: the inherited float won't have any effect. */ - margin-top: 0; -} - .wp-filter .search-form.search-plugins select, -.wp-filter .search-form.search-plugins .wp-filter-search { +.wp-filter .search-form.search-plugins .wp-filter-search, +.no-js .wp-filter .search-form.search-plugins .button { display: inline-block; - margin-top: 10px; vertical-align: top; } @@ -1125,6 +1151,7 @@ th.action-links { .wp-filter .drawer-toggle:before { content: "\f111"; + content: "\f111" / ''; margin: 0 5px 0 0; color: #646970; font: normal 16px/1 dashicons; @@ -1138,7 +1165,7 @@ th.action-links { .wp-filter .button.drawer-toggle:focus, .wp-filter .drawer-toggle:focus:before { background-color: transparent; - color: #135e96; + color: var(--wp-admin-theme-color); } .wp-filter .button.drawer-toggle:hover, @@ -1147,7 +1174,7 @@ th.action-links { } .wp-filter .button.drawer-toggle:focus { - border-color: #4f94d4; + border-color: var(--wp-admin-theme-color); } .wp-filter .button.drawer-toggle:active { @@ -1170,6 +1197,17 @@ th.action-links { overflow: hidden; } +.wp-filter .favorites-form .favorites-username { + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 0.5rem; +} + +.wp-filter .favorites-form .favorites-username input { + margin: 0; +} + .show-filters .filter-drawer, .show-favorites-form .favorites-form { display: block; @@ -1187,7 +1225,7 @@ th.action-links { .show-filters .wp-filter .drawer-toggle:hover, .show-filters .wp-filter .drawer-toggle:focus { - background: #2271b1; + background: var(--wp-admin-theme-color); } .show-filters .wp-filter .drawer-toggle:before { @@ -1278,11 +1316,13 @@ th.action-links { } .filtered-by .tags { - display: inline; + display: flex; + align-items: flex-start; + flex-wrap: wrap; + gap: 8px; } .filtered-by .tag { - margin: 0 5px; padding: 4px 8px; border: 1px solid #dcdcde; box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); @@ -1297,7 +1337,10 @@ th.action-links { } .filters-applied .filtered-by { - display: block; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 10px; } .filters-applied .filter-drawer { @@ -1322,6 +1365,18 @@ th.action-links { float: none; } +@media only screen and (max-width: 1138px) { + .wp-filter .search-form { + margin: 11px 0; + } +} + +@media only screen and (max-width: 1250px) { + .wp-filter:has(.plugin-install-search) .search-form { + margin: 11px 0; + } +} + @media only screen and (max-width: 1120px) { .filter-drawer { border-bottom: 1px solid #f0f0f1; @@ -1350,6 +1405,11 @@ th.action-links { position: relative; max-width: 100%; } + .wp-filter .search-form { + margin: 11px 0; + flex-wrap: wrap; + row-gap: 10px; + } } @media only screen and (max-width: 782px) { @@ -1382,11 +1442,11 @@ th.action-links { div.updated, div.error { background: #fff; - border: 1px solid #c3c4c7; - border-left-width: 4px; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); + border: none; + border-left: 4px solid #c3c4c7; + box-shadow: none; margin: 5px 15px 2px; - padding: 1px 12px; + padding: 8px 12px; } div[class="update-message"] { /* back-compat for pre-4.6 */ @@ -1399,15 +1459,31 @@ div.updated p, div.error p, .form-table td .notice p { margin: 0.5em 0; - padding: 2px; + padding: 0; + font-size: 13px; + line-height: 1.54; + color: #1e1e1e; } -.error a { +div.notice a, +div.error a, +div.updated a { + color: var(--wp-admin-theme-color-darker-10); text-decoration: underline; } -.updated a { - padding-bottom: 2px; +div.notice a:hover, +div.error a:hover, +div.updated a:hover { + color: var(--wp-admin-theme-color-darker-20); +} + +div.notice a:focus, +div.error a:focus, +div.updated a:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + outline: 2px solid transparent; + border-radius: 2px; } .notice-alt { @@ -1425,67 +1501,98 @@ div.error p, } .wp-core-ui .notice.is-dismissible { - padding-right: 38px; + padding-right: 48px; position: relative; } .notice-dismiss { position: absolute; - top: 0; - right: 1px; + top: 12px; + right: 12px; border: none; margin: 0; - padding: 9px; + padding: 0; background: none; - color: #787c82; + color: #1e1e1e; cursor: pointer; + width: 24px; + height: 24px; + display: flex; + align-items: center; + justify-content: center; + border-radius: 2px; } .notice-dismiss:hover:before, -.notice-dismiss:active:before, -.notice-dismiss:focus:before { - color: #d63638; +.notice-dismiss:active:before { + color: #1e1e1e; + opacity: 0.7; } .notice-dismiss:focus { - outline: none; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus) var(--wp-admin-theme-color); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + +.notice-dismiss:focus:before { + color: #1e1e1e; } .notice-success, div.updated { - border-left-color: #00a32a; + border-left-color: #4ab866; + background-color: #eff9f1; } -.notice-success.notice-alt { - background-color: #edfaef; +.notice-success.notice-alt, +div.updated.notice-alt { + background-color: #eff9f1; } .notice-warning { - border-left-color: #dba617; + border-left-color: #f0b849; + background-color: #fef8ee; } .notice-warning.notice-alt { - background-color: #fcf9e8; + background-color: #fef8ee; } .notice-error, div.error { - border-left-color: #d63638; + border-left-color: #cc1818; + background-color: #fcf0f0; } -.notice-error.notice-alt { - background-color: #fcf0f1; +.notice-error.notice-alt, +div.error.notice-alt { + background-color: #fcf0f0; } .notice-info { - border-left-color: #72aee6; + border-left-color: #3858e9; + background-color: #fff; } .notice-info.notice-alt { - background-color: #f0f6fc; + background-color: #fff; +} + +#plugin-information-footer .update-now:not(.button-disabled):before { + color: #d63638; + content: "\f463"; + content: "\f463" / ''; + display: inline-block; + font: normal 20px/1 dashicons; + margin: -3px 5px 0 -2px; + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; + vertical-align: middle; +} + +#plugin-information-footer .notice { + margin-top: -5px; } .update-message p:before, @@ -1495,7 +1602,9 @@ div.error { .button.updating-message:before, .button.updated-message:before, .button.installed:before, -.button.installing:before { +.button.installing:before, +.button.activating-message:before, +.button.activated-message:before { display: inline-block; font: normal 20px/1 'dashicons'; -webkit-font-smoothing: antialiased; @@ -1532,9 +1641,11 @@ div.error { .updating-message p:before, .import-php .updating-message:before, .button.updating-message:before, -.button.installing:before { +.button.installing:before, +.button.activating-message:before { color: #d63638; content: "\f463"; + content: "\f463" / ''; } /* Spins the update icon. */ @@ -1542,6 +1653,7 @@ div.error { .import-php .updating-message:before, .button.updating-message:before, .button.installing:before, +.button.activating-message:before, .plugins .column-auto-updates .dashicons-update.spin, .theme-overlay .theme-autoupdate .dashicons-update.spin { animation: rotation 2s infinite linear; @@ -1552,6 +1664,7 @@ div.error { .import-php .updating-message:before, .button.updating-message:before, .button.installing:before, + .button.activating-message:before, .plugins .column-auto-updates .dashicons-update.spin, .theme-overlay .theme-autoupdate .dashicons-update.spin { animation: none; @@ -1565,20 +1678,26 @@ div.error { /* Updated icon (check mark). */ .updated-message p:before, .installed p:before, -.button.updated-message:before { +.button.updated-message:before, +.button.activated-message:before { color: #68de7c; content: "\f147"; + content: "\f147" / ''; } /* Error icon. */ .update-message.notice-error p:before { color: #d63638; content: "\f534"; + content: "\f534" / ''; } .wrap .notice p:before, .import-php .updating-message:before { margin-right: 6px; +} + +.import-php .updating-message:before { vertical-align: bottom; } @@ -1647,33 +1766,55 @@ p.auto-update-status { .button.updating-message:before, .button.updated-message:before, .button.installed:before, -.button.installing:before { - margin: 3px 5px 0 -2px; +.button.installing:before, +.button.activated-message:before, +.button.activating-message:before { + margin: 0 5px 0 -2px; + line-height: 1.9; /* 38px (20px * 1.9) - matches button */ + vertical-align: top; } -.button-primary.updating-message:before { +#plugin-information-footer .button { + padding: 0 14px; + line-height: 2.71428571; /* 38px */ + font-size: 14px; + vertical-align: middle; + min-height: 40px; + margin-bottom: 4px; +} + +#plugin-information-footer .button.installed:before, +#plugin-information-footer .button.installing:before, +#plugin-information-footer .button.updating-message:before, +#plugin-information-footer .button.updated-message:before, +#plugin-information-footer .button.activated-message:before, +#plugin-information-footer .button.activating-message:before { + margin: 0 5px 0 -2px; + line-height: 1.9; /* 38px (20px * 1.9) - matches button */ + vertical-align: top; +} + +#plugin-information-footer .button.update-now.updating-message:before { + margin: 0 5px 0 -2px; +} + +.button-primary.updating-message:before, +.button-primary.activating-message:before { color: #fff; } -.button-primary.updated-message:before { +.button-primary.updated-message:before, +.button-primary.activated-message:before { color: #9ec2e6; } -.button.updated-message { +.button.updated-message, +.button.activated-message { transition-property: border, background, color; transition-duration: .05s; transition-timing-function: ease-in-out; } -@media aural { - .wrap .notice p:before, - .button.installing:before, - .button.installed:before, - .update-message p:before { - speak: never; - } -} - /* @todo: this does not need its own section anymore */ /*------------------------------------------------------------------------------ @@ -1732,12 +1873,13 @@ p.auto-update-status { border: 1px solid #c3c4c7; border-top: none; height: auto; + min-height: 32px; /* Compact size for header buttons */ margin-bottom: 0; - padding: 3px 6px 3px 16px; + padding: 0 6px 0 16px; background: #fff; border-radius: 0 0 4px 4px; color: #646970; - line-height: 1.7; + line-height: 2.30769231; /* 30px - matches compact button */ box-shadow: 0 0 0 transparent; transition: box-shadow 0.1s linear; } @@ -1749,8 +1891,10 @@ p.auto-update-status { } #screen-meta-links .show-settings:focus { - border-color: #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } #screen-meta-links .show-settings:active { @@ -1760,13 +1904,11 @@ p.auto-update-status { #screen-meta-links .show-settings:after { right: 0; content: "\f140"; - font: normal 20px/1 dashicons; - speak: never; + content: "\f140" / ''; + font: normal 20px/1.5 dashicons; /* line-height 1.5 = 30px to match compact button */ display: inline-block; padding: 0 5px 0 0; - bottom: 2px; - position: relative; - vertical-align: bottom; + vertical-align: top; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-decoration: none; @@ -1774,6 +1916,7 @@ p.auto-update-status { #screen-meta-links .screen-meta-active:after { content: "\f142"; + content: "\f142" / ''; } /* end screen options and help tabs */ @@ -1839,6 +1982,7 @@ p.auto-update-status { .metabox-prefs .screen-options .screen-per-page { margin-right: 15px; + padding-right: 0; } .metabox-prefs .screen-options label { @@ -1876,7 +2020,7 @@ p.auto-update-status { border: 1px solid #c3c4c7; border-top: none; border-bottom: none; - background: #f0f6fc; + background: rgba(var(--wp-admin-theme-color--rgb), 0.08); } #contextual-help-wrap.no-sidebar #contextual-help-back { @@ -1920,8 +2064,8 @@ p.auto-update-status { .contextual-help-tabs .active { padding: 0; margin: 0 -1px 0 0; - border-left: 2px solid #72aee6; - background: #f0f6fc; + border-left: 2px solid var(--wp-admin-theme-color); + background: color-mix(in srgb, var(--wp-admin-theme-color) 8%, white); box-shadow: 0 2px 0 rgba(0, 0, 0, 0.02), 0 1px 0 rgba(0, 0, 0, 0.02); } @@ -1962,7 +2106,7 @@ p.auto-update-status { ------------------------------------------------------------------------------*/ html.wp-toolbar { - padding-top: 32px; + padding-top: var(--wp-admin--admin-bar--height); box-sizing: border-box; -ms-overflow-style: scrollbar; /* See ticket #48545 */ } @@ -2087,8 +2231,8 @@ html.wp-toolbar { .postbox .handle-order-higher, .postbox .handle-order-lower, .postbox .handlediv { - width: 36px; - height: 36px; + width: 1.62rem; + height: 1.62rem; margin: 0; padding: 0; border: 0; @@ -2098,7 +2242,7 @@ html.wp-toolbar { .postbox .handle-order-higher, .postbox .handle-order-lower { - color: #787c82; + color: #646970; width: 1.62rem; } @@ -2116,8 +2260,9 @@ html.wp-toolbar { color: #a7aaad; } -.sortable-placeholder { +.sortable-placeholder:not(.empty-container .sortable-placeholder) { border: 1px dashed #c3c4c7; + border-radius: 8px; margin-bottom: 20px; } @@ -2128,7 +2273,7 @@ html.wp-toolbar { line-height: 1; } -.postbox.closed { +.postbox.closed .postbox-header { border-bottom: 0; } @@ -2307,7 +2452,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */ filter: alpha(opacity=70); width: 20px; height: 20px; - margin: 4px 10px 0; + margin: 10px 10px 0; } .spinner.is-active, @@ -2327,6 +2472,7 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */ } #template .submit .spinner { float: none; + vertical-align: top; } .metabox-holder .stuffbox > h3, /* Back-compat for pre-4.4 */ @@ -2341,8 +2487,59 @@ h1.nav-tab-wrapper, /* Back-compat for pre-4.4 */ /* Back-compat for nav-menus screen */ .nav-menus-php .metabox-holder h3 { + padding: 0; +} + +.accordion-container h3.accordion-section-title { + padding: 0 !important; +} + +.accordion-section-title button.accordion-trigger, +.nav-menus-php .metabox-holder .accordion-section-title button.accordion-trigger { + background: inherit; + color: #1d2327; + display: block; + position: relative; + text-align: left; + width: 100%; + outline: none; + border: 0; padding: 10px 10px 11px 14px; line-height: 1.5; + cursor: pointer; +} + +.accordion-section-title button.accordion-trigger:focus, +.nav-menus-php .metabox-holder .accordion-section-title button.accordion-trigger:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + outline: 2px solid transparent; +} + +.accordion-section-title span.dashicons.dashicons-arrow-down, +.nav-menus-php .metabox-holder .accordion-section-title span.dashicons.dashicons-arrow-down { + position: absolute; + right: 10px; + left: auto; + color: #787c82; + border-radius: 50px; + top: 50%; + transform: translateY(-50%); +} + +.accordion-section-title:hover span.dashicons.dashicons-arrow-down, +.nav-menus-php .metabox-holder .accordion-section-title:hover span.dashicons.dashicons-arrow-down { + color: #1d2327; +} + +.accordion-section-title span.dashicons.dashicons-arrow-down::before, +.nav-menus-php .metabox-holder .accordion-section-title span.dashicons.dashicons-arrow-down::before { + position: relative; + left: -1px; +} + +.accordion-section.open .accordion-section-title span.dashicons.dashicons-arrow-down, +.nav-menus-php .metabox-holder .accordion-section.open .accordion-section-title span.dashicons.dashicons-arrow-down { + transform: rotate(180deg) translate(0, 50%); } #templateside ul li a { @@ -2543,10 +2740,12 @@ div.star-holder .star-rating { .star-rating .star-full:before { content: "\f155"; + content: "\f155" / ''; } .star-rating .star-half:before { content: "\f459"; + content: "\f459" / ''; } .rtl .star-rating .star-half { @@ -2555,6 +2754,7 @@ div.star-holder .star-rating { .star-rating .star-empty:before { content: "\f154"; + content: "\f154" / ''; } div.action-links { @@ -3005,7 +3205,6 @@ div.action-links { } @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { #TB_window.plugin-details-modal.thickbox-loading:before { @@ -3030,19 +3229,25 @@ div.action-links { .plugin-details-modal #TB_closeWindowButton:hover, .plugin-details-modal #TB_closeWindowButton:focus { - color: #135e96; outline: none; box-shadow: none; } +.plugin-details-modal #TB_closeWindowButton:hover::after, +.plugin-details-modal #TB_closeWindowButton:focus::after { + outline: 2px solid; + outline-offset: -4px; + border-radius: 4px; +} + .plugin-details-modal .tb-close-icon { display: none; } .plugin-details-modal #TB_closeWindowButton:after { content: "\f335"; + content: "\f335" / ''; font: normal 32px/29px 'dashicons'; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -3068,9 +3273,9 @@ img { .bulk-action-notice .toggle-indicator::before, .privacy-text-box .toggle-indicator::before { content: "\f142"; + content: "\f142" / ''; display: inline-block; font: normal 20px/1 dashicons; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-decoration: none; @@ -3081,15 +3286,18 @@ img { .bulk-action-notice .bulk-action-errors-collapsed .toggle-indicator::before, .privacy-text-box.closed .toggle-indicator::before { content: "\f140"; + content: "\f140" / ''; } .postbox .handle-order-higher .order-higher-indicator::before { content: "\f343"; + content: "\f343" / ''; color: inherit; } .postbox .handle-order-lower .order-lower-indicator::before { content: "\f347"; + content: "\f347" / ''; color: inherit; } @@ -3125,11 +3333,10 @@ img { .postbox .handle-order-higher:focus, .postbox .handle-order-lower:focus, .postbox .handlediv:focus { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: inset 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + border-radius: 50%; /* Only visible in Windows High Contrast mode */ - outline: 1px solid transparent; + outline: 2px solid transparent; } .postbox .handle-order-higher:focus .order-higher-indicator::before, @@ -3145,7 +3352,7 @@ img { width: 300px; } -/* Theme/Plugin Editor */ +/* Theme/Plugin file editor */ .alignleft h2 { margin: 0; } @@ -3154,8 +3361,6 @@ img { font-family: Consolas, Monaco, monospace; font-size: 13px; background: #f6f7f7; - -moz-tab-size: 4; - -o-tab-size: 4; tab-size: 4; } @@ -3198,7 +3403,7 @@ img { } /* - * Styles for Theme and Plugin editors. + * Styles for Theme and Plugin file editors. */ /* Hide collapsed items. */ @@ -3216,9 +3421,11 @@ img { } [role="treeitem"][aria-expanded="false"] > .folder-label .icon:after { content: "\f139"; + content: "\f139" / ''; } [role="treeitem"][aria-expanded="true"] > .folder-label .icon:after { content: "\f140"; + content: "\f140" / ''; } [role="treeitem"] .folder-label { display: block; @@ -3230,12 +3437,17 @@ img { [role="treeitem"] { outline: 0; } + +[role="treeitem"] a:focus, [role="treeitem"] .folder-label.focus { color: #043959; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + /* Reset default focus style. */ + box-shadow: none; + /* Use an inset outline instead, so it's visible also over the current file item. */ + outline: 2px solid var(--wp-admin-theme-color, #3858e9); + outline-offset: -2px; } + [role="treeitem"].hover, [role="treeitem"] .folder-label.hover { background-color: #f0f0f1; @@ -3377,6 +3589,10 @@ img { text-decoration: none; } +#templateside li.current-file > a { + padding-bottom: 0; +} + #templateside li:not(.howto) > a:first-of-type { padding-top: 0; } @@ -3421,12 +3637,12 @@ img { /* @todo: can we use a common class for these? */ .nav-menus-php .item-edit:before, -.widget-top .widget-action .toggle-indicator:before, -.control-section .accordion-section-title:after, -.accordion-section-title:after { +.wp-customizer .control-section .accordion-section-title:after, +.wp-customizer .accordion-section-title:after, +.widget-top .widget-action .toggle-indicator:before { content: "\f140"; + content: "\f140" / ''; font: normal 20px/1 dashicons; - speak: never; display: block; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -3441,9 +3657,8 @@ img { .handlediv, .postbox .handlediv.button-link, .item-edit, -.toggle-indicator, -.accordion-section-title:after { - color: #787c82; +.toggle-indicator { + color: #646970; } .widget-action { @@ -3458,32 +3673,24 @@ img { .postbox .handlediv.button-link:focus, .item-edit:hover, .item-edit:focus, -.sidebar-name:hover .toggle-indicator, -.accordion-section-title:hover:after { +.sidebar-name:hover .toggle-indicator { color: #1d2327; /* Only visible in Windows High Contrast mode */ - outline: 1px solid transparent; + outline: 2px solid transparent; } .widget-top .widget-action:focus .toggle-indicator:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); -} - -.control-section .accordion-section-title:after, -.accordion-section-title:after { - float: right; - right: 20px; - top: -2px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } -.control-section.open .accordion-section-title:after, #customize-info.open .accordion-section-title:after, .nav-menus-php .menu-item-edit-active .item-edit:before, .widget.open .widget-top .widget-action .toggle-indicator:before, .widget.widget-in-question .widget-top .widget-action .toggle-indicator:before { content: "\f142"; + content: "\f142" / ''; } /*! @@ -3523,7 +3730,6 @@ img { .accordion-section-title { margin: 0; - padding: 12px 15px 15px; position: relative; border-left: 1px solid #dcdcde; border-right: 1px solid #dcdcde; @@ -3744,7 +3950,6 @@ img { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { /* Back-compat for pre-3.8 */ div.star-holder, @@ -3761,13 +3966,21 @@ img { @media screen and (max-width: 782px) { html.wp-toolbar { - padding-top: 46px; + padding-top: var(--wp-admin--admin-bar--height); } .screen-reader-shortcut:focus { top: -39px; } + .block-editor-page .screen-reader-shortcut:focus { + top: 7px; + } + + .screen-reader-shortcut[href="#wp-toolbar"] { + display: none; + } + body { min-width: 240px; overflow-x: hidden; @@ -3819,9 +4032,12 @@ img { .wrap .add-new-h2:active, /* deprecated */ .wrap .page-title-action, .wrap .page-title-action:active { - padding: 10px 15px; + padding: 0 14px; font-size: 14px; white-space: nowrap; + min-height: 40px; + line-height: 2.71428571; + vertical-align: middle; } /* Feedback Messages */ @@ -3869,6 +4085,10 @@ img { padding: 12px; } + .nav-menus-php .metabox-holder h3 { + padding: 0; + } + .postbox .handlediv { margin-top: 3px; } @@ -4112,11 +4332,22 @@ img { border-bottom: 1px solid #c3c4c7; } - .wp-filter .search-form input[type="search"] { + .wp-filter .search-form.search-plugins label { width: 100%; } } +@media screen and (max-width: 480px) { + .metabox-prefs-container { + display: grid; + } + + .metabox-prefs-container > * { + display: inline-block; + padding: 2px; + } +} + @media screen and (max-width: 320px) { /* Prevent default center alignment and larger font for the Right Now widget when the network dashboard is viewed on a small mobile device. */ diff --git a/src/wp-admin/css/customize-controls.css b/src/wp-admin/css/customize-controls.css index bdd28880c19f0..3dfdfea790fb3 100644 --- a/src/wp-admin/css/customize-controls.css +++ b/src/wp-admin/css/customize-controls.css @@ -31,7 +31,7 @@ body { max-width: 366px; min-height: 64px; width: auto; - padding: 25px 25px 25px 109px; + padding: 25px; position: relative; background: #fff; box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); @@ -41,6 +41,10 @@ body { top: calc( 50% - 100px ); } +#customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .customize-changeset-locked-message.has-avatar { + padding-left: 109px; +} + #customize-controls #customize-notifications-area .notice.notification-overlay.notification-changeset-locked .currently-editing { margin-top: 0; } @@ -66,7 +70,7 @@ body { #customize-save-button-wrapper { float: right; - margin-top: 9px; + margin-top: 7px; /* Vertically center 32px button in 45px header */ } body:not(.ready) #customize-save-button-wrapper .save { @@ -79,10 +83,6 @@ body:not(.ready) #customize-save-button-wrapper .save { margin-top: 0; } -#customize-save-button-wrapper .save:focus, #publish-settings:focus { - box-shadow: 0 1px 0 #2271b1, 0 0 2px 1px #72aee6; /* This is default box shadow for focus */ -} - #customize-save-button-wrapper .save.has-next-sibling { border-radius: 3px 0 0 3px; } @@ -181,7 +181,7 @@ body.trashing #publish-settings { } #customize-header-actions .spinner { - margin-top: 13px; + margin-top: 13px; /* Vertically center 20px spinner in 45px header */ margin-right: 4px; } @@ -230,6 +230,7 @@ body.trashing #publish-settings { } #customize-control-trash_changeset .button-link:before { content: "\f182"; + content: "\f182" / ''; font: normal 22px dashicons; text-decoration: none; position: absolute; @@ -267,8 +268,10 @@ body.trashing #publish-settings { .preview-link-wrapper .customize-copy-preview-link.preview-control-element.button { margin: 0; position: absolute; - bottom: 9px; + top: 50%; + transform: translateY(-50%) !important; right: 0; + background: #fff !important; } .preview-link-wrapper { @@ -278,10 +281,10 @@ body.trashing #publish-settings { .customize-copy-preview-link:before, .customize-copy-preview-link:after { content: ""; - height: 28px; + height: 40px; position: absolute; background: #fff; - top: -1px; + top: 0; } .customize-copy-preview-link:before { @@ -361,6 +364,10 @@ body.trashing #publish-settings { width: 46px; } +.customize-control-date_time select { + vertical-align: top; +} + .date-time-fields .date-input.year { width: 65px; } @@ -393,6 +400,7 @@ body.trashing #publish-settings { margin-bottom: 0; } +#customize-control-site_icon .customize-control-description, #customize-control-changeset_scheduled_date .customize-control-description { font-style: normal; } @@ -426,6 +434,7 @@ body.trashing #publish-settings { border-right: none; border-bottom: none; cursor: default; + padding: 10px 10px 11px 14px; } #customize-controls .customize-info.open .accordion-section-title:after, @@ -441,6 +450,9 @@ body.trashing #publish-settings { #customize-controls .customize-info .preview-notice { font-size: 13px; line-height: 1.9; + margin: 0; + font-weight: 400; + color: #50575e; } #customize-controls .customize-pane-child .customize-section-title h3, @@ -472,7 +484,6 @@ body.trashing #publish-settings { height: 20px; cursor: pointer; box-shadow: none; - -webkit-appearance: none; background: transparent; color: #50575e; border: none; @@ -484,12 +495,6 @@ body.trashing #publish-settings { left: 6px; } -#customize-controls .customize-info.open .customize-help-toggle, -#customize-controls .customize-info .customize-help-toggle:focus, -#customize-controls .customize-info .customize-help-toggle:hover { - color: #2271b1; -} - #customize-controls .customize-info .customize-panel-description, #customize-controls .customize-info .customize-section-description, #customize-outer-theme-controls .customize-info .customize-section-description, @@ -549,6 +554,28 @@ body.trashing #publish-settings { .15s border-color ease-in-out; } +.accordion-section-title:has(button.accordion-trigger), +#customize-controls .current-panel .control-section > h3.accordion-section-title:has(button.accordion-trigger) { + padding: 0; +} + +.accordion-section-title button.accordion-trigger { + all: unset; + width: 100%; + padding: 10px 30px 11px 14px; + display: flex; + align-items: center; + box-sizing: border-box; +} + +.accordion-section-title button.accordion-trigger:has(.menu-in-location) { + display: block; +} + +.accordion-section-title button.accordion-trigger .spinner { + margin-top: 0; +} + @media (prefers-reduced-motion: reduce) { #customize-theme-controls .accordion-section-title, #customize-outer-theme-controls .accordion-section-title { @@ -565,7 +592,9 @@ body.trashing #publish-settings { #customize-theme-controls .accordion-section-title:after, #customize-outer-theme-controls .accordion-section-title:after { content: "\f345"; + content: "\f345" / ''; color: #a7aaad; + pointer-events: none; } #customize-theme-controls .accordion-section-content, @@ -574,15 +603,6 @@ body.trashing #publish-settings { background: transparent; } -#customize-controls .control-section:hover > .accordion-section-title, -#customize-controls .control-section .accordion-section-title:hover, -#customize-controls .control-section.open .accordion-section-title, -#customize-controls .control-section .accordion-section-title:focus { - color: #2271b1; - background: #f6f7f7; - border-left-color: #2271b1; -} - #accordion-section-themes + .control-section { border-top: 1px solid #dcdcde; } @@ -594,17 +614,6 @@ body.trashing #publish-settings { background: #f6f7f7; } -#customize-theme-controls .control-section:hover > .accordion-section-title:after, -#customize-theme-controls .control-section .accordion-section-title:hover:after, -#customize-theme-controls .control-section.open .accordion-section-title:after, -#customize-theme-controls .control-section .accordion-section-title:focus:after, -#customize-outer-theme-controls .control-section:hover > .accordion-section-title:after, -#customize-outer-theme-controls .control-section .accordion-section-title:hover:after, -#customize-outer-theme-controls .control-section.open .accordion-section-title:after, -#customize-outer-theme-controls .control-section .accordion-section-title:focus:after { - color: #2271b1; -} - #customize-theme-controls .control-section.open { border-bottom: 1px solid #f0f0f1; } @@ -811,13 +820,18 @@ h3.customize-section-title { color: #3c434a; text-align: left; cursor: pointer; - transition: - color .15s ease-in-out, - border-color .15s ease-in-out, - background .15s ease-in-out; box-sizing: content-box; } +@media (prefers-reduced-motion: no-preference) { + .customize-controls-close { + transition: + color .15s ease-in-out, + border-color .15s ease-in-out, + background .15s ease-in-out; + } +} + .customize-panel-back, .customize-section-back { display: block; @@ -867,8 +881,6 @@ h3.customize-section-title { .customize-controls-preview-toggle:focus, .customize-controls-preview-toggle:hover { background: #fff; - color: #2271b1; - border-top-color: #2271b1; box-shadow: none; /* Only visible in Windows High Contrast mode */ outline: 1px solid transparent; @@ -884,9 +896,7 @@ h3.customize-section-title { .customize-panel-back:focus, .customize-section-back:hover, .customize-section-back:focus { - color: #2271b1; background: #f6f7f7; - border-left-color: #2271b1; box-shadow: none; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; @@ -896,6 +906,7 @@ h3.customize-section-title { .customize-controls-close:before { font: normal 22px/45px dashicons; content: "\f335"; + content: "\f335" / ''; position: relative; top: -3px; left: 13px; @@ -905,6 +916,7 @@ h3.customize-section-title { .customize-section-back:before { font: normal 20px/72px dashicons; content: "\f341"; + content: "\f341" / ''; position: relative; left: 9px; } @@ -990,7 +1002,8 @@ p.customize-section-description { .customize-section-description a.external-link:after { font: 16px/11px dashicons; - content: "\f310"; + content: "\f504"; + content: "\f504" / ''; top: 3px; position: relative; padding-left: 3px; @@ -1067,10 +1080,6 @@ p.customize-section-description { line-height: 0; } -/* Remove descender space. */ -.customize-control-site_icon .favicon-preview .browser-preview { - vertical-align: top; -} .customize-control .thumbnail-image img { cursor: pointer; @@ -1083,14 +1092,30 @@ p.customize-section-description { float: left; } -#available-menu-items .accordion-section-content .new-content-item, -.customize-control-dropdown-pages .new-content-item { +#available-menu-items .accordion-section-content .new-content-item-wrapper, +.customize-control-dropdown-pages .new-content-item-wrapper { width: calc(100% - 30px); padding: 8px 15px; position: absolute; bottom: 0; z-index: 10; background: #f0f0f1; +} + +.customize-control-dropdown-pages .new-content-item-wrapper { + width: 100%; + padding: 0; + position: static; +} + +#available-menu-items .new-content-item-wrapper > label, +.customize-control-dropdown-pages .new-content-item-wrapper > label { + margin-bottom: 4px; + display: block; +} + +#available-menu-items .accordion-section-content .new-content-item, +.customize-control-dropdown-pages .new-content-item { display: flex; } @@ -1100,17 +1125,36 @@ p.customize-section-description { position: relative; } +.customize-control-dropdown-pages .new-content-item-wrapper .new-content-item { + padding: 0; +} + +.customize-control-dropdown-pages .new-content-item-wrapper .new-content-item label { + line-height: 1.6; +} + #available-menu-items .new-content-item .create-item-input, .customize-control-dropdown-pages .new-content-item .create-item-input { flex-grow: 10; + width: 100%; +} + +#available-menu-items .new-content-item .create-item-input { + min-height: 32px; + line-height: 2.15384615; /* 28px for 32px min-height with 13px font */ } #available-menu-items .new-content-item .add-content, .customize-control-dropdown-pages .new-content-item .add-content { - margin: 2px 0 2px 6px; + margin: 0 0 0 6px; flex-grow: 1; } +#available-menu-items .new-content-item .add-content { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height with 13px font */ +} + .customize-control-dropdown-pages .new-content-item .create-item-input.invalid { border: 1px solid #d63638; } @@ -1308,8 +1352,8 @@ p.customize-section-description { .customize-control .dropdown-arrow:after { content: "\f140"; + content: "\f140" / ''; font: normal 20px/1 dashicons; - speak: never; display: block; padding: 0; text-indent: 0; @@ -1391,6 +1435,20 @@ p.customize-section-description { white-space: normal; } +.customize-control .attachment-media-view .upload-button { + width: 100%; + text-align: center; +} + +.customize-control .attachment-media-view .upload-button.control-focus { + width: auto; +} + +.customize-control.customize-control-header .actions .upload-button.button.new { + width: 100%; + text-align: center; +} + .customize-control .attachment-media-view .thumbnail, .customize-control-header .current .container { overflow: hidden; @@ -1526,7 +1584,7 @@ p.customize-section-description { } .customize-control-header button.random .dice { - margin-top: 4px; + margin-top: 0; } .customize-control-header .placeholder:hover .dice, @@ -1567,10 +1625,9 @@ p.customize-section-description { } .customize-control-header .choice:focus { - outline: none; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 3px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .customize-control-header .uploaded div:last-child > .choice { @@ -1605,8 +1662,6 @@ p.customize-section-description { font-family: Consolas, Monaco, monospace; font-size: 12px; padding: 6px 8px; - -moz-tab-size: 2; - -o-tab-size: 2; tab-size: 2; } .customize-control-code_editor textarea, @@ -1662,6 +1717,9 @@ p.customize-section-description { .theme-browser .theme.active .theme-actions, .wp-customizer .theme-browser .theme .theme-actions { padding: 9px 15px; +} + +.theme-browser .theme:not(.active) .theme-actions { box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); } @@ -1693,7 +1751,7 @@ p.customize-section-description { border-left: none; border-right: none; margin: 0 0 15px; - padding-right: 100px; /* Space for the button */ + padding: 12px 100px 15px 15px; /* Space for the button */ } #customize-theme-controls .control-section-themes .customize-themes-panel .accordion-section-title:first-child:hover, /* Not a focusable element. */ @@ -1729,7 +1787,13 @@ p.customize-section-description { position: absolute; right: 10px; top: 50%; - margin-top: -14px; + margin-top: -20px; /* Half of 40px button height for vertical centering */ + font-weight: 400; +} + +#customize-notifications-area .notification-message button.switch-to-editor { + display: block; + margin-top: 6px; font-weight: 400; } @@ -1814,12 +1878,13 @@ p.customize-section-description { } .themes-filter-bar .feature-filter-toggle { - float: right; - margin: 3px 0 3px 25px; + min-height: 32px; + line-height: 2.30769231; } .themes-filter-bar .feature-filter-toggle:before { content: "\f111"; + content: "\f111" / ''; margin: 0 5px 0 0; font: normal 16px/1 dashicons; vertical-align: text-bottom; @@ -1878,12 +1943,6 @@ p.customize-section-description { animation: .6s themes-fade-in 1; } -.control-panel-themes .filter-themes-count { - position: relative; - float: right; - line-height: 2.6; -} - .control-panel-themes .filter-themes-count .themes-displayed { font-weight: 600; color: #50575e; @@ -1944,8 +2003,6 @@ p.customize-section-description { .control-panel-themes .customize-themes-section-title:focus, .control-panel-themes .customize-themes-section-title:hover { - border-left-color: #2271b1; - color: #2271b1; background: #f6f7f7; } @@ -1964,6 +2021,7 @@ p.customize-section-description { .control-panel-themes .theme-section .customize-themes-section-title.selected:after { content: "\f147"; + content: "\f147" / ''; font: 16px/1 dashicons; box-sizing: border-box; width: 20px; @@ -1973,14 +2031,9 @@ p.customize-section-description { position: absolute; top: 9px; right: 15px; - background: #2271b1; color: #fff; } -.control-panel-themes .customize-themes-section-title.selected { - color: #2271b1; -} - #customize-theme-controls .themes.accordion-section-content { position: relative; left: 0; @@ -2095,6 +2148,28 @@ p.customize-section-description { box-sizing: border-box; border-bottom: 1px solid #dcdcde; } +.customize-preview-header.themes-filter-bar, +.customize-preview-header.themes-filter-bar .search-form { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; +} + +.customize-preview-header.themes-filter-bar .search-form-input { + position: relative; +} + +.customize-preview-header .filter-themes-wrapper { + display: grid; + align-items: center; + gap: 10px; + grid-template-columns: auto 1fr; +} + +.customize-preview-header .filter-themes-wrapper .filter-themes-count { + justify-self: end; +} @media screen and (min-width: 1670px) { .customize-preview-header.themes-filter-bar { @@ -2107,19 +2182,21 @@ p.customize-section-description { .themes-filter-bar .themes-filter-container { margin: 0; padding: 0; + display: flex; + align-items: center; + gap: 10px; } .themes-filter-bar .wp-filter-search { - line-height: 1.8; - padding: 6px 10px 6px 30px; + padding: 0 10px 0 30px; max-width: 100%; width: 40%; min-width: 300px; - position: absolute; - top: 6px; - left: 25px; height: 32px; + min-height: 32px; /* Override global 40px min-height for compact bar */ margin: 1px 0; + top: 0; + left: 0; } /* Unstick the filter bar on short windows/screens. This breakpoint is based on the @@ -2153,18 +2230,31 @@ p.customize-section-description { } } -@media screen and (max-width: 900px) { +@media screen and (max-width: 960px) { .customize-preview-header.themes-filter-bar { - height: 86px; - padding-top: 46px; + height: 96px; } +} +@media screen and (max-width: 900px) { .themes-filter-bar .wp-filter-search { - width: calc(100% - 50px); + width: 100%; margin: 0; min-width: 200px; } + .customize-preview-header.themes-filter-bar, + .customize-preview-header.themes-filter-bar .search-form + .themes-filter-bar .themes-filter-container { + display: grid; + gap: 4px; + } + + .customize-preview-header.themes-filter-bar .search-form-input { + display: flex; + flex-grow: 1; + } + .filter-drawer { top: 86px; } @@ -2245,9 +2335,7 @@ p.customize-section-description { .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:hover, .wp-customizer .showing-themes .control-panel-themes .customize-themes-mobile-back:focus { - color: #2271b1; background: #f6f7f7; - border-left-color: #2271b1; box-shadow: none; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; @@ -2314,7 +2402,7 @@ p.customize-section-description { .wp-customizer .theme-overlay .theme-actions { text-align: right; /* Because there're only one or two actions, match the UI pattern of media modals and right-align the action. */ - padding: 10px 25px; + padding: 10px 25px 5px; background: #f0f0f1; border-top: 1px solid #dcdcde; } @@ -2323,13 +2411,6 @@ p.customize-section-description { margin-left: 8px; } -.control-panel-themes .theme-actions .delete-theme { - left: 15px; /* these override themes.css on mobile */ - right: auto; - bottom: auto; - position: absolute; -} - .modal-open .in-themes-panel #customize-controls .wp-full-overlay-sidebar-content { overflow: visible; /* Prevent the top-level Customizer controls from becoming visible when elements on the right of the details modal are focused. */ } @@ -2350,13 +2431,11 @@ p.customize-section-description { .wp-customizer .theme-overlay .theme-header .left:focus, .wp-customizer .theme-overlay .theme-header .left:hover { background: #fff; - border-bottom: 4px solid #2271b1; - color: #2271b1; } .wp-customizer .theme-overlay .theme-header .close:focus:before, .wp-customizer .theme-overlay .theme-header .close:hover:before { - color: #2271b1; + color: var(--wp-admin-theme-color); } .wp-customizer .theme-overlay .theme-header button.disabled, @@ -2437,6 +2516,7 @@ body.cheatin p { .add-new-menu-item:before, #available-menu-items .new-content-item .add-content:before { content: "\f132"; + content: "\f132" / ''; display: inline-block; position: relative; left: -2px; @@ -2516,11 +2596,13 @@ body.cheatin p { .move-widget-down:before, .menus-move-down:before { content: "\f347"; + content: "\f347" / ''; } .move-widget-up:before, .menus-move-up:before { content: "\f343"; + content: "\f343" / ''; } #customize-theme-controls .first-widget .move-widget-up, @@ -2579,13 +2661,33 @@ body.adding-widget .add-new-widget:before, border-right: 1px solid #dcdcde; } +#available-widgets .accordion-section-title, +#available-menu-items .accordion-section-title { + z-index: 2; +} + #available-widgets .customize-section-title, #available-menu-items .customize-section-title { + border: 0; + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + /* Many screen reader and browser combinations announce broken words as they would appear visually. */ + word-wrap: normal !important; + word-break: normal !important; +} + +#available-widgets .customize-section-title button, +#available-menu-items .customize-section-title button { display: none; } #available-widgets-list { - top: 60px; + top: 82px; position: absolute; overflow: auto; bottom: 0; @@ -2615,7 +2717,7 @@ body.adding-widget .add-new-widget:before, #available-widgets-filter input, #available-menu-items-search input { width: 100%; - min-height: 32px; + min-height: 40px; margin: 1px 0; padding: 0 30px; } @@ -2629,7 +2731,7 @@ body.adding-widget .add-new-widget:before, #available-widgets-filter .search-icon { display: block; position: absolute; - top: 15px; /* 13 container padding +1 input margin +1 input border */ + bottom: 19px; /* 13 container padding +1 input margin +1 input border +4 centering in 40px input */ left: 16px; width: 30px; height: 30px; @@ -2639,9 +2741,9 @@ body.adding-widget .add-new-widget:before, } #available-widgets-filter .clear-results, -#available-menu-items-search .clear-results { +#available-menu-items-search .accordion-section-title .clear-results { position: absolute; - top: 15px; /* 13 container padding +1 input margin +1 input border */ + top: 40px; /* 13 container padding +1 input margin +1 input border +4 centering in 40px input */ right: 16px; width: 30px; height: 30px; @@ -2668,6 +2770,7 @@ body.adding-widget .add-new-widget:before, #available-widgets-filter .clear-results:before, #available-menu-items-search .clear-results:before { content: "\f335"; + content: "\f335" / ''; font: normal 20px/1 dashicons; vertical-align: middle; -webkit-font-smoothing: antialiased; @@ -2683,9 +2786,10 @@ body.adding-widget .add-new-widget:before, #available-widgets-filter .clear-results:focus, #available-menu-items-search .clear-results:focus { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + border-radius: 2px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } #available-menu-items-search .search-icon:after, @@ -2700,8 +2804,8 @@ body.adding-widget .add-new-widget:before, .themes-filter-bar .search-icon { position: absolute; - top: 7px; - left: 26px; + top: 2px; + left: 2px; z-index: 1; color: #646970; height: 30px; @@ -2809,18 +2913,14 @@ body.adding-widget .add-new-widget:before, bottom: 16px; } - .preview-link-wrapper .customize-copy-preview-link.preview-control-element.button { - bottom: 10px; - } - .media-widget-control .media-widget-buttons .button.edit-media, .media-widget-control .media-widget-buttons .button.change-media, .media-widget-control .media-widget-buttons .button.select-media { margin-top: 12px; } - .wp-core-ui .themes-filter-bar .feature-filter-toggle { - margin: 3px 0 3px 25px; + .customize-preview-header.themes-filter-bar .search-icon { + top: 6px; } } @@ -2879,6 +2979,7 @@ body.adding-widget .add-new-widget:before, .customize-controls-preview-toggle .controls:before { font: normal 20px/1 dashicons; content: "\f177"; + content: "\f177" / ''; position: relative; top: 4px; margin-right: 6px; @@ -2886,6 +2987,7 @@ body.adding-widget .add-new-widget:before, .customize-controls-preview-toggle .controls:before { content: "\f540"; + content: "\f540" / ''; } .preview-only #customize-controls { @@ -2898,13 +3000,16 @@ body.adding-widget .add-new-widget:before, } .wp-core-ui.wp-customizer .button { - min-height: 30px; padding: 0 14px; - line-height: 2; + line-height: 2.14285714; /* 30px */ font-size: 14px; vertical-align: middle; } + .customize-control .attachment-media-view .upload-button { + text-align: center; + } + #customize-control-changeset_status .customize-inside-control-row { padding-top: 15px; } @@ -2917,8 +3022,19 @@ body.adding-widget .add-new-widget:before, #available-widgets .customize-section-title, #available-menu-items .customize-section-title { - display: block; + border: 0; + clip-path: none; + height: inherit; margin: 0; + overflow: hidden; + padding: 0; + width: auto; + position: static; + } + + #available-widgets .customize-section-title button, + #available-menu-items .customize-section-title button { + display: block; } #available-widgets .customize-section-back, @@ -2957,22 +3073,18 @@ body.adding-widget .add-new-widget:before, } #available-widgets-list { - top: 130px; + top: 152px; } - #available-menu-items-search .clear-results, - #available-menu-items-search .search-icon { - top: 85px; /* 70 section title height + 13 container padding +1 input margin +1 input border */ + #available-menu-items-search .clear-results { + top: 40px; + right: 16px; } .reorder, .reordering .reorder-done { padding: 8px; } - - .wp-core-ui .themes-filter-bar .feature-filter-toggle { - margin: 0; - } } @media screen and (max-width: 600px) { diff --git a/src/wp-admin/css/customize-nav-menus.css b/src/wp-admin/css/customize-nav-menus.css index 46ac3066efd1d..cb3052e75f3b5 100644 --- a/src/wp-admin/css/customize-nav-menus.css +++ b/src/wp-admin/css/customize-nav-menus.css @@ -142,10 +142,12 @@ .menus-move-left:before { content: "\f341"; + content: "\f341" / ''; } .menus-move-right:before { content: "\f345"; + content: "\f345" / ''; } .reordering .menu-item .item-controls, @@ -178,6 +180,7 @@ .wp-customizer .menu-item.menu-item-edit-active .item-edit .toggle-indicator:before { content: "\f142"; + content: "\f142" / ''; } .wp-customizer .menu-item-settings p.description { @@ -265,19 +268,20 @@ #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:hover, #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:active, #customize-controls .customize-info.open.active-menu-screen-options .customize-help-toggle:focus { - color: #2271b1; + color: var(--wp-admin-theme-color); } .customize-screen-options-toggle:focus, #customize-controls .customize-info .customize-help-toggle:focus { /* Only visible in Windows High Contrast mode */ - outline: 1px solid transparent; + outline: 2px solid transparent; } .customize-screen-options-toggle:before { -moz-osx-font-smoothing: grayscale; border: none; content: "\f111"; + content: "\f111" / ''; display: block; font: 18px/1 dashicons; padding: 5px; @@ -321,9 +325,9 @@ .wp-customizer .menu-item .item-edit .toggle-indicator:before, #available-menu-items .accordion-section-title .toggle-indicator:before { content: "\f140"; + content: "\f140" / ''; display: block; padding: 1px 2px 1px 0; - speak: never; border-radius: 50%; color: #787c82; font: normal 20px/1 dashicons; @@ -468,6 +472,7 @@ .menu-item-bar .item-delete:before { content: "\f335"; + content: "\f335" / ''; position: absolute; top: 9px; left: 5px; @@ -542,8 +547,9 @@ background: transparent; } -#available-menu-items .accordion-section-title button { - display: block; +#available-menu-items .accordion-section-title button .toggle-indicator { + display: flex; + align-items: center; width: 28px; height: 35px; position: absolute; @@ -557,7 +563,7 @@ #available-menu-items .accordion-section-title .no-items, #available-menu-items .cannot-expand .accordion-section-title .spinner, -#available-menu-items .cannot-expand .accordion-section-title > button { +#available-menu-items .cannot-expand .accordion-section-title > button:not(#available-menu-items-search button.is-visible) { display: none; } @@ -581,7 +587,7 @@ } #available-menu-items .accordion-section-content .available-menu-items-list { - margin: 0 0 45px; + margin: 0 0 64px; padding: 1px 15px 15px; } @@ -652,6 +658,7 @@ #available-menu-items .item-add:before { content: "\f543"; + content: "\f543" / ''; position: relative; left: 2px; top: 3px; @@ -665,11 +672,12 @@ #available-menu-items .menu-item-handle.item-added .item-title, #available-menu-items .menu-item-handle.item-added:hover .item-add, #available-menu-items .menu-item-handle.item-added .item-add:focus { - color: #8c8f94; + color: #646970; } #available-menu-items .menu-item-handle.item-added .item-add:before { content: "\f147"; + content: "\f147" / ''; } #available-menu-items .accordion-section-title.loading .spinner, @@ -680,7 +688,7 @@ #available-menu-items-search .spinner { position: absolute; - top: 20px; /* 13 container padding +1 input margin +6 ( ( 32 input height - 20 spinner height ) / 2 ) */ + bottom: 24px; /* 13 container padding +1 input margin +10 ( ( 40 input height - 20 spinner height ) / 2 ) */ right: 21px; margin: 0 !important; } @@ -689,7 +697,7 @@ #available-menu-items #available-menu-items-search .accordion-section-content { position: absolute; left: 0; - top: 60px; /* below title div / search input */ + top: 83px; /* below title div / search input (75 + 8 for 40px input) */ bottom: 0; /* 100% height that still triggers lazy load */ max-height: none; width: 100%; @@ -719,8 +727,10 @@ opacity: 1; } -#customize-preview { - transition: all 0.2s; +@media (prefers-reduced-motion: no-preference) { + #customize-preview { + transition: all 0.2s; + } } body.adding-menu-items #available-menu-items { @@ -826,13 +836,13 @@ li.assigned-to-menu-location .add-new-menu-item { .menu-item-handle:hover { position: relative; z-index: 10; - color: #2271b1; + color: var(--wp-admin-theme-color); } .menu-item-handle:hover .item-type, .menu-item-handle:hover .item-edit, #available-menu-items .menu-item-handle:hover .item-add { - color: #2271b1; + color: var(--wp-admin-theme-color); } .menu-item-edit-active .menu-item-handle { @@ -850,6 +860,10 @@ li.assigned-to-menu-location .add-new-menu-item { } .customize-control-nav_menu .customize-control-nav_menu-buttons { + display: flex; + flex-direction: row-reverse; + align-items: center; + gap: 8px; margin-top: 12px; } @@ -864,20 +878,20 @@ li.assigned-to-menu-location .add-new-menu-item { .menu-delete:focus, .menu-item-bar .item-delete:focus:before, #available-menu-items .item-add:focus:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } @media screen and (max-width: 782px) { #available-menu-items #available-menu-items-search .accordion-section-content { - top: 63px; + top: 71px; /* 63 + 8 for 40px input */ } } @media screen and (max-width: 640px) { #available-menu-items #available-menu-items-search .accordion-section-content { - top: 130px; + top: 154px; /* 146 + 8 for 40px input */ } } diff --git a/src/wp-admin/css/customize-widgets.css b/src/wp-admin/css/customize-widgets.css index 3bd7a46b28e33..6e1bda06b13dc 100644 --- a/src/wp-admin/css/customize-widgets.css +++ b/src/wp-admin/css/customize-widgets.css @@ -131,6 +131,7 @@ .move-widget:before { content: "\f504"; + content: "\f504" / ''; } #customize-theme-controls .move-widget-area { @@ -171,6 +172,7 @@ #customize-theme-controls .widget-area-select li:before { display: none; content: "\f147"; + content: "\f147" / ''; position: absolute; top: 12px; left: 10px; @@ -185,7 +187,7 @@ #customize-theme-controls .widget-area-select .selected { color: #fff; - background: #2271b1; + background: var(--wp-admin-theme-color); } #customize-theme-controls .widget-area-select .selected:before { @@ -243,8 +245,8 @@ #available-widgets .widget-tpl.selected { background: #f6f7f7; border-bottom-color: #c3c4c7; - color: #2271b1; - border-left: 4px solid #2271b1; + color: var(--wp-admin-theme-color);; + border-left: 4px solid var(--wp-admin-theme-color); } #customize-controls .widget-title h3 { @@ -261,8 +263,10 @@ color: #646970; } -#customize-preview { - transition: all 0.2s; +@media (prefers-reduced-motion: no-preference) { + #customize-preview { + transition: all 0.2s; + } } body.adding-widget #available-widgets { @@ -291,6 +295,7 @@ body.adding-widget #customize-preview { #available-widgets .widget-title:before { content: "\f132"; + content: "\f132" / ''; position: absolute; top: -3px; right: 100%; @@ -306,73 +311,136 @@ body.adding-widget #customize-preview { } /* dashicons-smiley */ -#available-widgets [class*="easy"] .widget-title:before { content: "\f328"; top: -4px; } +#available-widgets [class*="easy"] .widget-title:before { + content: "\f328"; + content: "\f328" / ''; + top: -4px; +} /* dashicons-star-filled */ #available-widgets [class*="super"] .widget-title:before, -#available-widgets [class*="like"] .widget-title:before { content: "\f155"; top: -4px; } +#available-widgets [class*="like"] .widget-title:before { + content: "\f155"; + content: "\f155" / ''; + top: -4px; +} /* dashicons-wordpress */ -#available-widgets [class*="meta"] .widget-title:before { content: "\f120"; } +#available-widgets [class*="meta"] .widget-title:before { + content: "\f120"; + content: "\f120" / ''; +} /* dashicons-archive */ -#available-widgets [class*="archives"] .widget-title:before { content: "\f480"; top: -4px; } +#available-widgets [class*="archives"] .widget-title:before { + content: "\f480"; + content: "\f480" / ''; + top: -4px; +} /* dashicons-category */ -#available-widgets [class*="categor"] .widget-title:before { content: "\f318"; top: -4px; } +#available-widgets [class*="categor"] .widget-title:before { + content: "\f318"; + content: "\f318" / ''; + top: -4px; +} /* dashicons-admin-comments */ #available-widgets [class*="comment"] .widget-title:before, #available-widgets [class*="testimonial"] .widget-title:before, -#available-widgets [class*="chat"] .widget-title:before { content: "\f101"; } +#available-widgets [class*="chat"] .widget-title:before { + content: "\f101"; + content: "\f101" / ''; +} /* dashicons-admin-post */ -#available-widgets [class*="post"] .widget-title:before { content: "\f109"; } +#available-widgets [class*="post"] .widget-title:before { + content: "\f109"; + content: "\f109" / ''; +} /* dashicons-admin-page */ -#available-widgets [class*="page"] .widget-title:before { content: "\f105"; } +#available-widgets [class*="page"] .widget-title:before { + content: "\f105"; + content: "\f105" / ''; +} /* dashicons-text */ -#available-widgets [class*="text"] .widget-title:before { content: "\f478"; } +#available-widgets [class*="text"] .widget-title:before { + content: "\f478"; + content: "\f478" / ''; +} /* dashicons-admin-links */ -#available-widgets [class*="link"] .widget-title:before { content: "\f103"; } +#available-widgets [class*="link"] .widget-title:before { + content: "\f103"; + content: "\f103" / ''; +} /* dashicons-search */ -#available-widgets [class*="search"] .widget-title:before { content: "\f179"; } +#available-widgets [class*="search"] .widget-title:before { + content: "\f179"; + content: "\f179" / ''; +} /* dashicons-menu */ #available-widgets [class*="menu"] .widget-title:before, -#available-widgets [class*="nav"] .widget-title:before { content: "\f333"; } +#available-widgets [class*="nav"] .widget-title:before { + content: "\f333"; + content: "\f333" / ''; +} /* dashicons-tagcloud */ -#available-widgets [class*="tag"] .widget-title:before { content: "\f479"; } +#available-widgets [class*="tag"] .widget-title:before { + content: "\f479"; + content: "\f479" / ''; +} /* dashicons-rss */ -#available-widgets [class*="rss"] .widget-title:before { content: "\f303"; top: -6px; } +#available-widgets [class*="rss"] .widget-title:before { + content: "\f303"; + content: "\f303" / ''; + top: -6px; +} /* dashicons-calendar */ #available-widgets [class*="event"] .widget-title:before, -#available-widgets [class*="calendar"] .widget-title:before { content: "\f145"; top: -4px;} +#available-widgets [class*="calendar"] .widget-title:before { + content: "\f145"; + content: "\f145" / ''; + top: -4px; +} /* dashicons-format-image */ #available-widgets [class*="image"] .widget-title:before, #available-widgets [class*="photo"] .widget-title:before, #available-widgets [class*="slide"] .widget-title:before, -#available-widgets [class*="instagram"] .widget-title:before { content: "\f128"; } +#available-widgets [class*="instagram"] .widget-title:before { + content: "\f128"; + content: "\f128" / ''; +} /* dashicons-format-gallery */ #available-widgets [class*="album"] .widget-title:before, -#available-widgets [class*="galler"] .widget-title:before { content: "\f161"; } +#available-widgets [class*="galler"] .widget-title:before { + content: "\f161"; + content: "\f161" / ''; +} /* dashicons-format-video */ #available-widgets [class*="video"] .widget-title:before, -#available-widgets [class*="tube"] .widget-title:before { content: "\f126"; } +#available-widgets [class*="tube"] .widget-title:before { + content: "\f126"; + content: "\f126" / ''; +} /* dashicons-format-audio */ #available-widgets [class*="music"] .widget-title:before, #available-widgets [class*="radio"] .widget-title:before, -#available-widgets [class*="audio"] .widget-title:before { content: "\f127"; } +#available-widgets [class*="audio"] .widget-title:before { + content: "\f127"; + content: "\f127" / ''; +} /* dashicons-admin-users */ #available-widgets [class*="login"] .widget-title:before, @@ -381,55 +449,96 @@ body.adding-widget #customize-preview { #available-widgets [class*="avatar"] .widget-title:before, #available-widgets [class*="subscriber"] .widget-title:before, #available-widgets [class*="profile"] .widget-title:before, -#available-widgets [class*="grofile"] .widget-title:before { content: "\f110"; } +#available-widgets [class*="grofile"] .widget-title:before { + content: "\f110"; + content: "\f110" / ''; +} /* dashicons-cart */ #available-widgets [class*="commerce"] .widget-title:before, #available-widgets [class*="shop"] .widget-title:before, -#available-widgets [class*="cart"] .widget-title:before { content: "\f174"; top: -4px; } +#available-widgets [class*="cart"] .widget-title:before { + content: "\f174"; + content: "\f174" / ''; + top: -4px; +} /* dashicons-shield */ #available-widgets [class*="secur"] .widget-title:before, -#available-widgets [class*="firewall"] .widget-title:before { content: "\f332"; } +#available-widgets [class*="firewall"] .widget-title:before { + content: "\f332"; + content: "\f332" / ''; +} /* dashicons-chart-bar */ #available-widgets [class*="analytic"] .widget-title:before, #available-widgets [class*="stat"] .widget-title:before, -#available-widgets [class*="poll"] .widget-title:before { content: "\f185"; } +#available-widgets [class*="poll"] .widget-title:before { + content: "\f185"; + content: "\f185" / ''; +} /* dashicons-feedback */ -#available-widgets [class*="form"] .widget-title:before { content: "\f175"; } +#available-widgets [class*="form"] .widget-title:before { + content: "\f175"; + content: "\f175" / ''; +} /* dashicons-email-alt */ #available-widgets [class*="subscribe"] .widget-title:before, #available-widgets [class*="news"] .widget-title:before, #available-widgets [class*="contact"] .widget-title:before, -#available-widgets [class*="mail"] .widget-title:before { content: "\f466"; } +#available-widgets [class*="mail"] .widget-title:before { + content: "\f466"; + content: "\f466" / ''; +} /* dashicons-share */ #available-widgets [class*="share"] .widget-title:before, -#available-widgets [class*="socia"] .widget-title:before { content: "\f237"; } +#available-widgets [class*="socia"] .widget-title:before { + content: "\f237"; + content: "\f237" / ''; +} /* dashicons-translation */ #available-widgets [class*="lang"] .widget-title:before, -#available-widgets [class*="translat"] .widget-title:before { content: "\f326"; } +#available-widgets [class*="translat"] .widget-title:before { + content: "\f326"; + content: "\f326" / ''; +} /* dashicons-location-alt */ #available-widgets [class*="locat"] .widget-title:before, -#available-widgets [class*="map"] .widget-title:before { content: "\f231"; } +#available-widgets [class*="map"] .widget-title:before { + content: "\f231"; + content: "\f231" / ''; +} /* dashicons-download */ -#available-widgets [class*="download"] .widget-title:before { content: "\f316"; } +#available-widgets [class*="download"] .widget-title:before { + content: "\f316"; + content: "\f316" / ''; +} /* dashicons-cloud */ -#available-widgets [class*="weather"] .widget-title:before { content: "\f176"; top: -4px;} +#available-widgets [class*="weather"] .widget-title:before { + content: "\f176"; + content: "\f176" / ''; + top: -4px; +} /* dashicons-facebook */ -#available-widgets [class*="facebook"] .widget-title:before { content: "\f304"; } +#available-widgets [class*="facebook"] .widget-title:before { + content: "\f304"; + content: "\f304" / ''; +} /* dashicons-twitter */ #available-widgets [class*="tweet"] .widget-title:before, -#available-widgets [class*="twitter"] .widget-title:before { content: "\f301"; } +#available-widgets [class*="twitter"] .widget-title:before { + content: "\f301"; + content: "\f301" / ''; +} @media screen and (max-height: 700px) and (min-width: 981px) { /* Compact widget-tops on smaller laptops, but not tablets. See ticket #27112#comment:4 */ diff --git a/src/wp-admin/css/dashboard.css b/src/wp-admin/css/dashboard.css index 1f42793ac1dba..ab73f828f7067 100644 --- a/src/wp-admin/css/dashboard.css +++ b/src/wp-admin/css/dashboard.css @@ -43,6 +43,14 @@ margin: 0 -8px; } +#dashboard-widgets .postbox { + border-radius: 8px; +} + +#dashboard-widgets .postbox-header .hndle { + padding: 12px 16px; +} + #dashboard-widgets .postbox .inside { margin-bottom: 0; } @@ -50,20 +58,31 @@ #dashboard-widgets .meta-box-sortables { display: flow-root; /* avoid margin collapsing between parent and first/last child elements */ /* Required min-height to make the jQuery UI Sortable drop zone work. */ - min-height: 100px; + min-height: 0; margin: 0 8px 20px; } +#dashboard-widgets .meta-box-sortables:not(:empty) { + margin-bottom: 16px; +} + #dashboard-widgets .postbox-container .empty-container { - outline: 3px dashed #c3c4c7; + outline: 2px dashed rgb(0, 0, 0, 0.15); + outline-offset: -2px; + border-radius: 8px; height: 250px; + margin: 4px; } -/* Only highlight drop zones when dragging and only in the 2 columns layout. */ +/* Only highlight drop zones when dragging. */ .is-dragging-metaboxes #dashboard-widgets .meta-box-sortables { - outline: 3px dashed #646970; - /* Prevent margin on the child from collapsing with margin on the parent. */ - display: flow-root; + border-radius: 8px; + background: rgb(var(--wp-admin-theme-color--rgb), 0.04); + min-height: 100px; +} + +.is-dragging-metaboxes #dashboard-widgets .postbox-container .empty-container { + background: rgb(0, 0, 0, 0.01); } #dashboard-widgets .postbox-container .empty-container:after { @@ -110,65 +129,124 @@ max-width: 100%; } +/* Screen meta exception for when the "Dashboard" heading is missing or located below the Welcome Panel. */ +.index-php #screen-meta-links { + margin: 0 20px 8px 0; +} + /* Welcome Panel */ .welcome-panel { position: relative; overflow: auto; margin: 16px 0; - padding: 23px 10px 0; - border: 1px solid #c3c4c7; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - background: #fff; - font-size: 13px; - line-height: 1.7; + border-radius: 8px; + font-size: 14px; + line-height: 1.3; + clear: both; } .welcome-panel h2 { margin: 0; - font-size: 21px; - font-weight: 400; - line-height: 1.2; + font-size: 48px; + font-weight: 600; + line-height: 1.25; } .welcome-panel h3 { - margin: 1.33em 0 0; - font-size: 16px; + margin: 0; + font-size: 20px; + font-weight: 400; + line-height: 1.4; } -.welcome-panel li { - font-size: 14px; +.welcome-panel p { + font-size: inherit; + line-height: inherit; } -.welcome-panel p { - color: #646970; +.welcome-panel-header { + position: relative; + color: #fff; } -.welcome-panel li a { +.welcome-panel-header-image { + position: absolute !important; + top: 0; + right: 0; + bottom: 0; + left: 0; + z-index: 0 !important; + overflow: hidden; +} + +.welcome-panel-header-image svg { + display: block; + margin: auto; + width: 100%; + height: 100%; +} + +.rtl .welcome-panel-header-image svg { + transform: scaleX(-1); +} + +.welcome-panel-header * { + color: inherit; + position: relative; + z-index: 1; +} + +.welcome-panel-header a:focus, +.welcome-panel-header a:hover { + color: inherit; text-decoration: none; } -.welcome-panel .about-description { - font-size: 16px; - margin: 0; +.welcome-panel-header a:focus, +.welcome-panel .welcome-panel-close:focus { + outline-color: currentColor; + outline-offset: 1px; + box-shadow: none; +} + +.welcome-panel-header p { + margin: 0.5em 0 0; + font-size: 20px; + line-height: 1.4; } .welcome-panel .welcome-panel-close { + display: flex; + align-items: center; position: absolute; top: 10px; right: 10px; - padding: 10px 15px 10px 21px; + padding: 10px 15px; font-size: 13px; line-height: 1.23076923; /* Chrome rounding, needs to be 16px equivalent */ text-decoration: none; + z-index: 1; /* Raise above the version image. */ } .welcome-panel .welcome-panel-close:before { - position: absolute; - top: 8px; - left: 0; transition: all .1s ease-in-out; + content: '\f335'; + font-size: 24px; + color: #fff; } +.welcome-panel .welcome-panel-close { + color: #fff; +} + +.welcome-panel .welcome-panel-close:hover, +.welcome-panel .welcome-panel-close:focus, +.welcome-panel .welcome-panel-close:hover::before, +.welcome-panel .welcome-panel-close:focus::before { + color: #fff972; +} + +/* @deprecated 5.9.0 -- Button removed from panel. */ .wp-core-ui .welcome-panel .button.button-hero { margin: 15px 13px 3px 0; padding: 12px 36px; @@ -178,61 +256,113 @@ } .welcome-panel-content { - margin-left: 13px; + min-height: 400px; + display: flex; + flex-direction: column; + justify-content: space-between; +} + +.welcome-panel-header-wrap { + background-color: #151515; +} + +.welcome-panel-header { + box-sizing: border-box; + margin-left: auto; + margin-right: auto; max-width: 1500px; + width: 100%; + padding: 48px 0 80px 48px; } .welcome-panel .welcome-panel-column-container { + box-sizing: border-box; + width: 100%; clear: both; - position: relative; + display: grid; + z-index: 1; + padding: 24px; + grid-template-columns: repeat(3, 1fr); + gap: 32px; + align-self: flex-end; + background: #ffffff; + border: 1px solid #c3c4c7; + border-top: 0; + border-radius: 0 0 8px 8px; } -.welcome-panel .welcome-panel-column { - width: 32%; - min-width: 200px; - float: left; +[class*="welcome-panel-icon"] { + height: 60px; + width: 60px; + background-position: center; + background-size: 24px 24px; + background-repeat: no-repeat; + border-radius: 100%; +} + +.welcome-panel-column > svg { + margin-top: 4px; +} + +.welcome-panel-column { + display: grid; + grid-template-columns: min-content 1fr; + gap: 24px; } -.welcome-panel .welcome-panel-column:first-child { - width: 36%; +.welcome-panel-icon-pages { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M7 13.8h6v-1.5H7v1.5zM18 16V4c0-1.1-.9-2-2-2H6c-1.1 0-2 .9-2 2v12c0 1.1.9 2 2 2h10c1.1 0 2-.9 2-2zM5.5 16V4c0-.3.2-.5.5-.5h10c.3 0 .5.2.5.5v12c0 .3-.2.5-.5.5H6c-.3 0-.5-.2-.5-.5zM7 10.5h8V9H7v1.5zm0-3.3h8V5.8H7v1.4zM20.2 6v13c0 .7-.6 1.2-1.2 1.2H8v1.5h11c1.5 0 2.7-1.2 2.7-2.8V6h-1.5z' /%3E%3C/svg%3E"); } -.welcome-panel-column p.hide-if-no-customize { - margin-top: 10px; +.welcome-panel-icon-layout { + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M18 5.5H6a.5.5 0 00-.5.5v3h13V6a.5.5 0 00-.5-.5zm.5 5H10v8h8a.5.5 0 00.5-.5v-7.5zm-10 0h-3V18a.5.5 0 00.5.5h2.5v-8zM6 4h12a2 2 0 012 2v12a2 2 0 01-2 2H6a2 2 0 01-2-2V6a2 2 0 012-2z' /%3E%3C/svg%3E"); } -.welcome-panel-column p { - margin-top: 7px; - color: #3c434a; +.welcome-panel-icon-styles { + background-image: url("data:image/svg+xml,%3Csvg viewBox='0 0 24 24' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath fill='%23fff' d='M12 4c-4.4 0-8 3.6-8 8v.1c0 4.1 3.2 7.5 7.2 7.9h.8c4.4 0 8-3.6 8-8s-3.6-8-8-8zm0 15V5c3.9 0 7 3.1 7 7s-3.1 7-7 7z' /%3E%3C/svg%3E"); } +/* @deprecated 5.9.0 -- Section removed from welcome panel. */ .welcome-panel .welcome-widgets-menus { line-height: 1.14285714; } +/* @deprecated 5.9.0 -- Lists removed from welcome panel. */ .welcome-panel .welcome-panel-column ul { margin: 0.8em 1em 1em 0; } +/* @deprecated 5.9.0 -- Lists removed from welcome panel. */ +.welcome-panel li { + font-size: 14px; +} + +/* @deprecated 5.9.0 -- Lists removed from welcome panel. */ +.welcome-panel li a { + text-decoration: none; +} + +/* @deprecated 5.9.0 -- Lists removed from welcome panel. */ .welcome-panel .welcome-panel-column li { line-height: 1.14285714; list-style-type: none; padding: 0 0 8px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-icon { background: transparent !important; } /* Welcome Panel and Right Now common Icons style */ +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-icon:before, #dashboard_right_now li a:before, #dashboard_right_now li span:before, #dashboard_right_now .search-engines-info:before { color: #646970; font: normal 20px/1 dashicons; - speak: never; display: inline-block; padding: 0 10px 0 0; position: relative; @@ -244,49 +374,67 @@ /* Welcome Panel specific Icons styles */ +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-write-blog:before, .welcome-panel .welcome-edit-page:before { content: "\f119"; + content: "\f119" / ''; top: -3px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-add-page:before { content: "\f132"; + content: "\f132" / ''; top: -1px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-setup-home:before { content: "\f102"; + content: "\f102" / ''; top: -1px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-view-site:before { content: "\f115"; + content: "\f115" / ''; top: -2px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-widgets-menus:before { content: "\f116"; + content: "\f116" / ''; top: -2px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-widgets:before { content: "\f538"; + content: "\f538" / ''; top: -2px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-menus:before { content: "\f163"; + content: "\f163" / ''; top: -2px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-comments:before { content: "\f117"; + content: "\f117" / ''; top: -1px; } +/* @deprecated 5.9.0 -- Icons removed from welcome panel. */ .welcome-panel .welcome-learn-more:before { content: "\f118"; + content: "\f118" / ''; top: -1px; } @@ -295,38 +443,48 @@ #dashboard_right_now .search-engines-info:before, #dashboard_right_now li a:before, #dashboard_right_now li > span:before { /* get only the first level span to exclude screen-reader-text in mu-storage */ - content: "\f159"; /* generic icon for items added by CPTs ? */ padding: 0 5px 0 0; + /* generic icon for items added by CPTs ? */ + content: "\f159"; + content: "\f159" / ''; } #dashboard_right_now .page-count a:before, #dashboard_right_now .page-count span:before { content: "\f105"; + content: "\f105" / ''; } #dashboard_right_now .post-count a:before, #dashboard_right_now .post-count span:before { content: "\f109"; + content: "\f109" / ''; } #dashboard_right_now .comment-count a:before { content: "\f101"; + content: "\f101" / ''; } #dashboard_right_now .comment-mod-count a:before { content: "\f125"; + content: "\f125" / ''; } #dashboard_right_now .storage-count a:before { content: "\f104"; + content: "\f104" / ''; } #dashboard_right_now .storage-count.warning a:before { content: "\f153"; + content: "\f153" / ''; } #dashboard_right_now .search-engines-info:before { content: "\f348"; + content: "\f348" / ''; + color: #d63638; } /* Dashboard WordPress events */ @@ -345,9 +503,7 @@ } .community-events .spinner { - float: none; - margin: 5px 2px 0; - vertical-align: top; + margin: 0 2px 0; } .community-events-errors[aria-hidden="true"], @@ -366,17 +522,27 @@ .community-events-form { margin: 15px 0 5px; + display: flex; + gap: 5px; + align-items: center; + flex-wrap: wrap; } .community-events-form .regular-text { width: 40%; - height: 29px; margin: 0; - vertical-align: top; + min-height: 32px; + padding: 0 8px; +} + +#dashboard-widgets .community-events-form .button { + min-height: 32px; + line-height: 2.30769231; + padding: 0 12px; } .community-events li.event-none { - border-left: 4px solid #72aee6; + border-left: 4px solid var(--wp-admin-theme-color, #3858e9); } #dashboard-widgets .community-events li.event-none a { @@ -384,10 +550,7 @@ } .community-events-form label { - display: inline-block; - vertical-align: top; - line-height: 2.15384615; - height: 28px; + line-height: 2.14285714; } .community-events .activity-block > p { @@ -399,22 +562,13 @@ vertical-align: middle; } -#community-events-submit { - margin-left: 3px; - margin-right: 3px; -} - /* Needs higher specificity than #dashboard-widgets .button-link */ #dashboard-widgets .community-events-cancel.button-link { - vertical-align: top; - /* Same properties as the submit button for cross-browsers alignment. */ - line-height: 2; - height: 28px; text-decoration: underline; } .community-events ul { - background-color: #f6f7f7; + background-color: rgba(var(--wp-admin-theme-color--rgb),.08); padding-left: 0; padding-right: 0; padding-bottom: 0; @@ -426,15 +580,15 @@ color: #2c3338; } .community-events li:first-child { - border-top: 1px solid #f0f0f1; + border-top: 1px solid #e9e9ed; } .community-events li ~ li { - border-top: 1px solid #f0f0f1; + border-top: 1px solid #e9e9ed; } .community-events .activity-block.last { - border-bottom: 1px solid #f0f0f1; + border-bottom: 1px solid #e9e9ed; padding-top: 0; margin-top: -1px; } @@ -443,6 +597,11 @@ display: block; } +.community-events .ce-separator::before { + content: "\2022"; + content: "\2022" / ''; +} + .event-icon { height: 18px; padding-right: 10px; @@ -456,9 +615,11 @@ } .event-meetup .event-icon:before { content: "\f484"; + content: "\f484" / ''; } .event-wordcamp .event-icon:before { content: "\f486"; + content: "\f486" / ''; } .community-events .event-title { @@ -476,7 +637,7 @@ margin-bottom: 0; padding: 12px; border-top: 1px solid #f0f0f1; - color: #dcdcde; + color: #646970; } /* Safari 10 + VoiceOver specific: without this, the hidden text gets read out before the link. */ @@ -559,14 +720,6 @@ body #dashboard-widgets .postbox form .submit { margin-bottom: 10px; } -#dashboard_right_now .inside { - padding: 0; -} - -#dashboard_right_now .main { - padding: 0 12px 11px; -} - #dashboard_right_now .main p { margin: 0; } @@ -593,6 +746,11 @@ body #dashboard-widgets .postbox form .submit { font-weight: 400; } +#network_dashboard_right_now p input { + margin: 2px 1px; + vertical-align: middle; +} + /* Dashboard right now - Colors */ #dashboard_right_now .sub { @@ -662,7 +820,7 @@ body #dashboard-widgets .postbox form .submit { min-height: 90px; max-height: 1300px; margin: 0 0 8px; - padding: 6px 7px; + padding: 8px 12px; resize: none; } @@ -736,6 +894,7 @@ body #dashboard-widgets .postbox form .submit { #dashboard_activity .comment-meta span.approve:before { content: "\f227"; + content: "\f227" / ''; font: 20px/.5 dashicons; margin-left: 5px; vertical-align: middle; @@ -746,7 +905,7 @@ body #dashboard-widgets .postbox form .submit { #dashboard_activity .inside { margin: 0; - padding-bottom: 0; + padding: 0 12px; } #dashboard_activity .no-activity { @@ -774,35 +933,36 @@ body #dashboard-widgets .postbox form .submit { #future-posts ul, #published-posts ul { - clear: both; - margin-bottom: 0; + margin: 8px -12px 0 -12px; } #future-posts li, #published-posts li { - margin-bottom: 8px; + display: grid; + grid-template-columns: clamp(160px, calc(2vw + 140px), 200px) auto; + column-gap: 10px; + color: #646970; + padding: 4px 12px; } -#future-posts ul span, -#published-posts ul span { - display: inline-block; - margin-right: 5px; - min-width: 150px; - color: #646970; +#future-posts li:nth-child(odd), +#published-posts li:nth-child(odd) { + background-color: #f6f7f7; } .activity-block { border-bottom: 1px solid #f0f0f1; - margin: 0 -12px; + margin: 0 -12px 6px -12px; padding: 8px 12px 4px; } .activity-block:last-child { border-bottom: none; + margin-bottom: 0; } .activity-block .subsubsub li { - color: #dcdcde; + color: #646970; } /* Dashboard activity widget - Comments */ @@ -1009,6 +1169,7 @@ a.rsswidget { .rss-widget cite:before { content: "\2014"; + content: "\2014" / ''; } .dashboard-comment-wrap { @@ -1079,7 +1240,8 @@ a.rsswidget { padding-right: 6px; } -#dashboard_php_nag.php-insecure .dashicons-warning { +#dashboard_php_nag.php-no-security-updates .dashicons-warning, +#dashboard_php_nag.php-version-lower-than-future-minimum .dashicons-warning { color: #d63638; } @@ -1091,17 +1253,26 @@ a.rsswidget { margin: 12px 0; } -#dashboard_php_nag h3 { +.bigger-bolder-text { font-weight: 600; -} - -#dashboard_php_nag .button .dashicons-external { - line-height: 25px; + font-size: 14px; } /* =Media Queries -------------------------------------------------------------- */ +@media only screen and (min-width: 1600px) { + .welcome-panel .welcome-panel-column-container { + display: flex; + justify-content: center; + } + + .welcome-panel-column { + width: 100%; + max-width: 460px; + } +} + /* one column on the dash */ @media only screen and (max-width: 799px) { #wpbody-content #dashboard-widgets .postbox-container { @@ -1136,10 +1307,11 @@ a.rsswidget { #dashboard-widgets #postbox-container-3 .empty-container, #dashboard-widgets #postbox-container-4 .empty-container { - outline: none; + border: none; height: 0; min-height: 0; margin-bottom: 0; + display: none; } #dashboard-widgets #postbox-container-3 .empty-container:after, @@ -1152,7 +1324,7 @@ a.rsswidget { } #wpbody #dashboard-widgets .metabox-holder.columns-1 .postbox-container .empty-container { - outline: none; + border: none; height: 0; min-height: 0; margin-bottom: 0; @@ -1190,10 +1362,11 @@ a.rsswidget { } #dashboard-widgets #postbox-container-4 .empty-container { - outline: none; + border: none; height: 0; min-height: 0; margin-bottom: 0; + display: none; } #dashboard-widgets #postbox-container-4 .empty-container:after { @@ -1213,25 +1386,60 @@ a.rsswidget { } @media screen and (max-width: 870px) { - .welcome-panel .welcome-panel-column, - .welcome-panel .welcome-panel-column:first-child { - display: block; - float: none; - width: 100%; - } - + /* @deprecated 5.9.0 -- Lists removed from welcome panel. */ .welcome-panel .welcome-panel-column li { display: inline-block; margin-right: 13px; } + /* @deprecated 5.9.0 -- Lists removed from welcome panel. */ .welcome-panel .welcome-panel-column ul { margin: 0.4em 0 0; } } +@media screen and (max-width: 1180px) and (min-width: 783px) { + .welcome-panel-column { + grid-template-columns: 1fr; + } + + [class*="welcome-panel-icon"], + .welcome-panel-column > svg { + display: none; + } +} + @media screen and (max-width: 782px) { + .welcome-panel .welcome-panel-column-container { + grid-template-columns: 1fr; + box-sizing: border-box; + padding: 32px; + width: 100%; + } + + .welcome-panel .welcome-panel-column-content { + max-width: 520px; + } + + /* Keep the close icon from overlapping the Welcome text. */ + .welcome-panel .welcome-panel-close { + overflow: hidden; + text-indent: 40px; + white-space: nowrap; + width: 20px; + height: 20px; + padding: 5px; + top: 5px; + right: 5px; + } + + .welcome-panel .welcome-panel-close::before { + position: absolute; + top: 5px; + left: -35px; + } + #dashboard-widgets h2 { padding: 12px; } @@ -1247,14 +1455,8 @@ a.rsswidget { vertical-align: baseline; } - .community-events-form .regular-text { - height: 32px; - } - #community-events-submit { margin-bottom: 0; - /* Override .wp-core-ui .button */ - vertical-align: top; } .community-events-form label, @@ -1262,35 +1464,36 @@ a.rsswidget { /* Same properties as the submit button for cross-browsers alignment. */ font-size: 14px; line-height: normal; - height: auto; padding: 6px 0; border: 1px solid transparent; } - - .community-events .spinner { - margin-top: 7px; - } } /* Smartphone */ @media screen and (max-width: 600px) { - /* Keep the close icon from overlapping the Welcome text. */ - .welcome-panel .welcome-panel-close { - overflow: hidden; - text-indent: 40px; - white-space: nowrap; - width: 20px; - height: 20px; - padding: 5px; - top: 5px; - right: 5px; + .welcome-panel-header { + padding: 32px 32px 64px; } - /* Make the close icon larger for tappability. */ - .welcome-panel .welcome-panel-close:before { - font-size: 20px; - top: 5px; - left: -35px; + .welcome-panel-header-image { + display: none; + } +} + +@media screen and (max-width: 480px) { + .welcome-panel-column { + gap: 16px; + } +} + +@media screen and (max-width: 360px) { + .welcome-panel-column { + grid-template-columns: 1fr; + } + + [class*="welcome-panel-icon"], + .welcome-panel-column > svg { + display: none; } } diff --git a/src/wp-admin/css/deprecated-media.css b/src/wp-admin/css/deprecated-media.css index 359fc59e3c3b5..36fafeb65f76b 100644 --- a/src/wp-admin/css/deprecated-media.css +++ b/src/wp-admin/css/deprecated-media.css @@ -404,7 +404,6 @@ table.not-image tr.image-only { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { .image-align-none-label { diff --git a/src/wp-admin/css/edit.css b/src/wp-admin/css/edit.css index 44a444b7140c6..f1dd76ac31474 100644 --- a/src/wp-admin/css/edit.css +++ b/src/wp-admin/css/edit.css @@ -75,6 +75,20 @@ pointer-events: none; } +#titlewrap .skiplink { + background: #fff; + line-height: 2.30769231; /* 30px for 32px min-height */ + min-height: 32px; + right: 4px; +} + +#titlewrap .skiplink:focus { + clip: inherit; + clip-path: inherit; + top: 4px; + width: auto; +} + input#link_description, input#link_url { width: 100%; @@ -97,10 +111,15 @@ input#link_url { color: #646970; } +#sample-permalink { + display: inline-block; + max-width: 100%; + word-wrap: break-word; +} + #edit-slug-box .cancel { margin-right: 10px; padding: 0; - font-size: 11px; } #comment-link-box { @@ -119,7 +138,7 @@ input#link_url { #editable-post-name input { font-size: 13px; font-weight: 400; - height: 24px; + min-height: 32px; margin: 0; width: 16em; } @@ -146,13 +165,17 @@ body.post-new-php .submitbox .submitdelete { margin-top: 3px; } +body.post-type-wp_navigation div#minor-publishing, +body.post-type-wp_navigation .inline-edit-status { + display: none; +} + /* Post Screen */ /* Only highlight drop zones when dragging and only in the 2 columns layout. */ .is-dragging-metaboxes .metabox-holder .postbox-container .meta-box-sortables { - outline: 3px dashed #646970; - /* Prevent margin on the child from collapsing with margin on the parent. */ - display: flow-root; + border-radius: 8px; + background: rgb(var(--wp-admin-theme-color--rgb), 0.04); /* * This min-height is meant to limit jumpiness while dragging. It's equivalent * to the minimum height of the sortable-placeholder which is given by the height @@ -289,7 +312,6 @@ ul.wp-tab-bar li { #postimagediv .inside img { max-width: 100%; height: auto; - width: auto; vertical-align: top; background-image: linear-gradient(45deg, #c3c4c7 25%, transparent 25%, transparent 75%, #c3c4c7 75%, #c3c4c7), linear-gradient(45deg, #c3c4c7 25%, transparent 25%, transparent 75%, #c3c4c7 75%, #c3c4c7); background-position: 0 0, 10px 10px; @@ -307,8 +329,9 @@ form#tags-filter { } #post-body .tagsdiv #newtag { - margin-right: 5px; - width: 16em; + margin-right: 0; + flex: 1; + min-width: 0; } #side-sortables input#post_password { @@ -316,7 +339,8 @@ form#tags-filter { } #side-sortables .tagsdiv #newtag { - width: 68%; + flex: 1; + min-width: 0; } #post-status-info { @@ -475,7 +499,6 @@ form#tags-filter { #post-body .misc-pub-response-to:before, #post-body .misc-pub-comment-status:before { font: normal 20px/1 dashicons; - speak: never; display: inline-block; margin-left: -1px; padding-right: 3px; @@ -487,36 +510,43 @@ form#tags-filter { #post-body .misc-pub-post-status:before, #post-body .misc-pub-comment-status:before { content: "\f173"; + content: "\f173" / ''; } #post-body #visibility:before { content: "\f177"; + content: "\f177" / ''; } .curtime #timestamp:before { content: "\f145"; + content: "\f145" / ''; position: relative; top: -1px; } #post-body .misc-pub-uploadedby:before { content: "\f110"; + content: "\f110" / ''; position: relative; top: -1px; } #post-body .misc-pub-uploadedto:before { content: "\f318"; + content: "\f318" / ''; position: relative; top: -1px; } #post-body .misc-pub-revisions:before { content: "\f321"; + content: "\f321" / ''; } #post-body .misc-pub-response-to:before { content: "\f101"; + content: "\f101" / ''; } #timestampdiv { @@ -714,6 +744,17 @@ form#tags-filter { padding-left: 0; } +/* Better position for the WordPress admin notices. */ +.privacy-settings .notice, +.site-health .notice { + margin: 25px 20px 15px 22px; +} + +.privacy-settings .notice ~ .notice, +.site-health .notice ~ .notice { + margin-top: 5px; +} + /* Emulates .wrap h1 styling */ .privacy-settings-header h1, .health-check-header h1 { @@ -781,7 +822,7 @@ form#tags-filter { .privacy-settings-tab.active, .health-check-tab.active { - box-shadow: inset 0 -3px #3582c4; + box-shadow: inset 0 -3px var(--wp-admin-theme-color); font-weight: 600; } @@ -855,7 +896,7 @@ form#tags-filter { border: none; box-shadow: none; outline-offset: -1px; - outline: 2px solid #2271b1; + outline: 2px solid var(--wp-admin-theme-color); background-color: #f6f7f7; } @@ -894,7 +935,7 @@ form#tags-filter { .privacy-settings-accordion-trigger .badge.blue, .health-check-accordion-trigger .badge.blue { - border: 1px solid #72aee6; + border: 1px solid var(--wp-admin-theme-color); } .privacy-settings-accordion-trigger .badge.orange, @@ -947,15 +988,16 @@ form#tags-filter { } .privacy-settings-accordion-actions { - text-align: right; - display: block; + justify-content: right; + display: flex; + align-items: center; + flex-wrap: wrap; + gap: 1em; } .privacy-settings-accordion-actions .success { display: none; - color: #008a20; - padding-right: 1em; - padding-top: 6px; + color: #007017; } .privacy-settings-accordion-actions .success.visible { @@ -1016,6 +1058,14 @@ form#tags-filter { white-space: normal; line-height: 1.8; } + + #edit-slug-box { + padding: 0; + } + + #editable-post-name input { + min-height: 40px; + } } @media only screen and (max-width: 1004px) { @@ -1046,6 +1096,10 @@ form#tags-filter { padding: 0 8px 8px; } +#postcustom #postcustomstuff .add-custom-field { + padding: 12px 8px 8px; +} + #side-sortables #postcustom #postcustomstuff .submit { margin: 0; padding: 0; @@ -1096,7 +1150,8 @@ form#tags-filter { width: auto; } -#postcustomstuff #newmetaleft a { +#postcustomstuff #newmetaleft a, +#postcustomstuff #newmeta-button { display: inline-block; margin: 0 8px 8px; text-decoration: none; @@ -1161,7 +1216,6 @@ form#tags-filter { margin-right: 7px; color: #dcdcde; font: normal 20px/1 dashicons; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -1189,42 +1243,52 @@ label.post-format-icon { .post-format-icon.post-format-standard::before { content: "\f109"; + content: "\f109" / ''; } .post-format-icon.post-format-image::before { content: "\f128"; + content: "\f128" / ''; } .post-format-icon.post-format-gallery::before { content: "\f161"; + content: "\f161" / ''; } .post-format-icon.post-format-audio::before { content: "\f127"; + content: "\f127" / ''; } .post-format-icon.post-format-video::before { content: "\f126"; + content: "\f126" / ''; } .post-format-icon.post-format-chat::before { content: "\f125"; + content: "\f125" / ''; } .post-format-icon.post-format-status::before { content: "\f130"; + content: "\f130" / ''; } .post-format-icon.post-format-aside::before { content: "\f123"; + content: "\f123" / ''; } .post-format-icon.post-format-quote::before { content: "\f122"; + content: "\f122" / ''; } .post-format-icon.post-format-link::before { content: "\f103"; + content: "\f103" / ''; } /*------------------------------------------------------------------------------ @@ -1266,10 +1330,11 @@ div.tabs-panel-inactive { } div.tabs-panel-active:focus { - box-shadow: inset 0 0 0 1px #4f94d4, inset 0 0 2px 1px rgba(79, 148, 212, 0.8); - outline: 0 none; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } - +.options-discussion-php .indent-children ul, #front-page-warning, #front-static-pages ul, ul.export-filters, @@ -1336,6 +1401,11 @@ p.description code { font-style: normal; } +p.description code, +.form-wrap p code { + color: #50575e; +} + .form-wrap .form-field { margin: 1em 0; padding: 0; @@ -1360,6 +1430,9 @@ p.description code { #poststuff .tagsdiv .ajaxtag { margin-top: 1em; + display: flex; + gap: 8px; + align-items: center; } #poststuff .tagsdiv .howto { @@ -1371,7 +1444,8 @@ p.description code { } .tagsdiv .newtag { - width: 180px; + flex: 1; + min-width: 0; } .tagsdiv .the-tags { @@ -1467,6 +1541,9 @@ p.popular-tags a { } .edit-tag-actions { + display: flex; + align-items: center; + gap: 8px; margin-top: 20px; } @@ -1661,7 +1738,6 @@ table.links-table { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { #content-resize-handle, #post-body .wp_themeSkin .mceStatusbar a.mceResize { @@ -1708,7 +1784,8 @@ table.links-table { } .is-dragging-metaboxes.post-type-attachment #post-body .meta-box-sortables { - outline: none; + border: none; + background: transparent; min-height: 0; margin-bottom: 0; } @@ -1865,9 +1942,10 @@ table.links-table { /* Tags Metabox */ .tagsdiv .newtag { - width: 100%; + flex: 1; + min-width: 0; height: auto; - margin-bottom: 15px; + margin-bottom: 0; } .tagchecklist { @@ -1955,12 +2033,7 @@ table.links-table { } .misc-pub-section { - padding: 20px 10px; - } - - .misc-pub-section > a { - float: right; - font-size: 16px; + padding: 12px 10px; } #delete-action, diff --git a/src/wp-admin/css/forms.css b/src/wp-admin/css/forms.css index 7893915d6d9aa..b48825a1ef5a3 100644 --- a/src/wp-admin/css/forms.css +++ b/src/wp-admin/css/forms.css @@ -16,23 +16,19 @@ input { textarea { overflow: auto; - padding: 2px 6px; + padding: 8px 12px; /* inherits font size 14px */ line-height: 1.42857143; /* 20px */ resize: vertical; } -label { - cursor: pointer; -} - input, select { margin: 0 1px; } textarea.code { - padding: 4px 6px 1px; + padding: 8px 12px; } input[type="text"], @@ -52,10 +48,10 @@ input[type="week"], select, textarea { box-shadow: 0 0 0 transparent; - border-radius: 4px; - border: 1px solid #8c8f94; + border-radius: 2px; + border: 1px solid #949494; background-color: #fff; - color: #2c3338; + color: #1e1e1e; } input[type="text"], @@ -71,11 +67,16 @@ input[type="tel"], input[type="time"], input[type="url"], input[type="week"] { - padding: 0 8px; + padding: 0 12px; + /* inherits font size 14px */ + min-height: 40px; +} + +select { + padding: 0 24px 0 12px; /* inherits font size 14px */ - line-height: 2; /* 28px */ - /* Only necessary for IE11 */ - min-height: 30px; + line-height: 2.71428571; /* 38px for 40px min-height */ + min-height: 40px; } ::-webkit-datetime-edit { @@ -97,17 +98,26 @@ input[type="tel"]:focus, input[type="time"]:focus, input[type="url"]:focus, input[type="week"]:focus, -input[type="checkbox"]:focus, -input[type="radio"]:focus, select:focus, textarea:focus { - border-color: #2271b1; - box-shadow: 0 0 0 1px #2271b1; + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + +/* Checkbox and radio use outset focus style */ +input[type="checkbox"]:focus, +input[type="radio"]:focus { + border-color: #1e1e1e; + box-shadow: 0 0 0 2px #fff, 0 0 0 4px var(--wp-admin-theme-color); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } /* rtl:ignore */ +.ltr, +input[type="password"], input[type="email"], input[type="url"] { direction: ltr; @@ -115,10 +125,10 @@ input[type="url"] { input[type="checkbox"], input[type="radio"] { - border: 1px solid #8c8f94; - border-radius: 4px; + border: 1px solid #1e1e1e; + border-radius: 2px; background: #fff; - color: #50575e; + color: #1e1e1e; clear: none; cursor: pointer; display: inline-block; @@ -132,12 +142,12 @@ input[type="radio"] { width: 1rem; min-width: 1rem; -webkit-appearance: none; - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.1); + box-shadow: none; transition: .05s border-color ease-in-out; } input[type="radio"]:checked + label:before { - color: #8c8f94; + color: #949494; } .wp-core-ui input[type="reset"]:hover, @@ -160,10 +170,11 @@ td > input[type="checkbox"], } input[type="radio"] { + display: inline-flex; border-radius: 50%; margin-right: 0.25rem; - /* 10px not sure if still necessary, comes from the MP6 redesign in r26072 */ - line-height: 0.71428571; + align-items: center; + justify-content: center; } input[type="checkbox"]:checked::before, @@ -172,28 +183,38 @@ input[type="radio"]:checked::before { display: inline-block; vertical-align: middle; width: 1rem; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } input[type="checkbox"]:checked::before { /* Use the "Yes" SVG Dashicon */ - content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%233582c4%27%2F%3E%3C%2Fsvg%3E"); + content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%23ffffff%27%2F%3E%3C%2Fsvg%3E"); + content: url("data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%27http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%27%20viewBox%3D%270%200%2020%2020%27%3E%3Cpath%20d%3D%27M14.83%204.89l1.34.94-5.81%208.38H9.02L5.78%209.67l1.34-1.25%202.57%202.4z%27%20fill%3D%27%23ffffff%27%2F%3E%3C%2Fsvg%3E") / ''; margin: -0.1875rem 0 0 -0.25rem; height: 1.3125rem; width: 1.3125rem; } +input[type="checkbox"]:checked { + background: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); +} + +input[type="radio"]:checked { + background: var(--wp-admin-theme-color); + border-color: var(--wp-admin-theme-color); +} + input[type="radio"]:checked::before { content: ""; border-radius: 50%; width: 0.5rem; /* 8px */ height: 0.5rem; /* 8px */ - margin: 0.1875rem; /* 3px */ - background-color: #3582c4; - /* 16px not sure if still necessary, comes from the MP6 redesign in r26072 */ - line-height: 1.14285714; + margin: auto; + background-color: #fff; + /* Only visible in Windows High Contrast mode */ + outline: 4px solid transparent; } @-moz-document url-prefix() { @@ -213,6 +234,10 @@ input[type="search"]::-webkit-search-decoration { display: none; } +input[type="search"]::-webkit-search-cancel-button { + cursor: pointer; +} + .wp-admin input[type="file"] { padding: 3px 0; cursor: pointer; @@ -222,20 +247,15 @@ input.readonly, input[readonly], textarea.readonly, textarea[readonly] { - background-color: #f0f0f1; + background-color: #f0f0f0; } ::-webkit-input-placeholder { - color: #646970; + color: #757575; } ::-moz-placeholder { - color: #646970; - opacity: 1; -} - -:-ms-input-placeholder { - color: #646970; + color: #757575; } .form-invalid .form-required, @@ -250,6 +270,7 @@ textarea[readonly] { .form-table .form-required.form-invalid td:after { content: "\f534"; + content: "\f534" / ''; font: normal 20px/1 dashicons; color: #d63638; margin-left: -25px; @@ -263,6 +284,7 @@ textarea[readonly] { .form-table .form-required.user-pass1-wrap.form-invalid .password-input-wrapper:after { content: "\f534"; + content: "\f534" / ''; font: normal 20px/1 dashicons; color: #d63638; margin: 0 6px 0 -29px; @@ -279,16 +301,18 @@ select:disabled, select.disabled, textarea:disabled, textarea.disabled { - background: rgba(255, 255, 255, 0.5); - border-color: rgba(220, 220, 222, 0.75); - box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.04); - color: rgba(44, 51, 56, 0.5); + background: #f0f0f0; + border-color: #cccccc; + box-shadow: none; + color: #949494; } input[type="file"]:disabled, input[type="file"].disabled, +input[type="file"][aria-disabled="true"], input[type="range"]:disabled, -input[type="range"].disabled { +input[type="range"].disabled, +input[type="range"][aria-disabled="true"] { background: none; box-shadow: none; cursor: default; @@ -296,13 +320,16 @@ input[type="range"].disabled { input[type="checkbox"]:disabled, input[type="checkbox"].disabled, +input[type="checkbox"][aria-disabled="true"], input[type="radio"]:disabled, input[type="radio"].disabled, +input[type="radio"][aria-disabled="true"], input[type="checkbox"]:disabled:checked:before, input[type="checkbox"].disabled:checked:before, input[type="radio"]:disabled:checked:before, input[type="radio"].disabled:checked:before { opacity: 0.7; + cursor: default; } /*------------------------------------------------------------------------------ @@ -312,34 +339,35 @@ input[type="radio"].disabled:checked:before { /* Select styles are based on the default button in buttons.css */ .wp-core-ui select { font-size: 14px; - line-height: 2; /* 28px */ - color: #2c3338; - border-color: #8c8f94; + line-height: 2.71428571; /* 38px for 40px min-height */ + color: #1e1e1e; + border-color: #949494; box-shadow: none; - border-radius: 3px; - padding: 0 24px 0 8px; - min-height: 30px; + border-radius: 2px; + padding: 0 24px 0 12px; + min-height: 40px; max-width: 25rem; -webkit-appearance: none; /* The SVG is arrow-down-alt2 from Dashicons. */ - background: #fff url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%23555%22%2F%3E%3C%2Fsvg%3E') no-repeat right 5px top 55%; + background: #fff url('data:image/svg+xml;charset=US-ASCII,%3Csvg%20width%3D%2220%22%20height%3D%2220%22%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%3E%3Cpath%20d%3D%22M5%206l5%205%205-5%202%201-7%207-7-7%202-1z%22%20fill%3D%22%231e1e1e%22%2F%3E%3C%2Fsvg%3E') no-repeat right 8px top 55%; background-size: 16px 16px; cursor: pointer; vertical-align: middle; } .wp-core-ui select:hover { - color: #2271b1; + color: #1e1e1e; + border-color: #1e1e1e; } .wp-core-ui select:focus { - border-color: #2271b1; - color: #0a4b78; - box-shadow: 0 0 0 1px #2271b1; + border-color: var(--wp-admin-theme-color); + color: #1e1e1e; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color); } .wp-core-ui select:active { - border-color: #8c8f94; + border-color: #949494; box-shadow: none; } @@ -356,6 +384,10 @@ input[type="radio"].disabled:checked:before { transform: none; } +.wp-core-ui select[aria-disabled="true"] { + cursor: default; +} + /* Reset Firefox inner outline that appears on :focus. */ /* This ruleset overrides the color change on :focus thus needs to be after select:focus. */ .wp-core-ui select:-moz-focusring { @@ -370,7 +402,7 @@ input[type="radio"].disabled:checked:before { } .wp-core-ui select:hover::-ms-value { - color: #2271b1; + color: #1e1e1e; } .wp-core-ui select:focus::-ms-value { @@ -465,7 +497,7 @@ textarea.large-text { input.small-text { width: 50px; - padding: 0 6px; + padding: 0 8px; } label input.small-text { @@ -474,7 +506,7 @@ label input.small-text { input[type="number"].small-text { width: 65px; - padding-right: 0; + padding-right: 4px; } input.tiny-text { @@ -490,6 +522,9 @@ input[type="number"].tiny-text { #doaction2, #post-query-submit { margin: 0 8px 0 0; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height with 13px font */ + padding: 0 12px; } /* @since 5.7.0 secondary bulk action controls require JS. */ @@ -502,24 +537,56 @@ input[type="number"].tiny-text { display: none; } -.tablenav .actions select { +.wp-core-ui .tablenav input[type="text"], +.wp-core-ui .tablenav input[type="password"], +.wp-core-ui .tablenav input[type="date"], +.wp-core-ui .tablenav input[type="datetime"], +.wp-core-ui .tablenav input[type="datetime-local"], +.wp-core-ui .tablenav input[type="email"], +.wp-core-ui .tablenav input[type="month"], +.wp-core-ui .tablenav input[type="number"], +.wp-core-ui .tablenav input[type="search"], +.wp-core-ui .tablenav input[type="tel"], +.wp-core-ui .tablenav input[type="time"], +.wp-core-ui .tablenav input[type="url"], +.wp-core-ui .tablenav input[type="week"], +.wp-core-ui .tablenav select { + padding: 0 12px; + /* inherits font size 14px */ + line-height: 2.14285714; /* 30px for 32px height with 14px font */ + min-height: 32px; +} + +.wp-core-ui .tablenav select { float: left; margin-right: 6px; max-width: 12.5rem; + padding: 0 24px 0 8px; +} + +.wp-core-ui .tablenav .button { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height with 13px font */ + padding: 0 12px; } #timezone_string option { margin-left: 1em; } -.wp-hide-pw > .dashicons, -.wp-cancel-pw > .dashicons { - position: relative; - top: 3px; +.wp-core-ui .button.wp-hide-pw > .dashicons, +.wp-core-ui .button.wp-cancel-pw > .dashicons { width: 1.25rem; height: 1.25rem; - top: 0.25rem; font-size: 20px; + line-height: 1; + vertical-align: middle; +} + +.button.wp-hide-pw.user-new-password-toggle { + display: inline-flex; + align-items: center; + column-gap: 4px; } .wp-cancel-pw .dashicons-no { @@ -547,10 +614,62 @@ fieldset label, .wp-generate-pw { margin-top: 1em; + position: relative; +} + +.wp-pwd button { + height: min-content; } .wp-pwd { margin-top: 1em; + position: relative; +} + +.mailserver-pass-wrap .wp-pwd { + display: inline-block; + margin-top: 0; +} + +/* rtl:ignore */ +#mailserver_pass { + padding-right: 2.5rem; +} + +/* rtl:ignore */ +.mailserver-pass-wrap .button.wp-hide-pw { + background: transparent; + border: 1px solid transparent; + box-shadow: none; + font-size: 14px; + line-height: 2; + width: 2.5rem; + min-width: 40px; + margin: 0; + padding: 0 9px; + position: absolute; + right: 0; + top: 0; +} + +.mailserver-pass-wrap .button.wp-hide-pw:hover { + background: transparent; + border-color: transparent; +} + +.mailserver-pass-wrap .button.wp-hide-pw:focus { + background: transparent; + border-color: var(--wp-admin-theme-color); + border-radius: 2px; + box-shadow: 0 0 0 0.5px var(--wp-admin-theme-color); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + +.mailserver-pass-wrap .button.wp-hide-pw:active { + background: transparent; + box-shadow: none; + transform: none; } #misc-publishing-actions label { @@ -558,9 +677,10 @@ fieldset label, } #pass-strength-result { - background-color: #f0f0f1; - border: 1px solid #dcdcde; - color: #1d2327; + background-color: #f0f0f0; + border: 1px solid #cccccc; + border-radius: 2px; + color: #1e1e1e; margin: -1px 1px 5px; padding: 3px 5px; text-align: center; @@ -593,6 +713,10 @@ fieldset label, opacity: 1; } +.password-input-wrapper { + display: inline-block; +} + .password-input-wrapper input { font-family: Consolas, Monaco, monospace; } @@ -613,6 +737,13 @@ fieldset label, border-color: #68de7c; } +#pass1:focus, +#pass1-text:focus { + box-shadow: 0 0 0 0.5px var(--wp-admin-theme-color); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + .pw-weak { display: none; } @@ -625,7 +756,7 @@ fieldset label, .wp-pwd [type="password"] { margin-bottom: 0; /* Same height as the buttons */ - min-height: 30px; + min-height: 40px; } /* Hide the Edge "reveal password" native button */ @@ -646,22 +777,72 @@ fieldset label, display: inline-block; } +/* Caps lock warning */ +.wp-pwd .caps-warning { + display: none; + position: relative; + background: #fcf9e8; + border: 1px solid #f0c33c; + color: #1d2327; + padding: 6px 10px; + top: -8px; +} + +.profile-php .wp-pwd .caps-warning { + padding: 3px 5px; + top: -4px; + border-radius: 2px; +} + +.wp-pwd .caps-icon { + display: inline-flex; + justify-content: center; + width: 20px; + height: 20px; + margin-right: 5px; + vertical-align: middle; +} + +.wp-pwd .caps-warning-text { + vertical-align: middle; +} +/* Caps lock warning */ + p.search-box { + display: flex; + flex-wrap: wrap; + align-items: center; + column-gap: 0.5rem; + position: relative; float: right; - margin: 0; + margin: 11px 0; +} + +p.search-box input[type="search"], +p.search-box input[type="text"] { + min-height: 32px; + padding: 0 8px; +} + +p.search-box .button { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height with 13px font */ + padding: 0 12px; } .network-admin.themes-php p.search-box { clear: left; } -.search-box input[name="s"], -.tablenav .search-plugins input[name="s"], -.tagsdiv .newtag { +.tablenav .search-plugins input[name="s"] { float: left; margin: 0 4px 0 0; } +.tagsdiv .newtag { + margin: 0; +} + .js.plugins-php .search-box .wp-filter-search { margin: 0; width: 280px; @@ -671,7 +852,7 @@ input[type="text"].ui-autocomplete-loading, input[type="email"].ui-autocomplete-loading { background-image: url(../images/loading.gif); background-repeat: no-repeat; - background-position: right center; + background-position: right 5px center; visibility: visible; } @@ -710,7 +891,7 @@ ul#add-to-blog-users { /* Colors for the tags autocomplete. */ .wp-tags-autocomplete .ui-state-focus, .wp-tags-autocomplete [aria-selected="true"] { - background-color: #2271b1; + background-color: var(--wp-admin-theme-color); color: #fff; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; @@ -776,6 +957,12 @@ ul#add-to-blog-users { .form-table p.timezone-info { margin: 1em 0; + display: flex; + flex-direction: column; +} + +#local-time { + margin-top: 0.5em; } .form-table td fieldset label { @@ -793,13 +980,6 @@ ul#add-to-blog-users { line-height: 1.4; } -.form-table input.tog, -.form-table input[type="radio"] { - margin-top: -4px; - margin-right: 4px; - float: none; -} - .form-table .pre { padding: 8px; margin: 0; @@ -854,11 +1034,14 @@ table.form-table td .updated p { } .color-palette { + display: table; width: 100%; border-spacing: 0; border-collapse: collapse; } +.color-palette .color-palette-shade, .color-palette td { + display: table-cell; height: 20px; padding: 0; border: none; @@ -885,10 +1068,13 @@ table.form-table td .updated p { #application-passwords-section .notice { margin-top: 20px; margin-bottom: 0; + word-wrap: break-word; } .application-password-display input.code { - width: 19em; + margin-bottom: 6px; + width: 100%; + max-width: 20em; } .auth-app-card.card { @@ -927,12 +1113,12 @@ table.form-table td .updated p { .card { position: relative; margin-top: 20px; - padding: 0.7em 2em 1em; + padding: 16px 24px; min-width: 255px; max-width: 520px; - border: 1px solid #c3c4c7; - box-shadow: 0 1px 1px rgba(0, 0, 0, 0.04); - background: #fff; + border: 1px solid rgb(0, 0, 0, 0.1); + border-radius: 8px; + background: #ffffff; box-sizing: border-box; } @@ -1007,6 +1193,7 @@ table.form-table td .updated p { color: #787c82; font: normal 20px/1 dashicons; content: "\f157"; + content: "\f157" / ''; position: relative; display: inline-block; top: 4px; @@ -1054,6 +1241,7 @@ table.form-table td .updated p { .options-general-php input.small-text { width: 56px; margin: -2px 0; + min-height: 32px; } .options-general-php .spinner { @@ -1062,16 +1250,53 @@ table.form-table td .updated p { } .settings-php .language-install-spinner, -.options-general-php .language-install-spinner { +.options-general-php .language-install-spinner, +.user-edit-php .language-install-spinner, +.profile-php .language-install-spinner { display: inline-block; float: none; margin: -3px 5px 0; vertical-align: middle; } +.form-table.permalink-structure .available-structure-tags { + margin-top: 8px; +} + +.form-table.permalink-structure .available-structure-tags ul { + display: flex; + flex-wrap: wrap; + margin: 8px 0 0; +} + .form-table.permalink-structure .available-structure-tags li { - float: left; - margin-right: 5px; + margin: 6px 5px 0 0; +} + +.form-table.permalink-structure .available-structure-tags li:last-child { + margin-right: 0; +} + +.form-table.permalink-structure .structure-selection .row { + margin-bottom: 16px; +} + +.form-table.permalink-structure .structure-selection .row > div { + max-width: calc(100% - 24px); + display: inline-flex; + flex-direction: column; +} + +.form-table.permalink-structure .structure-selection .row label { + font-weight: 600; +} + +.form-table.permalink-structure .structure-selection .row p { + margin-top: 0; +} + +.permalink-structure-optional-description code { + display: inline-block; } /*------------------------------------------------------------------------------ @@ -1107,6 +1332,11 @@ table.form-table td .updated p { max-width: 60%; } +.configuration-rules-label { + font-weight: 600; + margin-bottom: 4px; +} + /*------------------------------------------------------------------------------ Credentials check dialog for Install and Updates ------------------------------------------------------------------------------*/ @@ -1207,7 +1437,7 @@ table.form-table td .updated p { } .request-filesystem-credentials-dialog .ftp-password em { - color: #8c8f94; + color: #757575; } .request-filesystem-credentials-dialog label { @@ -1332,6 +1562,11 @@ table.form-table td .updated p { font-weight: 600; } +.privacy_requests .status-date { + display: block; + font-weight: 400; +} + .wp-privacy-request-form { clear: both; } @@ -1344,28 +1579,6 @@ table.form-table td .updated p { margin: 0; } -.email-personal-data::before { - display: inline-block; - font: normal 20px/1 dashicons; - margin: 3px 5px 0 -2px; - speak: never; - -webkit-font-smoothing: antialiased; - -moz-osx-font-smoothing: grayscale; - vertical-align: top; -} - -.email-personal-data--sending::before { - color: #d63638; - content: "\f463"; - animation: rotation 2s infinite linear; -} - -.email-personal-data--sent::before { - color: #68de7c; - content: "\f147"; -} - - /* =Media Queries -------------------------------------------------------------- */ @@ -1389,8 +1602,7 @@ table.form-table td .updated p { input[type="url"], input[type="week"] { -webkit-appearance: none; - padding: 3px 10px; - /* Only necessary for IE11 */ + padding: 0 12px; min-height: 40px; } @@ -1439,16 +1651,12 @@ table.form-table td .updated p { line-height: 0.76190476; } - .wp-upload-form input[type="submit"] { - margin-top: 10px; - } - .wp-core-ui select, .wp-admin .form-table select { min-height: 40px; font-size: 16px; - line-height: 1.625; /* 26px */ - padding: 5px 24px 5px 8px; + line-height: 2.5; /* 40px for 16px font */ + padding: 0 24px 0 12px; } .wp-admin .button-cancel { @@ -1483,12 +1691,15 @@ table.form-table td .updated p { margin-bottom: 0; } + .form-table .color-palette .color-palette-shade, .form-table .color-palette td { display: table-cell; width: 15px; + height: 30px; + padding: 0; } - .form-table table.color-palette { + .form-table .color-palette { margin-right: 10px; } @@ -1528,27 +1739,41 @@ table.form-table td .updated p { margin: 0 3px; } + .form-table .regular-text ~ input[type="text"].small-text { + margin-top: 5px; + } + #pass-strength-result { width: 100%; box-sizing: border-box; padding: 8px; } + .profile-php .wp-pwd .caps-warning { + padding: 8px; + } + + .password-input-wrapper { + display: block; + } + p.search-box { float: none; - position: absolute; - bottom: 0; - width: 98%; - height: 90px; + width: 100%; margin-bottom: 20px; + display: flex; } p.search-box input[name="s"] { - float: none; width: 100%; + float: none; margin-bottom: 10px; vertical-align: middle; } + .js.plugins-php .search-box .wp-filter-search { + width: 100%; + margin-bottom: 0; + } p.search-box input[type="submit"] { margin-bottom: 10px; @@ -1572,14 +1797,25 @@ table.form-table td .updated p { } .form-table.permalink-structure td code { - margin-left: 32px; display: inline-block; } + .form-table.permalink-structure .structure-selection { + margin-top: 8px; + } + + .form-table.permalink-structure .structure-selection .row > div { + max-width: calc(100% - 36px); + width: 100%; + } + .form-table.permalink-structure td input[type="text"] { - margin-left: 32px; margin-top: 4px; - width: 96%; + } + + .permalink-structure-has-blog-prefix { + display: flex; + align-items: center; } .form-table input.regular-text { @@ -1590,6 +1826,11 @@ table.form-table td .updated p { font-size: 14px; } + .form-table td > label:first-child { + display: inline-block; + margin-top: 0.35em; + } + .background-position-control .button-group > label { font-size: 0; } @@ -1598,13 +1839,6 @@ table.form-table td .updated p { display: block; } - #utc-time, - #local-time { - display: block; - float: none; - margin-top: 0.5em; - } - .form-field #domain { max-width: none; } @@ -1639,6 +1873,10 @@ table.form-table td .updated p { right: 2.5rem; } + body.user-new-php .wp-pwd button.wp-hide-pw { + right: 0; + } + .wp-pwd button.button:hover, .wp-pwd button.button:focus { background: transparent; @@ -1660,10 +1898,24 @@ table.form-table td .updated p { padding-right: 5rem; } + body.user-new-php .wp-pwd [type="text"], + body.user-new-php .wp-pwd [type="password"] { + padding-right: 2.5rem; + } + .wp-cancel-pw .dashicons-no { display: inline-block; } + .mailserver-pass-wrap .wp-pwd { + display: block; + } + + /* rtl:ignore */ + #mailserver_pass { + padding-left: 10px; + } + .options-general-php input[type="text"].small-text { max-width: 6.25em; margin: 0; diff --git a/src/wp-admin/css/install.css b/src/wp-admin/css/install.css index 62a596f0fe1fa..9476749dd7cf2 100644 --- a/src/wp-admin/css/install.css +++ b/src/wp-admin/css/install.css @@ -16,19 +16,20 @@ body { } a { - color: #2271b1; + color: var(--wp-admin-theme-color); } a:hover, a:active { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20); } a:focus { - color: #043959; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + color: var(--wp-admin-theme-color-darker-20); + border-radius: 2px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } h1, h2 { @@ -72,18 +73,14 @@ fieldset { margin: 0; } -label { - cursor: pointer; -} - #logo { margin: -130px auto 25px; padding: 0 0 25px; width: 84px; height: 84px; overflow: hidden; - background-image: url(../images/w-logo-blue.png?ver=20131202); - background-image: none, url(../images/wordpress-logo.svg?ver=20131107); + background-image: url(../images/w-logo-gray.png?ver=20260303); + background-image: none, url(../images/wordpress-logo-gray.svg?ver=20260303); background-size: 84px; background-position: center top; background-repeat: no-repeat; @@ -131,7 +128,7 @@ textarea { font-size: 14px; text-align: left; padding: 10px 20px 10px 0; - width: 140px; + width: 115px; vertical-align: top; } @@ -145,12 +142,30 @@ textarea { font-size: 11px; } +.form-table .setup-description { + margin: 4px 0 0; + line-height: 1.6; +} + .form-table input { line-height: 1.33333333; font-size: 15px; padding: 3px 5px; } +.wp-pwd { + margin-top: 0; +} + +.form-table .wp-pwd { + display: flex; + column-gap: 4px; +} + +.form-table .password-input-wrapper { + width: 100%; +} + input, submit { font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen-Sans, Ubuntu, Cantarell, "Helvetica Neue", sans-serif; @@ -161,7 +176,7 @@ submit { .form-table input[type=url], .form-table input[type=password], #pass-strength-result { - width: 218px; + width: 100%; } .form-table th p { @@ -286,6 +301,10 @@ body.rtl, box-sizing: border-box; } + #pwd { + padding-right: 2.5rem; + } + .wp-pwd #pass1 { padding-right: 50px; } @@ -321,7 +340,7 @@ body.language-chooser { .language-chooser select option:hover, .language-chooser select option:focus { - color: #0a4b78; + color: var(--wp-admin-theme-color-darker-20); } .language-chooser .step { @@ -331,8 +350,6 @@ body.language-chooser { .screen-reader-input, .screen-reader-text { border: 0; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; margin: -1px; @@ -340,7 +357,9 @@ body.language-chooser { padding: 0; position: absolute; width: 1px; + /* Many screen reader and browser combinations announce broken words as they would appear visually. */ word-wrap: normal !important; + word-break: normal !important; } .spinner { @@ -369,7 +388,6 @@ body.language-chooser { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { .spinner { diff --git a/src/wp-admin/css/l10n.css b/src/wp-admin/css/l10n.css index 967fb4c6c6553..2d0b9b2145f69 100644 --- a/src/wp-admin/css/l10n.css +++ b/src/wp-admin/css/l10n.css @@ -66,6 +66,14 @@ body.locale-he-il .press-this a.wp-switch-editor { .locale-de-de-formal #customize-header-actions .spinner { margin: 16px 3px 0; /* default 16px 4px 0 5px */ } +body[class*="locale-de-"] .inline-edit-row fieldset label span.title, +body[class*="locale-de-"] .inline-edit-row fieldset.inline-edit-date legend { + width: 7em; /* default 6em */ +} +body[class*="locale-de-"] .inline-edit-row fieldset label span.input-text-wrap, +body[class*="locale-de-"] .inline-edit-row fieldset .timestamp-wrap { + margin-left: 7em; /* default 6em */ +} /* ru_RU: Text needs more room to breathe. */ .locale-ru-ru #adminmenu { @@ -110,7 +118,16 @@ body.locale-he-il .press-this a.wp-switch-editor { margin-left: 8em; /* default 6em */ } +/* Fix overridden width for adjusted locales */ +body[class*="locale-de-"] .quick-edit-row-post fieldset.inline-edit-col-right label span.title, +.locale-ru-ru .quick-edit-row-post fieldset.inline-edit-col-right label span.title, +.locale-lt-lt .quick-edit-row-post fieldset.inline-edit-col-right label span.title { + width: auto; +} + @media screen and (max-width: 782px) { + body[class*="locale-de-"] .inline-edit-row fieldset label span.input-text-wrap, + body[class*="locale-de-"] .inline-edit-row fieldset .timestamp-wrap, .locale-ru-ru .inline-edit-row fieldset label span.input-text-wrap, .locale-ru-ru .inline-edit-row fieldset .timestamp-wrap, .locale-lt-lt .inline-edit-row fieldset label span.input-text-wrap, diff --git a/src/wp-admin/css/list-tables.css b/src/wp-admin/css/list-tables.css index a39a3b725cd34..e2d49d09b0a42 100644 --- a/src/wp-admin/css/list-tables.css +++ b/src/wp-admin/css/list-tables.css @@ -79,14 +79,14 @@ .column-response a.post-com-count-approved:focus .comment-count-approved, .column-comments a.post-com-count-approved:hover .comment-count-approved, .column-comments a.post-com-count-approved:focus .comment-count-approved { - background: #2271b1; + background: #3858e9; } .column-response a.post-com-count-approved:hover:after, .column-response a.post-com-count-approved:focus:after, .column-comments a.post-com-count-approved:hover:after, .column-comments a.post-com-count-approved:focus:after { - border-top-color: #2271b1; + border-top-color: #3858e9; } /* @todo: consider to use a single rule for these counters and the admin menu counters. */ @@ -257,18 +257,20 @@ .vim-current, .vim-current th, .vim-current td { - background-color: #f0f6fc !important; + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08) !important; } th .comment-grey-bubble { - height: 16px; width: 16px; + /* Make sure the link clickable area fills the entire table header. */ + position: relative; + top: 2px; } th .comment-grey-bubble:before { content: "\f101"; + content: "\f101" / ''; font: normal 20px/.5 dashicons; - speak: never; display: inline-block; padding: 0; top: 4px; @@ -317,7 +319,6 @@ table.fixed { .fixed .column-role, .fixed .column-posts { - -webkit-hyphens: auto; hyphens: auto; } @@ -343,7 +344,6 @@ table.fixed { .fixed .column-comments { width: 5.5em; - padding: 8px 0; text-align: left; } @@ -384,6 +384,23 @@ table.media .column-title .filename { margin-bottom: 0.2em; } +/* Media Copy to clipboard row action */ +.media .row-actions .copy-to-clipboard-container { + display: inline; + position: relative; +} + +.media .row-actions .copy-to-clipboard-container .success { + position: absolute; + left: 50%; + transform: translate(-50%, -100%); + background: #000; + color: #fff; + border-radius: 5px; + margin: 0; + padding: 2px 5px; +} + /* @todo: pick a consistent list table selector */ .wp-list-table a { transition: none; @@ -444,48 +461,63 @@ table.media .column-title .filename { width: 160px; } +.sorting-indicators { + display: grid; +} + .sorting-indicator { display: block; - visibility: hidden; width: 10px; height: 4px; - margin-top: 8px; + margin-top: 4px; margin-left: 7px; } .sorting-indicator:before { - content: "\f142"; font: normal 20px/1 dashicons; - speak: never; display: inline-block; padding: 0; top: -4px; left: -8px; - color: #3c434a; line-height: 0.5; position: relative; vertical-align: top; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; text-decoration: none !important; - color: #3c434a; -} - -.column-comments .sorting-indicator:before { - top: 0; - left: -10px; + color: #a7aaad; } -th.sorted.asc .sorting-indicator:before, -th.desc:hover span.sorting-indicator:before, -th.desc a:focus span.sorting-indicator:before { +.sorting-indicator.asc:before { content: "\f142"; + content: "\f142" / ''; } -th.sorted.desc .sorting-indicator:before, -th.asc:hover span.sorting-indicator:before, -th.asc a:focus span.sorting-indicator:before { +.sorting-indicator.desc:before { content: "\f140"; + content: "\f140" / ''; +} + +th.sorted.desc .sorting-indicator.desc:before { + color: #1d2327; +} + +th.sorted.asc .sorting-indicator.asc:before { + color: #1d2327; +} + +th.sorted.asc a:focus .sorting-indicator.asc:before, +th.sorted.asc:hover .sorting-indicator.asc:before, +th.sorted.desc a:focus .sorting-indicator.desc:before, +th.sorted.desc:hover .sorting-indicator.desc:before { + color: #a7aaad; +} + +th.sorted.asc a:focus .sorting-indicator.desc:before, +th.sorted.asc:hover .sorting-indicator.desc:before, +th.sorted.desc a:focus .sorting-indicator.asc:before, +th.sorted.desc:hover .sorting-indicator.asc:before { + color: #1d2327; } .wp-list-table .toggle-row { @@ -506,9 +538,9 @@ th.asc a:focus span.sorting-indicator:before { } .wp-list-table .toggle-row:focus:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .wp-list-table .toggle-row:active { @@ -524,15 +556,44 @@ th.asc a:focus span.sorting-indicator:before { padding: 1px 2px 1px 0; color: #3c434a; /* same as table headers sort arrows */ content: "\f140"; + content: "\f140" / ''; font: normal 20px/1 dashicons; line-height: 1; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; - speak: never; } .wp-list-table .is-expanded .toggle-row:before { content: "\f142"; + content: "\f142" / ''; +} + +.check-column { + position: relative; +} + +.check-column label { + box-sizing: border-box; + width: 100%; + height: 100%; + display: block; + position: absolute; + top: 0; + left: 0; +} + +.check-column input { + position: relative; + z-index: 1; +} + +.check-column .label-covers-full-cell:hover + input:not(:disabled) { + box-shadow: 0 0 0 1px #2271b1; +} + +.check-column label:hover, +.check-column input:hover + label { + background: rgba(0, 0, 0, 0.05); } .locked-indicator { @@ -545,9 +606,9 @@ th.asc a:focus span.sorting-indicator:before { .locked-indicator-icon:before { color: #8c8f94; content: "\f160"; + content: "\f160" / ''; display: inline-block; font: normal 20px/1 dashicons; - speak: never; vertical-align: middle; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -574,10 +635,6 @@ tr.wp-locked .row-actions .trash { display: none; } -.fixed .column-comments .sorting-indicator { - margin-top: 3px; -} - #menu-locations-wrap .widefat { width: 60%; } @@ -594,9 +651,12 @@ th.sorted a { padding: 8px; } -.fixed .column-comments.sortable a, -.fixed .column-comments.sorted a { - padding: 8px 0; +th.sortable a:focus, +th.sorted a:focus { + border-radius: 2px; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } th.sortable a span, @@ -605,18 +665,13 @@ th.sorted a span { cursor: pointer; } -th.sorted .sorting-indicator, -th.desc:hover span.sorting-indicator, -th.desc a:focus span.sorting-indicator, -th.asc:hover span.sorting-indicator, -th.asc a:focus span.sorting-indicator { - visibility: visible; -} - .tablenav-pages .current-page { + vertical-align: top; margin: 0 2px 0 0; font-size: 13px; text-align: center; + min-height: 32px; + padding: 0 8px; } .tablenav .total-pages { @@ -629,9 +684,8 @@ th.asc a:focus span.sorting-indicator { .tablenav { clear: both; - height: 30px; + height: 32px; margin: 6px 0 4px; - padding-top: 5px; vertical-align: middle; } @@ -653,12 +707,12 @@ th.asc a:focus span.sorting-indicator { .tablenav .tablenav-pages .tablenav-pages-navspan { display: inline-block; vertical-align: baseline; - min-width: 30px; - min-height: 30px; + min-width: 32px; + min-height: 32px; margin: 0; padding: 0 4px; font-size: 16px; - line-height: 1.625; /* 26px */ + line-height: 1.875; /* 30px for 32px height */ text-align: center; } @@ -715,7 +769,6 @@ th.asc a:focus span.sorting-indicator { color: #c3c4c7; display: inline-block; font: normal 20px/1 dashicons; - speak: never; vertical-align: middle; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -732,14 +785,17 @@ th.asc a:focus span.sorting-indicator { .view-switch .view-list:before { content: "\f163"; + content: "\f163" / ''; } .view-switch .view-excerpt:before { content: "\f164"; + content: "\f164" / ''; } .view-switch .view-grid:before { content: "\f509"; + content: "\f509" / ''; } .filter { @@ -797,7 +853,7 @@ p.pagenav { } .row-actions { - color: #a7aaad; + color: #646970; font-size: 13px; padding: 2px 0 0; position: relative; @@ -841,16 +897,35 @@ tr:hover .row-actions, /* Layout */ #wpbody-content .inline-edit-row fieldset { - font-size: 12px; float: left; margin: 0; - padding: 0; + padding: 0 12px 0 0; width: 100%; + box-sizing: border-box; +} + +#wpbody-content .inline-edit-row td fieldset:last-of-type { + padding-right: 0; +} + +tr.inline-edit-row td { + padding: 0; + /* Prevents the focus style on .inline-edit-wrapper from being cut-off */ + position: relative; +} + +.inline-edit-wrapper { + display: flow-root; + padding: 0 12px; + border: 1px solid transparent; + border-radius: 4px; } -tr.inline-edit-row td, -#wpbody-content .inline-edit-row fieldset .inline-edit-col { - padding: 0 0.5em; +.inline-edit-wrapper:focus { + border-color: var(--wp-admin-theme-color, #3858e9); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } #wpbody-content .quick-edit-row-post .inline-edit-col-left { @@ -871,7 +946,7 @@ tr.inline-edit-row td, #wpbody-content .quick-edit-row-page .inline-edit-col-right, #wpbody-content .bulk-edit-row-post .inline-edit-col-right { - width: 49%; + width: 50%; } #wpbody-content .bulk-edit-row .inline-edit-col-left { @@ -897,12 +972,26 @@ tr.inline-edit-row td, } .inline-edit-row .submit { + display: flex; + flex-wrap: wrap; + align-items: center; clear: both; - padding: 0.5em; - margin: 0.5em 0 0; + margin: 0; + padding: 0.5em 0 1em; +} + +.inline-edit-save.submit .button { + margin-right: 8px; +} + +.inline-edit-save .spinner { + float: none; + margin: 0; } .inline-edit-row .notice-error { + box-sizing: border-box; + min-width: 100%; margin-top: 1em; } @@ -916,15 +1005,11 @@ tr.inline-edit-row td, /* Needs higher specificity for the padding */ #the-list .inline-edit-row .inline-edit-legend { margin: 0; - padding: 0.2em 0.5em 0; + padding: 0.2em 0; line-height: 2.5; font-weight: 600; } -#the-list #bulk-edit.inline-edit-row .inline-edit-legend { - padding: 0.2em 0.5em; -} - .inline-edit-row fieldset span.title, .inline-edit-row fieldset span.checkbox-title { margin: 0; @@ -996,6 +1081,17 @@ tr.inline-edit-row td, vertical-align: top; } +.inline-edit-row select, +.inline-edit-row input:where(:not([type=checkbox],[type=radio],[type=submit],[type=button])) { + min-height: 32px; + padding: 0 8px 0 8px; +} + +.inline-edit-row select { + line-height: 2.14285714; /* 30px for 32px height with 14px font */ + padding-right: 24px; +} + #wpbody-content .bulk-edit-row fieldset .inline-edit-group label { max-width: 50%; } @@ -1031,34 +1127,17 @@ tr.inline-edit-row td, width: 8em; } -ul.cat-checklist { - height: 12em; - border: solid 1px #dcdcde; - overflow-y: scroll; - padding: 0 5px; - margin: 0; - background-color: #fff; -} - -#bulk-titles { - display: block; - height: 12em; - border: 1px solid #dcdcde; - overflow-y: scroll; - padding: 0 5px; - margin: 0 0 5px; -} - +#bulk-titles-list, +#bulk-titles-list li, .inline-edit-row fieldset ul.cat-checklist li, .inline-edit-row fieldset ul.cat-checklist input { margin: 0; position: relative; /* RTL fix, #WP27629 */ } -.inline-edit-row fieldset ul.cat-checklist label, -.inline-edit-row #bulk-titles div { - font-style: normal; - font-size: 11px; +.inline-edit-row fieldset ul.cat-checklist input { + margin-top: -1px; + margin-left: 3px; } .inline-edit-row fieldset label input.inline-edit-menu-order-input { @@ -1069,7 +1148,7 @@ ul.cat-checklist { width: 75%; } -.inline-edit-row #post_parent, +.inline-edit-row select[name="post_parent"], .inline-edit-row select[name="page_template"] { max-width: 80%; } @@ -1078,27 +1157,59 @@ ul.cat-checklist { float: left; } -#bulk-titles { - line-height: 140%; +#bulk-titles, +ul.cat-checklist { + height: 14em; + border: 1px solid #ddd; + margin: 0 0 5px; + padding: 0.2em 5px; + overflow-y: scroll; } -#bulk-titles div { - margin: 0.2em 0.3em; + +ul.cat-checklist input[name="post_category[]"]:indeterminate::before { + content: ''; + border-top: 2px solid grey; + width: 65%; + height: 2px; + position: absolute; + top: calc( 50% + 1px ); + left: 50%; + transform: translate( -50%, -50% ); } -#bulk-titles div a { - cursor: pointer; - display: block; - float: left; - height: 18px; - margin: 0 3px 0 -2px; - overflow: hidden; - position: relative; - width: 20px; +#bulk-titles .ntdelbutton, +#bulk-titles .ntdeltitle, +.inline-edit-row fieldset ul.cat-checklist label { + display: inline-block; + margin: 0; + padding: 3px 0; + line-height: 20px; + vertical-align: top; } -#bulk-titles div a:before { - position: relative; - top: -3px; +#bulk-titles .ntdelitem { + padding-left: 23px; +} + +#bulk-titles .ntdelbutton { + width: 26px; + height: 26px; + margin: 0 0 0 -26px; + text-align: center; + border-radius: 3px; +} + +#bulk-titles .ntdelbutton:before { + display: inline-block; + vertical-align: top; +} + +#bulk-titles .ntdelbutton:focus { + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; + /* Reset inherited offset from Gutenberg */ + outline-offset: 0; } /*------------------------------------------------------------------------------ @@ -1170,7 +1281,7 @@ ul.cat-checklist { .plugins .active td, .plugins .active th { - background-color: #f0f6fc; + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); } .plugins .update th, @@ -1211,7 +1322,7 @@ ul.cat-checklist { .plugins .active th.check-column, .plugin-update-tr.active td { - border-left: 4px solid #72aee6; + border-left: 4px solid var(--wp-admin-theme-color); } .wp-list-table.plugins .plugin-title, @@ -1220,8 +1331,10 @@ ul.cat-checklist { white-space: nowrap; } -.plugins .plugin-title img, -.plugins .plugin-title .dashicons { + +.plugins .plugin-title .dashicons, +.plugins .plugin-title img.plugin-icon, +.plugins .plugin-title img.updates-table-screenshot { float: left; padding: 0 10px 0 0; width: 64px; @@ -1236,7 +1349,7 @@ ul.cat-checklist { color: #c3c4c7; } -#update-themes-table .plugin-title img, +#update-themes-table .plugin-title img.updates-table-screenshot, #update-themes-table .plugin-title .dashicons { width: 85px; } @@ -1294,7 +1407,7 @@ ul.cat-checklist { } .plugins tr.paused th.check-column { - border-left: 4px solid #d63638; + border-left: 4px solid #b32d2e; } .plugins tr.paused th, @@ -1304,7 +1417,7 @@ ul.cat-checklist { .plugins tr.paused .plugin-title, .plugins .paused .dashicons-warning { - color: #d63638; + color: #b32d2e; } .plugins .paused .error-display p, @@ -1314,16 +1427,16 @@ ul.cat-checklist { } .plugins .resume-link { - color: #d63638; + color: #b32d2e; } .plugin-card .update-now:before { color: #d63638; content: "\f463"; + content: "\f463" / ''; display: inline-block; - font: normal 20px/1 dashicons; - margin: 3px 5px 0 -2px; - speak: never; + font: normal 16px/1.875 dashicons; /* line-height 1.875 = 30px to match button */ + margin: 0 5px 0 -2px; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; vertical-align: top; @@ -1331,6 +1444,7 @@ ul.cat-checklist { .plugin-card .updating-message:before { content: "\f463"; + content: "\f463" / ''; animation: rotation 2s infinite linear; } @@ -1346,6 +1460,26 @@ ul.cat-checklist { .plugin-card .updated-message:before { color: #68de7c; content: "\f147"; + content: "\f147" / ''; +} + +.plugin-card .updated-message:before, +.plugin-card .updating-message:before { + line-height: 1; + position: relative; + top: -2px; + vertical-align: middle; +} + +.plugin-install-php #the-list { + display: flex; + flex-wrap: wrap; +} + +.plugin-install-php .plugin-card { + display: flex; + flex-direction: column; + justify-content: space-between; } .plugin-install-php h2 { @@ -1375,9 +1509,11 @@ ul.cat-checklist { margin: 0 8px 16px; width: 48.5%; width: calc( 50% - 8px ); - background-color: #fff; - border: 1px solid #dcdcde; + background-color: #ffffff; + border: 1px solid rgb(0, 0, 0, 0.1); + border-radius: 8px; box-sizing: border-box; + overflow: hidden; } .plugin-card:nth-child(odd) { @@ -1441,7 +1577,7 @@ ul.cat-checklist { .plugin-card-top { position: relative; - padding: 20px 20px 10px; + padding: 16px; min-height: 135px; } @@ -1450,16 +1586,97 @@ div.action-links, margin: 0; /* Override existing margins */ } +/* Use compact size for space-constrained plugin cards */ +.plugin-action-buttons li .button { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height */ + padding: 0 12px; +} + .plugin-card h3 { - margin: 0 12px 12px 0; + margin: 0 12px 16px 0; font-size: 18px; line-height: 1.3; } -.plugin-card .name, .plugin-card .desc { - margin-left: 148px; /* icon + margin */ - margin-right: 128px; /* action links + margin */ + margin-inline: 0; +} + +.plugin-card .name, .plugin-card .desc > p { + margin-left: 148px; +} + +@media (min-width: 1101px) { + .plugin-card .name, .plugin-card .desc > p { + margin-right: 128px; + } +} + +@media (min-width: 481px) and (max-width: 781px) { + .plugin-card .name, .plugin-card .desc > p { + margin-right: 128px; + } +} + +.plugin-card .column-description { + display: flex; + flex-direction: column; + justify-content: flex-start; +} + +.plugin-card .column-description > p { + margin-top: 0; +} + +.plugin-card .column-description p:empty { + display: none; +} + +.plugin-card .notice.plugin-dependencies { + margin: auto 20px 20px; + padding: 15px; +} + +.plugin-card .plugin-dependencies-explainer-text { + margin-block: 0; +} + +.plugin-card .plugin-dependency { + align-items: center; + display: flex; + flex-wrap: wrap; + margin-top: .5em; + column-gap: 1%; + row-gap: .5em; +} + +.plugin-card .plugin-dependency:nth-child(2), +.plugin-card .plugin-dependency:last-child { + margin-top: 1em; +} + +.plugin-card .plugin-dependency-name { + flex-basis: 74%; +} + +.plugin-card .plugin-dependency .more-details-link { + margin-left: auto; +} + +.rtl .plugin-card .plugin-dependency .more-details-link { + margin-right: auto; +} + +@media (max-width: 939px) { + .plugin-card .plugin-dependency-name { + flex-basis: 69%; + } +} + +.plugins #the-list .required-by, +.plugins #the-list .requires { + margin-top: 1em; } .plugin-card .action-links { @@ -1482,9 +1699,9 @@ div.action-links, .plugin-card-bottom { clear: both; - padding: 12px 20px; + padding: 16px; background-color: #f6f7f7; - border-top: 1px solid #dcdcde; + border-top: 1px solid rgb(0, 0, 0, 0.1); overflow: hidden; } @@ -1533,7 +1750,6 @@ div.action-links, .plugin-card .column-compatibility span:before { font: normal 20px/.5 dashicons; - speak: never; display: inline-block; padding: 0; top: 4px; @@ -1548,11 +1764,13 @@ div.action-links, .plugin-card .column-compatibility .compatibility-incompatible:before { content: "\f158"; + content: "\f158" / ''; color: #d63638; } .plugin-card .column-compatibility .compatibility-compatible:before { content: "\f147"; + content: "\f147" / ''; color: #007017; } @@ -1560,7 +1778,7 @@ div.action-links, margin: 20px 20px 0; } -.plugin-icon { +.plugin-card .plugin-icon { position: absolute; top: 20px; left: 20px; @@ -1575,6 +1793,7 @@ div.action-links, font-style: normal; margin: 0; padding: 100px 0 0; + width: 100%; text-align: center; } @@ -1702,6 +1921,15 @@ div.action-links, display: none; } + .tablenav.bottom .actions select { + margin-bottom: 5px; + } + + .tablenav.bottom .actions.alignleft + .actions.alignleft { + clear: left; + margin-top: 10px; + } + .tablenav.bottom .tablenav-pages.one-page { margin-top: 15px; height: 0; @@ -1814,7 +2042,6 @@ div.action-links, /* Show comment bubble as text instead */ .post-com-count .screen-reader-text { position: static; - -webkit-clip-path: none; clip-path: none; width: auto; height: auto; @@ -1877,7 +2104,6 @@ div.action-links, } .row-actions { - margin-left: -8px; margin-right: -8px; padding-top: 4px; } @@ -1890,10 +2116,15 @@ div.action-links, color: transparent; } + .row-actions span { + font-size: 0; + } + .row-actions span a, .row-actions span .button-link { display: inline-block; - padding: 4px 8px; + padding: 4px 16px 4px 0; + font-size: 13px; line-height: 1.5; } @@ -1914,6 +2145,20 @@ div.action-links, #wpbody-content .bulk-edit-row .inline-edit-col-bottom { float: none; width: 100%; + padding: 0; + } + + #the-list .inline-edit-row .inline-edit-legend, + .inline-edit-row span.title { + font-size: 16px; + } + + .inline-edit-row p.howto { + font-size: 14px; + } + + #wpbody-content .inline-edit-row-page .inline-edit-col-right { + margin-top: 0; } #wpbody-content .quick-edit-row fieldset .inline-edit-col label, @@ -1941,9 +2186,28 @@ div.action-links, padding: 3px 4px; } - .inline-edit-row fieldset ul.cat-checklist label, - .inline-edit-row #bulk-titles div { + #bulk-titles .ntdelbutton, + #bulk-titles .ntdeltitle, + .inline-edit-row fieldset ul.cat-checklist label { + padding: 6px 0; font-size: 16px; + line-height: 28px; + } + + #bulk-titles .ntdelitem { + padding-left: 37px; + } + + #bulk-titles .ntdelbutton { + width: 40px; + height: 40px; + margin: 0 0 0 -40px; + overflow: hidden; + } + + #bulk-titles .ntdelbutton:before { + font-size: 20px; + line-height: 28px; } .inline-edit-row fieldset label span.title, @@ -1951,10 +2215,6 @@ div.action-links, float: none; } - .inline-edit-row fieldset label.inline-edit-tags { - padding: 0 0.5em; - } - .inline-edit-row fieldset .inline-edit-col label.inline-edit-tags { padding: 0; } @@ -1984,14 +2244,6 @@ div.action-links, display: block; } - #bulk-titles div { - margin: 0.8em 0.3em; - } - - #bulk-titles div a { - height: 22px; - } - /* Updates */ #wpbody-content .updates-table .plugin-title { width: auto; @@ -2020,6 +2272,11 @@ div.action-links, padding: 10px 9px; /* reset from other list tables that have a label at this width */ } + #wpbody-content .wp-list-table.plugins .plugin-deleted-tr td, + #wpbody-content .wp-list-table.plugins .no-items td { + display: table-cell; + } + /* Plugin description hidden via Screen Options */ #wpbody-content .wp-list-table.plugins .desc.hidden { display: none; @@ -2084,8 +2341,8 @@ div.action-links, .plugins .active.update + .plugin-update-tr:before, .plugins .active.updated + .plugin-update-tr:before { - background-color: #f0f6fc; - border-left: 4px solid #72aee6; + background-color: rgba(var(--wp-admin-theme-color--rgb), 0.08); + border-left: 4px solid var(--wp-admin-theme-color); } .plugins .plugin-update-tr .update-message { diff --git a/src/wp-admin/css/login.css b/src/wp-admin/css/login.css index 3c795ebab5c42..fec7b6ff78387 100644 --- a/src/wp-admin/css/login.css +++ b/src/wp-admin/css/login.css @@ -15,7 +15,7 @@ body { } a { - color: #2271b1; + color: var(--wp-admin-theme-color-darker-10); transition-property: border, background, color; transition-duration: .05s; transition-timing-function: ease-in-out; @@ -27,14 +27,14 @@ a { a:hover, a:active { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20); } a:focus { color: #043959; - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } p { @@ -42,22 +42,42 @@ p { } .login .message, -.login .success, -.login #login_error { - border-left: 4px solid #72aee6; - padding: 12px; +.login .notice, +.login .success { + border-left: 4px solid #3858e9; + padding: 8px 12px; + margin-top: 0; margin-left: 0; margin-bottom: 20px; background-color: #fff; - box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.1); + word-wrap: break-word; +} + +.login .message p, +.login .notice p, +.login .success p { + font-size: 13px; + line-height: 1.54; + margin: 0.5em 0; } .login .success { - border-left-color: #00a32a; + border-left-color: #4ab866; + background-color: #eff9f1; } -.login #login_error { - border-left-color: #d63638; +/* Match border color from common.css */ +.login .notice-error { + border-left-color: #cc1818; + background-color: #fcf0f0; +} + +.login .login-error-list { + list-style: none; +} + +.login .login-error-list li + li { + margin-top: 4px; } #loginform p.submit, @@ -79,12 +99,13 @@ p { margin-bottom: 15px; } +/* rtl:ignore */ .login .button.wp-hide-pw { background: transparent; border: 1px solid transparent; box-shadow: none; font-size: 14px; - line-height: 2; + line-height: normal; width: 2.5rem; height: 2.5rem; min-width: 40px; @@ -102,8 +123,8 @@ p { .login .button.wp-hide-pw:focus { background: transparent; - border-color: #3582c4; - box-shadow: 0 0 0 1px #3582c4; + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 1px var(--wp-admin-theme-color); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -117,7 +138,6 @@ p { .login .button.wp-hide-pw .dashicons { width: 1.25rem; height: 1.25rem; - top: 0.25rem; } .login .wp-pwd { @@ -129,9 +149,8 @@ p { } .login form { - margin-top: 20px; - margin-left: 0; - padding: 26px 24px 34px; + margin: 24px 0; + padding: 26px 24px; font-weight: 400; overflow: hidden; background: #fff; @@ -214,7 +233,7 @@ p { margin: 1.1em 0; } -.login h1.admin-email__heading { +.login .admin-email__heading { border-bottom: 1px #f0f0f1 solid; color: #50575e; font-weight: normal; @@ -236,6 +255,11 @@ p { margin-bottom: 0; } +#login form .indicator-hint, +#login #reg_passmail { + margin-bottom: 16px; +} + #login form p.submit { margin: 0; padding: 0; @@ -259,8 +283,8 @@ p { } .login h1 a { - background-image: url(../images/w-logo-blue.png?ver=20131202); - background-image: none, url(../images/wordpress-logo.svg?ver=20131107); + background-image: url(../images/w-logo-gray.png?ver=20260303); + background-image: none, url(../images/wordpress-logo-gray.svg?ver=20260303); background-size: 84px; background-position: center top; background-repeat: no-repeat; @@ -269,7 +293,7 @@ p { font-size: 20px; font-weight: 400; line-height: 1.3; - margin: 0 auto 25px; + margin: 0 auto 24px; padding: 0; text-decoration: none; width: 84px; @@ -281,7 +305,7 @@ p { #login { width: 320px; - padding: 8% 0 0; + padding: 5% 0 0; margin: auto; } @@ -297,7 +321,7 @@ p { #backtoblog { margin: 16px 0; - word-break: break-word; + word-wrap: break-word; } .login #nav a, @@ -309,7 +333,7 @@ p { .login #nav a:hover, .login #backtoblog a:hover, .login h1 a:hover { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20); } .login #nav a:focus, @@ -321,7 +345,7 @@ p { .login .privacy-policy-page-link { text-align: center; width: 100%; - margin: 5em 0 2em; + margin: 3em 0 2em; } .login form .input, @@ -341,18 +365,13 @@ p { font-family: Consolas, Monaco, monospace; } -.js.login input.password-input, -.js.login-action-rp form .input, -.js.login-action-rp input[type="text"] { +/* rtl:ignore */ +.js.login input.password-input { padding-right: 2.5rem; } -.login form .input, -.login input[type="text"], -.login form input[type="checkbox"] { - background: #fff; -} - +.js.login-action-resetpass input[type="text"], +.js.login-action-resetpass input[type="password"], .js.login-action-rp input[type="text"], .js.login-action-rp input[type="password"] { margin-bottom: 0; @@ -392,8 +411,6 @@ body.interim-login { .screen-reader-text, .screen-reader-text span { border: 0; - clip: rect(1px, 1px, 1px, 1px); - -webkit-clip-path: inset(50%); clip-path: inset(50%); height: 1px; margin: -1px; @@ -401,7 +418,9 @@ body.interim-login { padding: 0; position: absolute; width: 1px; - word-wrap: normal !important; /* many screen reader and browser combinations announce broken words as they would appear visually */ + /* Many screen reader and browser combinations announce broken words as they would appear visually. */ + word-wrap: normal !important; + word-break: normal !important; } /* Hide the Edge "reveal password" native button */ @@ -409,10 +428,45 @@ input::-ms-reveal { display: none; } +#language-switcher { + padding: 0; + overflow: visible; + background: none; + border: none; + box-shadow: none; +} + +#language-switcher select { + margin-right: 0.25em; +} + +.language-switcher { + margin: 0 auto; + padding: 0 0 24px; + text-align: center; +} + +.language-switcher label { + margin-right: 0.25em; +} + +.language-switcher label .dashicons { + width: auto; + height: auto; +} + +.login .language-switcher .button { + margin-bottom: 0; +} + @media screen and (max-height: 550px) { #login { padding: 20px 0; } + + #language-switcher { + margin-top: 0; + } } @@ -427,4 +481,16 @@ input::-ms-reveal { height: 1.3125rem; margin: -0.1875rem 0 0 -0.25rem; } + + #language-switcher label, + #language-switcher select { + margin-right: 0; + } +} + +@media screen and (max-width: 400px) { + .login .language-switcher .button { + display: block; + margin: 5px auto 0; + } } diff --git a/src/wp-admin/css/media.css b/src/wp-admin/css/media.css index 5aaf859ad4cfb..ae21bb77d3f82 100644 --- a/src/wp-admin/css/media.css +++ b/src/wp-admin/css/media.css @@ -112,14 +112,8 @@ margin-right: 10px; } -.media-item-wrapper { - display: grid; - grid-template-columns: 1fr 1fr; -} - .media-item .attachment-tools { display: flex; - justify-content: flex-end; align-items: center; } @@ -130,6 +124,7 @@ } .media-item .edit-attachment.copy-to-clipboard-container { + display: flex; margin-top: 0; } @@ -172,14 +167,14 @@ } .media-item .filename { - padding: 14px 0; + padding: 14px 2px; overflow: hidden; - margin-left: 6px; + margin-left: 4px; } .media-item .pinkynail { float: left; - margin: 0 10px 0 0; + margin: 14px; max-height: 70px; max-width: 70px; } @@ -189,15 +184,10 @@ display: none; } -.media-item .original { - position: relative; - height: 34px; -} - .media-item .progress { - float: right; + display: inline-block; height: 22px; - margin: 7px 6px; + margin: 0 6px 7px; width: 200px; line-height: 2em; padding: 0; @@ -351,13 +341,11 @@ #find-posts-close:hover, #find-posts-close:focus { - color: #135e96; + color: var(--wp-admin-theme-color-darker-20, #183ad6); } #find-posts-close:focus { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; outline-offset: -2px; @@ -366,10 +354,10 @@ #find-posts-close:before { font: normal 20px/36px dashicons; vertical-align: top; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; content: "\f158"; + content: "\f158" / ''; } .find-box-buttons { @@ -455,6 +443,10 @@ border color while dragging a file over the uploader drop area */ position: relative; } +.post-type-attachment .wp-filter select { + margin: 0 6px 0 0; +} + /** * Media Library grid view */ @@ -507,24 +499,6 @@ border color while dragging a file over the uploader drop area */ background: none; } -.upload-php .mode-grid .media-sidebar .media-uploader-status .upload-dismiss-errors { - top: -10px; - right: -14px; - padding: 10px; -} - -.upload-php .mode-grid .media-sidebar .media-uploader-status .upload-dismiss-errors:before { - content: "\f153"; - display: block; - font: normal 16px/1 dashicons; - color: #787c82; -} - -.upload-php .mode-grid .media-sidebar .media-uploader-status .upload-dismiss-errors:focus:before, -.upload-php .mode-grid .media-sidebar .media-uploader-status .upload-dismiss-errors:hover:before { - color: #d63638; -} - .upload-php .mode-grid .media-sidebar .media-uploader-status.errors h2 { display: none; } @@ -550,9 +524,7 @@ border color while dragging a file over the uploader drop area */ .media-frame.mode-grid .attachment:focus, .media-frame.mode-grid .selected.attachment:focus, .media-frame.mode-grid .attachment.details:focus { - box-shadow: - inset 0 0 2px 3px #f0f0f1, - inset 0 0 0 7px #4f94d4; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; outline-offset: -6px; @@ -567,7 +539,7 @@ border color while dragging a file over the uploader drop area */ .media-frame.mode-grid .attachment.details { box-shadow: inset 0 0 0 3px #f0f0f1, - inset 0 0 0 7px #4f94d4; + inset 0 0 0 7px var(--wp-admin-theme-color, #3858e9); } .media-frame.mode-grid.mode-select .attachment .thumbnail { @@ -583,13 +555,42 @@ border color while dragging a file over the uploader drop area */ height: auto; } +.media-frame.mode-grid .media-toolbar label:not(.media-search-input-label) { + border: 0; + clip-path: inset(50%); + height: 1px; + margin: -1px; + overflow: hidden; + padding: 0; + position: absolute; + width: 1px; + /* Many screen reader and browser combinations announce broken words as they would appear visually. */ + word-wrap: normal !important; + word-break: normal !important; +} + .media-frame.mode-grid .media-toolbar select { margin: 0 10px 0 0; + min-height: 32px; + line-height: 2.14285714; /* 30px for 32px height with 14px font */ + padding: 0 24px 0 8px; +} + +.media-frame.mode-grid .media-toolbar input[type="search"] { + min-height: 32px; + padding: 0 8px; +} + +.media-frame.mode-grid .media-toolbar-secondary { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; } .media-frame.mode-grid.mode-edit .media-toolbar-secondary > .select-mode-toggle-button { margin: 0 8px 0 0; - vertical-align: middle; + height: 100%; } .media-frame.mode-grid .attachments-browser .bulk-select { @@ -601,16 +602,10 @@ border color while dragging a file over the uploader drop area */ margin-top: 0; } -.media-search-input-label { - margin: 0 .2em 0 0; +.media-frame-content .media-search-input-label { vertical-align: baseline; } -.media-frame.mode-grid .media-search-input-label { - position: static; - margin: 0 .5em 0 0; -} - .attachments-browser .media-toolbar-secondary > .media-button { margin-right: 10px; } @@ -656,6 +651,7 @@ border color while dragging a file over the uploader drop area */ .upload-php .media-modal-close .media-modal-icon:before { content: "\f335"; + content: "\f335" / ''; font-size: 22px; } @@ -728,10 +724,12 @@ border color while dragging a file over the uploader drop area */ .edit-attachment-frame .edit-media-header .left:before { content: "\f341"; + content: "\f341" / ''; } .edit-attachment-frame .edit-media-header .right:before { content: "\f345"; + content: "\f345" / ''; } .edit-attachment-frame .edit-media-header [disabled], @@ -793,14 +791,20 @@ border color while dragging a file over the uploader drop area */ text-align: center; } +.edit-attachment-frame .button { + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px height with 13px font */ + padding: 0 12px; +} + .edit-attachment-frame .wp-media-wrapper { margin-bottom: 12px; } .edit-attachment-frame input, .edit-attachment-frame textarea { - padding: 6px 8px; - line-height: 1.14285714; + padding: 4px 8px; + line-height: 1.42857143; } .edit-attachment-frame .attachment-info { @@ -856,7 +860,7 @@ border color while dragging a file over the uploader drop area */ } .copy-to-clipboard-container .success { - color: #008a20; + color: #007017; margin-left: 8px; } @@ -867,6 +871,11 @@ border color while dragging a file over the uploader drop area */ margin-bottom: 5px; } +.wp_attachment_details #attachment_alt { + max-width: 500px; + height: 3.28571428em; +} + .wp_attachment_details .attachment-alt-text-description { margin-top: 5px; } @@ -890,16 +899,16 @@ border color while dragging a file over the uploader drop area */ padding-top: 10px; } -.imgedit-settings p, -.imgedit-settings fieldset { +.image-editor p, +.image-editor fieldset { margin: 8px 0; } -.imgedit-settings legend { +.image-editor legend { margin-bottom: 5px; } -.describe .imgedit-wrap .imgedit-settings { +.describe .imgedit-wrap .image-editor { padding: 0 5px; } @@ -911,19 +920,40 @@ border color while dragging a file over the uploader drop area */ height: auto; } -.wp_attachment_holder .imgedit-wrap .imgedit-panel-content { - float: left; - padding: 3px 16px 0 0; - min-width: 400px; - max-width: calc( 100% - 266px ); +.imgedit-panel-content { + display: flex; + flex-wrap: wrap; + gap: 20px; + margin-bottom: 20px; +} + +.imgedit-settings { + max-width: 240px; /* Prevent reflow when help info is expanded. */ +} + +.imgedit-group-controls > * { + display: none; +} + +.imgedit-panel-active .imgedit-group-controls > * { + display: block; +} + +.imgedit-panel-active .imgedit-group-controls > .imgedit-crop-apply { + display: flex; } -.wp_attachment_holder .imgedit-wrap .imgedit-settings { +.imgedit-crop-apply { + gap: 4px; + flex-wrap: wrap; +} + +.wp_attachment_holder .imgedit-wrap .image-editor { float: right; width: 250px; } -.imgedit-settings input { +.image-editor input { margin-top: 0; vertical-align: middle; } @@ -958,7 +988,7 @@ border color while dragging a file over the uploader drop area */ } .media-disabled, -.imgedit-settings .disabled { +.image-editor .disabled { /* WCAG 1.4.3 Text or images of text that are part of an inactive user interface component ... have no contrast requirement. */ color: #a7aaad; @@ -982,30 +1012,16 @@ border color while dragging a file over the uploader drop area */ float: left; } -.imgedit-menu { - margin: 0 0 12px; -} - .imgedit-menu .note-no-rotate { clear: both; margin: 0; padding: 1em 0 0; } -.image-editor .imgedit-menu .button { - display: inline-block; - width: auto; - min-height: 28px; - font-size: 13px; - line-height: 2; - margin: 0 8px 8px 0; - padding: 0 10px; -} - +.imgedit-menu .button:after, .imgedit-menu .button:before { font: normal 16px/1 dashicons; margin-right: 8px; - speak: never; vertical-align: middle; position: relative; top: -2px; @@ -1013,6 +1029,16 @@ border color while dragging a file over the uploader drop area */ -moz-osx-font-smoothing: grayscale; } +.imgedit-menu .imgedit-rotate.button:after { + content: '\f140'; + margin-left: 2px; + margin-right: 0; +} + +.imgedit-menu .imgedit-rotate.button[aria-expanded="true"]:after { + content: '\f142'; +} + .imgedit-menu .button.disabled { color: #a7aaad; border-color: #dcdcde; @@ -1025,30 +1051,27 @@ border color while dragging a file over the uploader drop area */ .imgedit-crop:before { content: "\f165"; + content: "\f165" / ''; } -.imgedit-rleft:before { - content: "\f166"; +.imgedit-scale:before { + content: "\f211"; + content: "\f211" / ''; } -.imgedit-rright:before { +.imgedit-rotate:before { content: "\f167"; -} - -.imgedit-flipv:before { - content: "\f168"; -} - -.imgedit-fliph:before { - content: "\f169"; + content: "\f167" / ''; } .imgedit-undo:before { content: "\f171"; + content: "\f171" / ''; } .imgedit-redo:before { content: "\f172"; + content: "\f172" / ''; } .imgedit-crop-wrap { @@ -1061,23 +1084,19 @@ border color while dragging a file over the uploader drop area */ background-size: 20px 20px; } -.imgedit-crop { - margin: 0 8px 0 0; -} - -.imgedit-rleft { - margin: 0 3px; +.imgedit-crop-wrap { + padding: 20px; + background-image: linear-gradient(45deg, #c3c4c7 25%, transparent 25%, transparent 75%, #c3c4c7 75%, #c3c4c7), linear-gradient(45deg, #c3c4c7 25%, transparent 25%, transparent 75%, #c3c4c7 75%, #c3c4c7); + background-position: 0 0, 10px 10px; + background-size: 20px 20px; } -.imgedit-rright { - margin: 0 8px 0 3px; -} -.imgedit-flipv { - margin: 0 3px; +.imgedit-crop { + margin: 0 8px 0 0; } -.imgedit-fliph { +.imgedit-rotate { margin: 0 8px 0 3px; } @@ -1089,6 +1108,12 @@ border color while dragging a file over the uploader drop area */ margin: 0 8px 0 3px; } +.imgedit-thumbnail-preview-group { + display: flex; + flex-wrap: wrap; + column-gap: 10px; +} + .imgedit-thumbnail-preview { margin: 10px 8px 0 0; } @@ -1115,11 +1140,41 @@ border color while dragging a file over the uploader drop area */ padding: .5em 0 0; } +.imgedit-popup-menu, .imgedit-help { display: none; padding-bottom: 8px; } +.imgedit-panel-tools > .imgedit-menu { + display: flex; + column-gap: 4px; + align-items: flex-start; + flex-wrap: wrap; +} + +.imgedit-popup-menu { + width: calc( 100% - 20px ); + position: absolute; + background: #fff; + padding: 10px; + box-shadow: 0 3px 6px rgba(0, 0, 0, 0.3); +} + +.image-editor .imgedit-menu .imgedit-popup-menu button { + display: block; + margin: 2px 0; + width: 100%; + white-space: break-spaces; + line-height: 1.5; + padding-top: 3px; + padding-bottom: 2px; +} + +.imgedit-rotate-menu-container { + position: relative; +} + .imgedit-help.imgedit-restore { padding-bottom: 0; } @@ -1142,8 +1197,8 @@ border color while dragging a file over the uploader drop area */ .image-editor .imgedit-settings .imgedit-help-toggle:focus { color: #2271b1; - border-color: #4f94d4; - box-shadow: 0 0 3px rgba(34, 113, 177, 0.8); + border-color: #2271b1; + box-shadow: 0 0 0 1px #2271b1; /* Only visible in Windows High Contrast mode */ outline: 2px solid transparent; } @@ -1152,10 +1207,6 @@ border color while dragging a file over the uploader drop area */ padding: 0; } -.imgedit-submit { - margin: 8px 0 0; -} - .imgedit-submit-btn { margin-left: 20px; } @@ -1167,8 +1218,11 @@ border color while dragging a file over the uploader drop area */ } span.imgedit-scale-warn { - color: #d63638; - font-size: 20px; + display: flex; + align-items: center; + margin: 4px; + gap: 4px; + color: #b32d2e; font-style: normal; visibility: hidden; vertical-align: middle; @@ -1183,17 +1237,19 @@ span.imgedit-scale-warn { } .imgedit-group { - margin-bottom: 8px; - padding: 10px; + margin-bottom: 20px; } -.imgedit-settings .imgedit-original-dimensions { +.image-editor .imgedit-original-dimensions { display: inline-block; } -.imgedit-settings .imgedit-scale input[type="text"], -.imgedit-settings .imgedit-crop-ratio input[type="text"], -.imgedit-settings .imgedit-crop-sel input[type="text"] { +.image-editor .imgedit-scale-controls input[type="text"], +.image-editor .imgedit-crop-ratio input[type="text"], +.image-editor .imgedit-crop-sel input[type="text"], +.image-editor .imgedit-scale-controls input[type="number"], +.image-editor .imgedit-crop-ratio input[type="number"], +.image-editor .imgedit-crop-sel input[type="number"] { width: 80px; font-size: 14px; padding: 0 8px; @@ -1207,12 +1263,12 @@ span.imgedit-scale-warn { color: #3c434a; } -.imgedit-settings .imgedit-scale-button-wrapper { +.image-editor .imgedit-scale-button-wrapper { margin-top: 0.3077em; display: block; } -.imgedit-settings .imgedit-scale .button { +.image-editor .imgedit-scale-controls .button { margin-bottom: 0; } @@ -1241,7 +1297,6 @@ audio, video { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { .imgedit-wait:before { background-image: url(../images/spinner-2x.gif); @@ -1249,11 +1304,24 @@ audio, video { } @media screen and (max-width: 782px) { + .edit-attachment-frame input, + .edit-attachment-frame textarea { + line-height: 1.5; + } + .wp_attachment_details label[for="content"] { font-size: 14px; line-height: 1.5; } + .wp_attachment_details textarea { + line-height: 1.5; + } + + .wp_attachment_details #attachment_alt { + height: 3.375em; + } + .media-upload-form .media-item.error, .media-upload-form .media-item .error { font-size: 13px; @@ -1268,15 +1336,15 @@ audio, video { padding: 10px 0 10px 12px; } - .imgedit-settings .imgedit-scale input[type="text"], - .imgedit-settings .imgedit-crop-ratio input[type="text"], - .imgedit-settings .imgedit-crop-sel input[type="text"] { + .image-editor .imgedit-scale input[type="text"], + .image-editor .imgedit-crop-ratio input[type="text"], + .image-editor .imgedit-crop-sel input[type="text"] { font-size: 16px; padding: 6px 10px; } .wp_attachment_holder .imgedit-wrap .imgedit-panel-content, - .wp_attachment_holder .imgedit-wrap .imgedit-settings { + .wp_attachment_holder .imgedit-wrap .image-editor { float: none; width: auto; max-width: none; @@ -1286,6 +1354,25 @@ audio, video { .copy-to-clipboard-container .success { font-size: 14px; } + + /* Restructure image editor on narrow viewports. */ + .imgedit-crop-wrap img{ + width: 100%; + } + + .media-modal .imgedit-wrap .imgedit-panel-content, + .media-modal .imgedit-wrap .image-editor { + position: initial !important; + } + + .media-modal .imgedit-wrap .image-editor { + box-sizing: border-box; + width: 100% !important; + } + + .image-editor .imgedit-scale-button-wrapper { + display: inline-block; + } } @media only screen and (max-width: 600px) { @@ -1297,7 +1384,6 @@ audio, video { /** * Media queries for media grid. */ - @media only screen and (max-width: 1120px) { /* override for media-views.css */ #wp-media-grid .wp-filter .attachment-filters { @@ -1305,6 +1391,23 @@ audio, video { } } +@media only screen and (max-width: 1000px) { + /* override for forms.css */ + .wp-filter p.search-box { + float: none; + width: 100%; + margin-bottom: 20px; + display: flex; + flex-wrap: nowrap; + column-gap: 0; + } + + .wp-filter p.search-box #media-search-input { + width: 100%; + } + +} + @media only screen and (max-width: 782px) { .media-frame.mode-select .attachments-browser.fixed .media-toolbar { top: 46px; @@ -1364,3 +1467,17 @@ audio, video { max-width: 100%; } } + +@media only screen and (max-width: 375px) { + .media-item .attachment-tools { + align-items: baseline; + } + .media-item .edit-attachment.copy-to-clipboard-container { + flex-direction: column; + } + + .copy-to-clipboard-container .success { + line-height: normal; + margin-top: 10px; + } +} diff --git a/src/wp-admin/css/nav-menus.css b/src/wp-admin/css/nav-menus.css index 086ceafad807f..8e3eadaad53bc 100644 --- a/src/wp-admin/css/nav-menus.css +++ b/src/wp-admin/css/nav-menus.css @@ -21,6 +21,7 @@ ul.add-menu-item-tabs li { #nav-menu-meta .accordion-section-content { padding: 18px 13px; + resize: vertical; } #nav-menu-meta .button-controls { @@ -48,6 +49,26 @@ ul.add-menu-item-tabs li { #menu-settings-column .inside { clear: both; margin: 10px 0 0; + height: 100%; + max-height: inherit; +} + +#menu-settings-column .categorydiv, +#menu-settings-column .customlinkdiv, +#menu-settings-column .posttypediv, +#menu-settings-column .taxonomydiv { + max-height: inherit; + height: 100%; +} + +#menu-settings-column .wp-tab-panel, +#menu-settings-column .categorydiv div.tabs-panel, +#menu-settings-column .customlinkdiv div.tabs-panel, +#menu-settings-column .posttypediv div.tabs-panel, +#menu-settings-column .taxonomydiv div.tabs-panel { + /* Allow space for content after tab panels in nav menu editor. */ + max-height: calc( 100% - 75px ); + height: 100%; } .metabox-holder-disabled .postbox, @@ -103,7 +124,7 @@ ul.add-menu-item-tabs li { #nav-menu-bulk-actions-bottom { margin: 1em 0; - margin: calc( 1em + 9px ) 0 ; + margin: calc( 1em + 9px ) 0; } .bulk-actions input.button { @@ -169,12 +190,11 @@ label.bulk-select-button:focus-within { color: #0a4b78; } -input.bulk-select-switcher:focus + .bulk-select-button-label{ +input.bulk-select-switcher:focus + .bulk-select-button-label { color: #0a4b78; } .bulk-actions input.menu-items-delete { - -webkit-appearance: none; appearance: none; font-size: inherit; border: 0; @@ -191,9 +211,7 @@ input.bulk-select-switcher:focus + .bulk-select-button-label{ } .bulk-actions input.menu-items-delete.disabled { - cursor: default; - color: #a7aaad; - box-shadow: none; + display: none; } .menu-settings { @@ -203,7 +221,6 @@ input.bulk-select-switcher:focus + .bulk-select-button-label{ .menu-settings-group { margin: 0 0 10px; - overflow: hidden; padding-left: 20%; } @@ -358,8 +375,7 @@ input.bulk-select-switcher:focus + .bulk-select-button-label{ } /* Add Menu Item Boxes */ -.postbox .howto input, -.customlinkdiv .menu-item-textbox { +.postbox .howto input { width: 180px; float: right; } @@ -405,7 +421,6 @@ input.bulk-select-switcher:focus + .bulk-select-button-label{ /* Button Secondary Actions */ .list-controls { float: left; - margin-top: 5px; } .add-to-menu { @@ -456,7 +471,10 @@ input.bulk-select-switcher:focus + .bulk-select-button-label{ width: 180px; } -.customlinkdiv label, +.customlinkdiv .menu-item-textbox { + width: 100%; +} + .nav-menus-php .howto span { float: left; margin-top: 6px; @@ -706,7 +724,6 @@ body.menu-max-depth-11 { min-width: 1280px !important; } .no-js.nav-menus-php .item-edit .screen-reader-text { position: static; - -webkit-clip-path: none; clip-path: none; width: auto; height: auto; @@ -734,9 +751,9 @@ body.menu-max-depth-11 { min-width: 1280px !important; } } .nav-menus-php .item-edit:focus:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } /* Menu editing */ @@ -810,22 +827,13 @@ body.menu-max-depth-11 { min-width: 1280px !important; } display: none; } -.menu-item-settings .description-thin, -.menu-item-settings .description-wide { - margin-right: 10px; - float: left; -} - -.description-thin { - width: calc(50% - 5px); +.description-group { + display: flex; + column-gap: 10px; } -.menu-item-settings .description-thin + .description-thin { - margin-right: 0; -} - -.description-wide { - width: 100%; +.description-group > * { + flex-grow: 1; } .menu-item-actions { @@ -839,20 +847,13 @@ body.menu-max-depth-11 { min-width: 1280px !important; } /* Major/minor publishing actions (classes) */ .nav-menus-php .major-publishing-actions { - clear: both; padding: 10px 0; - line-height: 2.15384615; -} - -.nav-menus-php .major-publishing-actions .publishing-action { - text-align: right; - float: right; + display: flex; + align-items: center; } -/* Same as the Publish Meta Box #delete-action */ -.nav-menus-php .delete-action { - float: left; - line-height: 2.1; +.nav-menus-php .major-publishing-actions > * { + margin-right: 10px; } .nav-menus-php .major-publishing-actions .form-invalid { @@ -861,12 +862,21 @@ body.menu-max-depth-11 { min-width: 1280px !important; } } #nav-menus-frame, -.button-controls, #menu-item-url-wrap, #menu-item-name-wrap { display: block; } +.button-controls { + display: flex; + align-items: center; + justify-content: space-between; +} + +.button-controls-customlinkdiv { + justify-content: flex-end; +} + /* =Media Queries -------------------------------------------------------------- */ @@ -941,8 +951,7 @@ body.menu-max-depth-11 { min-width: 1280px !important; } } .menu-item-bar .menu-item-handle, - .menu-item-settings, - .description-wide { + .menu-item-settings { width: auto; } @@ -950,9 +959,8 @@ body.menu-max-depth-11 { min-width: 1280px !important; } padding: 10px; } - .menu-item-settings .description-thin, - .menu-item-settings .description-wide { - width: 100%; + .menu-item-settings .description-group { + display: block; } .menu-item-settings input { @@ -1000,7 +1008,7 @@ body.menu-max-depth-11 { min-width: 1280px !important; } @media only screen and (min-width: 783px) { @supports (position: sticky) and (scroll-margin-bottom: 130px) { - + #nav-menu-footer { position: sticky; bottom: 0; @@ -1019,4 +1027,8 @@ body.menu-max-depth-11 { min-width: 1280px !important; } #menu-locations-wrap .widefat { width: 100%; } + + .bulk-select-button { + padding: 5px 10px; + } } diff --git a/src/wp-admin/css/revisions.css b/src/wp-admin/css/revisions.css index e523ee431ce93..da238456178fc 100644 --- a/src/wp-admin/css/revisions.css +++ b/src/wp-admin/css/revisions.css @@ -309,7 +309,6 @@ table.diff .diff-addedline ins { float: right; margin-left: 6px; margin-right: 6px; - margin-top: 2px; } .diff-meta-from { @@ -458,8 +457,7 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { touch-action: none; } -.wp-slider .ui-slider-handle, -.wp-slider .ui-slider-handle.focus { +.wp-slider .ui-slider-handle { background: #f6f7f7; border: 1px solid #c3c4c7; box-shadow: 0 1px 0 #c3c4c7; @@ -479,6 +477,15 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { transform: translateY(1px); } +.wp-slider .ui-slider-handle:focus, +.wp-slider .ui-slider-handle.ui-state-focus { + background: #f0f0f1; + border-color: #8c8f94; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; +} + .wp-slider .ui-slider-handle:before { background: none; position: absolute; @@ -486,8 +493,8 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { left: 2px; color: #50575e; content: "\f229"; + content: "\f229" / ''; font: normal 18px/1 dashicons; - speak: never; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } @@ -505,18 +512,22 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { .wp-slider .ui-slider-handle.from-handle:before { content: "\f139"; + content: "\f139" / ''; } .wp-slider .ui-slider-handle.to-handle:before { content: "\f141"; + content: "\f141" / ''; } .rtl .wp-slider .ui-slider-handle.from-handle:before { content: "\f141"; + content: "\f141" / ''; } .rtl .wp-slider .ui-slider-handle.to-handle:before { content: "\f139"; + content: "\f139" / ''; right: -1px; } @@ -558,13 +569,34 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { .revision-tick.completed-false { background-image: url(../images/spinner-2x.gif); } } +@media screen and (max-width: 600px) { + .revisions-meta .author-card:not(.comparing-two-revisions .author-card) { + display: flex; + flex-direction: column; + width: fit-content; + gap: 16px; + } + + .comparing-two-revisions .revisions-meta .restore-revision { + margin-top: 16px; + } + + .revisions-controls { + padding-top: 0; + } + + .revision-toggle-compare-mode { + position: relative; + padding: 1rem 0; + } +} + @media screen and (max-width: 782px) { #diff-next-revision, #diff-previous-revision { @@ -578,14 +610,18 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { .revisions-controls, .comparing-two-revisions .revisions-controls { - height: 170px; + height: fit-content; } .revisions-tooltip { - bottom: 130px; + bottom: 155px; z-index: 2; } + .comparing-two-revisions .revisions-tooltip { + bottom: 200px; + } + .diff-meta { overflow: hidden; } @@ -595,8 +631,4 @@ div.revisions-controls > .wp-slider > .ui-slider-handle { word-break: break-all; word-wrap: break-word; } - - .diff-meta input.restore-revision { - margin-top: 0; - } } diff --git a/src/wp-admin/css/site-health.css b/src/wp-admin/css/site-health.css index ed539d33ae279..cf6c24e4a955c 100644 --- a/src/wp-admin/css/site-health.css +++ b/src/wp-admin/css/site-health.css @@ -11,10 +11,6 @@ are styled in the Privacy section of edit.css */ font-weight: 400; } -.health-check-widget-title-section { - text-align: center; -} - .site-health-progress-wrapper { margin-bottom: 1rem; } @@ -87,7 +83,7 @@ are styled in the Privacy section of edit.css */ stroke: #c3c4c7; } 50% { - stroke: #72aee6; + stroke: var(--wp-admin-theme-color); } 100% { stroke: #c3c4c7; @@ -170,6 +166,10 @@ are styled in the Privacy section of edit.css */ margin: 0 auto; } +.widefat.health-check-table th { + font-size: 13px; +} + .health-check-table td:first-child { width: 30%; } @@ -190,22 +190,26 @@ are styled in the Privacy section of edit.css */ .health-check-body .pass::before, .health-check-body .good::before { content: "\f147"; + content: "\f147" / ''; color: #00a32a; } .health-check-body .warning::before { content: "\f460"; + content: "\f460" / ''; color: #dba617; } .health-check-body .info::before { content: "\f348"; + content: "\f348" / ''; color: #72aee6; } .health-check-body .fail::before, .health-check-body .error::before { content: "\f335"; + content: "\f335" / ''; color: #d63638; } @@ -220,7 +224,7 @@ are styled in the Privacy section of edit.css */ } .site-health-copy-buttons .success { - color: #008a20; + color: #007017; margin-left: 0.5rem; } @@ -284,17 +288,6 @@ are styled in the Privacy section of edit.css */ padding-left: 20px; } - -/* Better position for the WordPress admin notices and update nag. */ -.site-health .notice { - margin: 5px 20px 15px 22px; -} - -.site-health .update-nag { - margin-bottom: 20px; - margin-left: 22px; -} - .health-check-wp-paths-sizes.spinner { visibility: visible; float: none; @@ -307,6 +300,14 @@ are styled in the Privacy section of edit.css */ padding-left: 16px; } +#dashboard_site_health .site-health-details p:first-child { + margin-top: 0; +} + +#dashboard_site_health .site-health-details p:last-child { + margin-bottom: 0; +} + #dashboard_site_health .health-check-widget { display: grid; grid-template-columns: 1fr 2fr; @@ -318,6 +319,11 @@ are styled in the Privacy section of edit.css */ margin-left: 0; } +.health-check-widget-title-section { + margin-bottom: 0; + text-align: center; +} + @media screen and (max-width: 480px) { #dashboard_site_health .health-check-widget { grid-template-columns: 100%; @@ -339,6 +345,7 @@ are styled in the Privacy section of edit.css */ table-layout: fixed; } + .health-check-table th, .health-check-table td { box-sizing: border-box; display: block; @@ -346,6 +353,7 @@ are styled in the Privacy section of edit.css */ word-wrap: break-word; } + .widefat.health-check-table th, .health-check-table td:first-child { width: 100%; padding-bottom: 0; diff --git a/src/wp-admin/css/site-icon.css b/src/wp-admin/css/site-icon.css index eae9a576357a8..93f36f093caf9 100644 --- a/src/wp-admin/css/site-icon.css +++ b/src/wp-admin/css/site-icon.css @@ -2,53 +2,200 @@ 28.0 - Site Icon ------------------------------------------------------------------------------*/ -.site-icon-preview .favicon-preview { - margin: 5px 0 20px; - overflow: hidden; - position: relative; - max-width: 180px; +.site-icon-section { + --site-icon-removal: #b32d2e; } -.site-icon-preview .favicon, -.site-icon-preview .browser-title { - height: 16px; - left: 88px; +.site-icon-preview { + --site-icon-input-border: #8c8f94; + --site-icon-preview-background: #fff; + --site-icon-preview-browser-top: #dcdcde; + --site-icon-preview-browser-bottom: #a7aaad; + --site-icon-preview-browser-border: rgba(255, 255, 255, 0.2); + --site-icon-address-bar-background: #f0f0f1; + --site-icon-address-bar-close: #646970; + --site-icon-address-bar-text: #3c434a; + --site-icon-shadow-1: rgba(0, 0, 0, 0.1); + --site-icon-shadow-2: rgba(0, 0, 0, 0.2); + --site-icon-shadow-3: rgba(0, 0, 0, 0.5); + + direction: initial; + display: flex; + height: 60px; + padding: 8px 0 0 8px; + align-items: flex-start; + position: relative; overflow: hidden; + box-sizing: border-box; + border: 1px solid var(--site-icon-input-border); + border-radius: 4px; + background-color: var(--site-icon-preview-background); + width: 275px; +} + +@media (prefers-color-scheme: dark) { + .site-icon-preview { + --site-icon-preview-browser-top: #2c3338; + --site-icon-preview-browser-bottom: #111; + --site-icon-address-bar-background: #3c434a; + --site-icon-address-bar-close: #f0f0f1; + --site-icon-address-bar-text: #f0f0f1; + } +} + +.site-icon-preview.settings { + height: 88px; + padding: 16px 0 0 16px; + width: 350px; + margin: 0 0 16px 0; +} + +.site-icon-preview.crop { + width: 258px; + height: 100%; + display: grid; + grid-template-columns: 8px 1fr; + grid-template-rows: 64px 1fr; + padding-left: 0; + row-gap: 16px; + direction: inherit; +} + +.site-icon-preview.hidden { + display: none; +} + +.site-icon-preview .direction-wrap { + grid-template-columns: 44px 1fr; + gap: 8px; + display: grid; + direction: ltr; + height: 100%; + width: 100%; +} + +.site-icon-preview.settings .direction-wrap { + grid-template-columns: 58px 1fr; + gap: 16px; +} + +.site-icon-preview:after { + --after-size: 150%; + aspect-ratio: 1/1; + content: ""; + display: block; position: absolute; - top: 16px; + top: 0; + left: 0; + width: var(--after-size);; + transform: translate(calc(var(--after-size) * -0.125), calc(var(--after-size) * -0.125)); + filter: blur(5px); + opacity: 0.5; + background: var(--site-icon-url); +} + +.site-icon-preview .app-icon-preview { + aspect-ratio: 1/1; + border-radius: 10px; + box-shadow: 0 1px 5px 0 var(--site-icon-shadow-3); + flex-shrink: 0; + width: 100%; + z-index: 1; } -.site-icon-preview .favicon { - width: 16px; +.site-icon-preview-browser { + display: flex; + padding: 4px 4px 0 12px; + align-items: flex-start; + gap: 16px; + flex: 1 0 0; + z-index: 1; + border-top-left-radius: 10px; + border-top: 1px solid var(--site-icon-preview-browser-border); + border-left: 1px solid var(--site-icon-preview-browser-border); + background: linear-gradient(180deg, var(--site-icon-preview-browser-top) 0%, var(--site-icon-preview-browser-bottom) 100%); + box-shadow: 0 10px 22px 0 var(--site-icon-shadow-2); } -.site-icon-preview .browser-title { - left: 109px; - width: 72px; +.site-icon-preview .browser-buttons { + width: 48px; + height: 40px; + fill: var(--site-icon-input-border); +} + +.site-icon-preview-tab { + padding: 8px; + align-items: center; + gap: 8px; + flex: 1 0 0; + border-radius: 4px; + background-color: var(--site-icon-address-bar-background); + box-shadow: 0 1px 3px 0 var(--site-icon-shadow-1); + display: grid; + grid-template-columns: 24px auto 24px; +} + +.site-icon-preview-browser .browser-icon-preview { + box-shadow: 0 0 20px 0 var(--site-icon-shadow-1); +} + +.site-icon-preview-tab > img, +.site-icon-preview-tab > svg { + width: 24px; + height: 24px; +} + +.site-icon-preview-tab > svg { + fill: var(--site-icon-address-bar-close); +} + +.site-icon-preview-site-title { + color: var(--site-icon-address-bar-text); + text-overflow: ellipsis; white-space: nowrap; + overflow: hidden; + font-weight: 500; } -.site-icon-preview .app-icon-preview { - background-color: #000; - border-radius: 16px; +.site-icon-preview-crop-modal .image-preview-wrap.app-icon-preview { + width: 64px; height: 64px; + margin: 0; + grid-column: 2; +} + +.site-icon-preview-crop-modal .site-icon-preview-browser { + grid-column: 2; +} + +.site-icon-preview-crop-modal .image-preview-wrap { overflow: hidden; - width: 64px; - margin-top: 5px; + aspect-ratio: 1/1; } -/* rtl:ignore */ -.site-icon-preview .favicon, -.site-icon-preview .app-icon-preview { - direction: ltr; +.site-icon-preview-crop-modal .image-preview-wrap.browser { + width: 24px; + height: 24px; +} + +button.reset.remove-site-icon { + color: var(--site-icon-removal); + text-decoration: none; + border-color: currentColor; + box-shadow: none; + background: transparent; } -.customize-control-site_icon .favicon-preview { - float: left; - margin-right: 12px; - margin-bottom: 0; +button.reset.remove-site-icon:focus, +button.reset.remove-site-icon:hover { + background: var(--site-icon-removal); + color: #fff; + border-color: var(--site-icon-removal); + box-shadow: 0 0 0 1px var(--site-icon-removal); } -.customize-control-site_icon .app-icon-preview { - margin-top: 9px; +.site-icon-action-buttons { + display: flex; + flex-wrap: wrap; + gap: 10px; } diff --git a/src/wp-admin/css/themes.css b/src/wp-admin/css/themes.css index 04bfdd00f7f7d..4a644974c50c4 100644 --- a/src/wp-admin/css/themes.css +++ b/src/wp-admin/css/themes.css @@ -7,6 +7,14 @@ 16.1 - Manage Themes ------------------------------------------------------------------------------*/ +.themes-php { + overflow-y: scroll; +} + +.themes-php #adminmenuwrap { + z-index: 10001; /* above Theme Overlay */ +} + body.js .theme-browser.search-loading { display: none; } @@ -15,23 +23,23 @@ body.js .theme-browser.search-loading { clear: both; } -.themes-php:not(.network-admin) .wrap h1 { - margin-bottom: 15px; -} - .themes-php .wrap h1 .button { margin-left: 20px; } /* Search form */ .themes-php .search-form { - display: inline; + display: inline-flex; + align-items: center; + position: relative; + top: 0; + gap: .5rem; + width: 100%; + justify-content: end; } .themes-php .wp-filter-search { position: relative; - top: -2px; - left: 20px; margin: 0; width: 280px; } @@ -56,9 +64,11 @@ body.js .theme-browser.search-loading { margin: 0 4% 4% 0; position: relative; width: 30.6%; - border: 1px solid #dcdcde; - box-shadow: 0 1px 1px -1px rgba(0, 0, 0, 0.1); + background: #ffffff; + border: 1px solid rgb(0, 0, 0, 0.1); + border-radius: 8px; box-sizing: border-box; + overflow: hidden; } .theme-browser .theme:nth-child(3n) { @@ -75,13 +85,12 @@ body.js .theme-browser.search-loading { font-weight: 600; height: 18px; margin: 0; - padding: 15px; - box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); + padding: 16px 15px; + border-top: 1px solid rgb(0, 0, 0, 0.1); overflow: hidden; white-space: nowrap; text-overflow: ellipsis; - background: #fff; - background: rgba(255, 255, 255, 0.65); + background: #ffffff; } /* Activate and Customize buttons, shown on hover and focus */ @@ -104,9 +113,36 @@ body.js .theme-browser.search-loading { margin-right: 3px; } +/* Use compact size for space-constrained theme cards */ +.theme-browser .theme .theme-actions .button.updated-message, +.theme-browser .theme .theme-actions .button.updating-message, .theme-browser .theme .theme-actions .button { float: none; margin-left: 3px; + min-height: 32px; + line-height: 2.30769231; /* 30px for 32px min-height */ + padding: 0 12px; +} + +.theme-browser .theme .theme-actions .button.updated-message::before, +.theme-browser .theme .theme-actions .button.updating-message::before { + line-height: 1; + vertical-align: text-bottom; + position: relative; + top: 2px; +} + +/* Secondary buttons need white background for visibility on semi-transparent overlay */ +.theme-browser .theme .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme .theme-actions .button:not(.button-primary):focus { + background: #fff; } /** @@ -168,15 +204,18 @@ body.js .theme-browser.search-loading { border-radius: 3px; border: none; transition: opacity 0.1s ease-in-out; + cursor: pointer; } .theme-browser .theme .more-details:focus { - box-shadow: 0 0 0 1px #fff, 0 0 0 3px #2271b1; + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); } .theme-browser .theme.focus { - border-color: #4f94d4; - box-shadow: 0 0 2px rgba(79, 148, 212, 0.8); + border-color: var(--wp-admin-theme-color); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .theme-browser .theme.focus .more-details { @@ -200,7 +239,7 @@ body.js .theme-browser.search-loading { .theme-browser .theme.active .theme-name { background: #1d2327; color: #fff; - padding-right: 110px; + padding-right: 115px; font-weight: 300; box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.5); } @@ -214,11 +253,15 @@ body.js .theme-browser.search-loading { } .theme-browser .theme.active .theme-actions { - background: rgba(44, 51, 56, 0.7); + background: transparent; border-left: none; opacity: 1; } +.theme-browser .theme.active .theme-actions .button-primary { + border-color: #fff; +} + .theme-id-container { position: relative; } @@ -229,7 +272,10 @@ body.js .theme-browser.search-loading { top: 50%; transform: translateY(-50%); right: 0; - padding: 9px 15px; + padding: 9px 12px; +} + +.theme-browser .theme:not(.active) .theme-actions { box-shadow: inset 0 1px 0 rgba(0, 0, 0, 0.1); } @@ -237,6 +283,19 @@ body.js .theme-browser.search-loading { margin-right: 0; } +/* Active theme secondary buttons need white background for visibility on dark overlay */ +.theme-browser .theme.active .theme-actions .button:not(.button-primary) { + background: #fff; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):hover { + background: #f0f0f0; +} + +.theme-browser .theme.active .theme-actions .button:not(.button-primary):focus { + background: #fff; +} + .theme-browser .theme .theme-author { background: #1d2327; color: #f0f0f1; @@ -260,8 +319,9 @@ body.js .theme-browser.search-loading { * Add new theme */ .theme-browser .theme.add-new-theme { + background: transparent; border: none; - box-shadow: none; + overflow: visible; } .theme-browser .theme.add-new-theme a { @@ -294,6 +354,7 @@ body.js .theme-browser.search-loading { border-radius: 50%; display: inline-block; content: "\f132"; + content: "\f132" / ''; -webkit-font-smoothing: antialiased; font: normal 74px/115px dashicons; width: 100px; @@ -336,8 +397,8 @@ body.js .theme-browser.search-loading { .theme-browser .theme.add-new-theme .theme-name { background: none; + border: none; text-align: center; - box-shadow: none; font-weight: 400; position: relative; top: 0; @@ -365,6 +426,7 @@ body.js .theme-browser.search-loading { background: #f0f0f1; background: rgba(240, 240, 241, 0.9); z-index: 10000; /* Over WP Pointers. */ + min-height: calc(100vh - var(--wp-admin--admin-bar--height, 32px)); } .theme-overlay .theme-header { @@ -397,6 +459,7 @@ body.js .theme-browser.search-loading { color: #787c82; display: inline-block; content: "\f335"; + content: "\f335" / ''; font-weight: 300; } @@ -456,10 +519,12 @@ body.js .theme-browser.search-loading { .theme-overlay .theme-header .left:before { content: "\f341"; + content: "\f341" / ''; } .theme-overlay .theme-header .right:before { content: "\f345"; + content: "\f345" / ''; } .theme-overlay .theme-wrap { @@ -501,10 +566,12 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { z-index: 30; box-sizing: border-box; border-top: 1px solid #f0f0f1; + display: flex; + justify-content: center; + gap: 5px; } -.theme-overlay .theme-actions a { - margin-right: 5px; +.theme-overlay .theme-actions .button { margin-bottom: 5px; } @@ -516,26 +583,21 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { .broken-themes a.delete-theme, .theme-overlay .theme-actions .delete-theme { - color: #d63638; + color: #b32d2e; text-decoration: none; border-color: transparent; box-shadow: none; background: transparent; } -.theme-overlay .theme-actions .delete-theme { - position: absolute; - right: 10px; - bottom: 5px; -} - .broken-themes a.delete-theme:hover, .broken-themes a.delete-theme:focus, .theme-overlay .theme-actions .delete-theme:hover, .theme-overlay .theme-actions .delete-theme:focus { - background: #d63638; + background: #b32d2e; color: #fff; - border-color: #d63638; + border-color: #b32d2e; + box-shadow: 0 0 0 1px #b32d2e; } .theme-overlay .theme-actions .active-theme, @@ -835,7 +897,7 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { } } -@media only screen and (max-width: 780px) { +@media only screen and (max-width: 782px) { body.folded .theme-overlay .theme-wrap, .theme-overlay .theme-wrap { top: 0; /* The adminmenu isn't fixed on mobile, so this can use the full viewport height */ @@ -911,6 +973,10 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { padding-left: 4%; padding-right: 4%; } + + .theme-install-php .wp-filter .filter-count { + margin-top: 10px; + } } @media only screen and (max-width: 650px) { @@ -931,6 +997,7 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { .theme-overlay .theme-screenshots { width: 100%; float: none; + margin: 0; } .theme-overlay .theme-info { @@ -947,13 +1014,12 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { } .themes-php .wp-filter-search { - float: none; - clear: both; - left: 0; - right: 0; - margin: -5px 0 20px; width: 100%; - max-width: 280px; + } + + .theme-install-php .wp-filter p.search-box { + display: grid; + row-gap: .5rem; } .theme-browser .theme.add-new-theme span:after { @@ -1010,6 +1076,7 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { .theme-browser .theme .notice-success p:before { color: #68de7c; content: "\f147"; + content: "\f147" / ''; display: inline-block; font: normal 20px/1 'dashicons'; -webkit-font-smoothing: antialiased; @@ -1025,6 +1092,13 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { padding-left: 20px; } +/* Override column gap adjustment in media library. */ +@media only screen and (max-width: 1000px) { + .theme-install-php .wp-filter p.search-box { + column-gap: .5rem; + } +} + .theme-install-php a.upload, .theme-install-php a.browse-themes { cursor: pointer; @@ -1061,18 +1135,24 @@ body.folded .theme-browser ~ .theme-overlay .theme-wrap { .upload-theme .wp-upload-form, .upload-plugin .wp-upload-form { - background: #f6f7f7; - border: 1px solid #c3c4c7; - padding: 30px; - margin: 30px auto; + position: relative; + margin: 30px; display: inline-flex; justify-content: space-between; align-items: center; + border: 1px solid #c3c4c7; + background: #f6f7f7; } .upload-theme .wp-upload-form input[type="file"], .upload-plugin .wp-upload-form input[type="file"] { - margin-right: 10px; + background: transparent; + margin: 0; + padding: 30px 0 30px 30px; +} + +.wp-upload-form input[type="submit"].button { + margin-right: 30px; } .upload-theme .install-help, @@ -1106,6 +1186,7 @@ p.no-themes-local { } @media only screen and (max-width: 1120px) { + .upload-plugin .wp-upload-form, .upload-theme .wp-upload-form { margin: 20px 0; max-width: 100%; @@ -1323,10 +1404,6 @@ div#custom-background-image img { transform: rotate( 45deg ); } -.background-position-control .button-group .dashicons { - margin-top: 9px; -} - .background-position-control .button-group + .button-group { margin-top: -1px; } @@ -1442,7 +1519,7 @@ body.full-overlay-active { } .wp-full-overlay-sidebar .wp-full-overlay-header a.back { - margin-top: 9px; + margin-top: 3px; /* Vertically center 40px button in 45px header */ } .wp-full-overlay-sidebar .wp-full-overlay-footer { @@ -1498,6 +1575,7 @@ body.full-overlay-active { .theme-install-overlay .close-full-overlay:before { font: normal 22px/1 dashicons; content: "\f335"; + content: "\f335" / ''; position: relative; top: 7px; left: 13px; @@ -1506,6 +1584,7 @@ body.full-overlay-active { .theme-install-overlay .previous-theme:before { font: normal 20px/1 dashicons; content: "\f341"; + content: "\f341" / ''; position: relative; top: 6px; left: 14px; @@ -1514,6 +1593,7 @@ body.full-overlay-active { .theme-install-overlay .next-theme:before { font: normal 20px/1 dashicons; content: "\f345"; + content: "\f345" / ''; position: relative; top: 6px; left: 13px; @@ -1584,9 +1664,9 @@ body.full-overlay-active { .wp-full-overlay .collapse-sidebar:hover .collapse-sidebar-arrow, .wp-full-overlay .collapse-sidebar:focus .collapse-sidebar-arrow { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .wp-full-overlay .collapse-sidebar-label { @@ -1600,9 +1680,9 @@ body.full-overlay-active { .wp-full-overlay .collapse-sidebar-arrow:before { display: block; content: "\f148"; + content: "\f148" / ''; background: #f0f0f1; font: normal 20px/1 dashicons; - speak: never; padding: 0; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; @@ -1623,12 +1703,14 @@ body.full-overlay-active { } /* Animations */ -.wp-full-overlay, -.wp-full-overlay-sidebar, -.wp-full-overlay .collapse-sidebar, -.wp-full-overlay-main { - transition-property: left, right, top, bottom, width, margin; - transition-duration: 0.2s; +@media (prefers-reduced-motion: no-preference) { + .wp-full-overlay, + .wp-full-overlay-sidebar, + .wp-full-overlay .collapse-sidebar, + .wp-full-overlay-main { + transition-property: left, right, top, bottom, width, margin; + transition-duration: 0.2s; + } } /* Device/preview size toggles */ @@ -1720,14 +1802,17 @@ body.full-overlay-active { .wp-full-overlay-footer .devices .preview-desktop:before { content: "\f472"; + content: "\f472" / ''; } .wp-full-overlay-footer .devices .preview-tablet:before { content: "\f471"; + content: "\f471" / ''; } .wp-full-overlay-footer .devices .preview-mobile:before { content: "\f470"; + content: "\f470" / ''; } @media screen and (max-width: 1024px) { @@ -1847,6 +1932,22 @@ body.full-overlay-active { margin: 15px 0; width: 258px; border: 1px solid #c3c4c7; + position: relative; + overflow: hidden; +} + +.install-theme-info .theme-screenshot > img { + width: 100%; + height: auto; + position: absolute; + left: 0; + top: 0; +} + +.install-theme-info .theme-screenshot:after { + content: ""; + display: block; + padding-top: 66.66666666%; } .install-theme-info .theme-details { @@ -1866,7 +1967,7 @@ body.full-overlay-active { .theme-install-overlay .wp-full-overlay-header .button { float: right; - margin: 8px 10px 0 0; + margin: 7px 10px 0 0; /* Vertically center 32px button in 45px header */ } .theme-install-overlay .wp-full-overlay-sidebar { @@ -1920,7 +2021,6 @@ body.full-overlay-active { * HiDPI Displays */ @media print, - (-webkit-min-device-pixel-ratio: 1.25), (min-resolution: 120dpi) { .wp-full-overlay .collapse-sidebar-arrow { background-image: url(../images/arrows-2x.png); @@ -1953,8 +2053,6 @@ body.full-overlay-active { .theme-install-overlay .wp-full-overlay-header .button { font-size: 13px; - line-height: 2.15384615; - min-height: 30px; } .theme-browser .theme .theme-actions .button { @@ -1967,17 +2065,15 @@ body.full-overlay-active { padding-bottom: 4px; } - .upload-theme .wp-upload-form, - .upload-plugin .wp-upload-form { - display: block; + .upload-plugin .wp-upload-form, + .upload-theme .wp-upload-form { + width: 100%; + box-sizing: border-box; } -} -@media aural { - .theme .notice:before, - .theme-info .updating-message:before, - .theme-info .updated-message:before, - .theme-install.updating-message:before { - speak: never; + .upload-plugin .wp-upload-form input[type=file], + .upload-theme .wp-upload-form input[type=file] { + padding: 30px 0 30px 30px; + width: 100%; } } diff --git a/src/wp-admin/css/view-transitions.css b/src/wp-admin/css/view-transitions.css new file mode 100644 index 0000000000000..538a90b502f9e --- /dev/null +++ b/src/wp-admin/css/view-transitions.css @@ -0,0 +1,9 @@ +@media (prefers-reduced-motion: no-preference) { + @view-transition { + navigation: auto; + } + + #adminmenu > .menu-top { + view-transition-name: attr(id type(), none); + } +} diff --git a/src/wp-admin/css/widgets.css b/src/wp-admin/css/widgets.css index 9d64f979702b4..99039d3c7c66a 100644 --- a/src/wp-admin/css/widgets.css +++ b/src/wp-admin/css/widgets.css @@ -336,9 +336,9 @@ } .sidebar-name .handlediv:focus .toggle-indicator:before { - box-shadow: - 0 0 0 1px #4f94d4, - 0 0 2px 1px rgba(79, 148, 212, 0.8); + box-shadow: 0 0 0 var(--wp-admin-border-width-focus, 1.5px) var(--wp-admin-theme-color, #3858e9); + /* Only visible in Windows High Contrast mode */ + outline: 2px solid transparent; } .sidebar-name h2, @@ -346,7 +346,8 @@ margin: 0; padding: 8px 10px; overflow: hidden; - white-space: nowrap; + white-space: normal; + line-height: 1.5; } .widgets-holder-wrap .description { @@ -422,7 +423,6 @@ div#widgets-left .widget-holder { word-wrap: break-word; -ms-word-break: break-all; word-break: break-word; - -webkit-hyphens: auto; hyphens: auto; } @@ -481,7 +481,7 @@ div#widgets-right .sidebar-description { div#widgets-right .sidebar-name h2, div#widgets-right .sidebar-name h3 { - padding: 15px 7px; + padding: 15px 15px 15px 7px; } div#widgets-right .widget-top { @@ -508,6 +508,12 @@ div#widgets-right .closed .widgets-sortables { margin: -5px 5px; } +.sidebar-name .spinner { + position: absolute; + top: 18px; + right: 30px; +} + /* Dragging a widget over a closed sidebar */ #widgets-right .widgets-holder-wrap.widget-hover { border-color: #787c82; @@ -576,6 +582,12 @@ div#widgets-right .closed .widgets-sortables { padding: 0 15px; } +.widget-control-actions { + display: flex; + align-items: center; + justify-content: space-between; +} + .editwidget .widget-control-actions { margin-top: 20px; } @@ -718,12 +730,13 @@ div#widgets-right .widget-top:hover, } .widgets-chooser .widgets-chooser-selected .widgets-chooser-button { - background: #2271b1; + background: var(--wp-admin-theme-color, #3858e9); color: #fff; } .widgets-chooser .widgets-chooser-selected:before { content: "\f147"; + content: "\f147" / ''; display: block; -webkit-font-smoothing: antialiased; font: normal 26px/1 dashicons; diff --git a/src/wp-admin/custom-background.php b/src/wp-admin/custom-background.php index 37b8c3d8b8403..bbd56bdb6cee2 100644 --- a/src/wp-admin/custom-background.php +++ b/src/wp-admin/custom-background.php @@ -9,6 +9,11 @@ * @subpackage Administration */ +// Don't load directly. +if ( ! defined( 'ABSPATH' ) ) { + die( '-1' ); +} + _deprecated_file( basename( __FILE__ ), '5.3.0', 'wp-admin/includes/class-custom-background.php' ); /** Custom_Background class */ diff --git a/src/wp-admin/custom-header.php b/src/wp-admin/custom-header.php index d89f03bbaab2a..31c78dcb5b372 100644 --- a/src/wp-admin/custom-header.php +++ b/src/wp-admin/custom-header.php @@ -9,6 +9,11 @@ * @subpackage Administration */ +// Don't load directly. +if ( ! defined( 'ABSPATH' ) ) { + die( '-1' ); +} + _deprecated_file( basename( __FILE__ ), '5.3.0', 'wp-admin/includes/class-custom-image-header.php' ); /** Custom_Image_Header class */ diff --git a/src/wp-admin/customize.php b/src/wp-admin/customize.php index 0dd548d43a380..257dbbe0d60a8 100644 --- a/src/wp-admin/customize.php +++ b/src/wp-admin/customize.php @@ -62,7 +62,7 @@ ?> ID ), array( 'publish', 'trash' ), true ) ) { wp_die( - '

    ' . __( 'Something went wrong.' ) . '

    ' . - '

    ' . __( 'This changeset cannot be further modified.' ) . '

    ' . + '

    ' . __( 'An error occurred while saving your changeset.' ) . '

    ' . + '

    ' . __( 'Please try again or start a new changeset. This changeset cannot be further modified.' ) . '

    ' . '

    ' . __( 'Customize New Changes' ) . '

    ', 403 ); } } +$url = ! empty( $_REQUEST['url'] ) ? esc_url_raw( wp_unslash( $_REQUEST['url'] ) ) : ''; +$return = ! empty( $_REQUEST['return'] ) ? esc_url_raw( wp_unslash( $_REQUEST['return'] ) ) : ''; +$autofocus = ! empty( $_REQUEST['autofocus'] ) && is_array( $_REQUEST['autofocus'] ) + ? array_map( 'sanitize_text_field', wp_unslash( $_REQUEST['autofocus'] ) ) + : array(); -wp_reset_vars( array( 'url', 'return', 'autofocus' ) ); if ( ! empty( $url ) ) { - $wp_customize->set_preview_url( wp_unslash( $url ) ); + $wp_customize->set_preview_url( $url ); } if ( ! empty( $return ) ) { - $wp_customize->set_return_url( wp_unslash( $return ) ); + $wp_customize->set_return_url( $return ); } -if ( ! empty( $autofocus ) && is_array( $autofocus ) ) { - $wp_customize->set_autofocus( wp_unslash( $autofocus ) ); +if ( ! empty( $autofocus ) ) { + $wp_customize->set_autofocus( $autofocus ); } +// Let's roll. +header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); + +wp_user_settings(); +_wp_admin_html_begin(); + $registered = $wp_scripts->registered; -$wp_scripts = new WP_Scripts; +$wp_scripts = new WP_Scripts(); $wp_scripts->registered = $registered; add_action( 'customize_controls_print_scripts', 'print_head_scripts', 20 ); @@ -116,18 +126,12 @@ wp_enqueue_style( 'customize-controls' ); /** - * Enqueue Customizer control scripts. + * Fires when enqueuing Customizer control scripts. * * @since 3.4.0 */ do_action( 'customize_controls_enqueue_scripts' ); -// Let's roll. -header( 'Content-Type: ' . get_option( 'html_type' ) . '; charset=' . get_option( 'blog_charset' ) ); - -wp_user_settings(); -_wp_admin_html_begin(); - $body_class = 'wp-core-ui wp-customizer js'; if ( wp_is_mobile() ) : @@ -143,6 +147,8 @@ $body_class .= ' rtl'; } $body_class .= ' locale-' . sanitize_html_class( strtolower( str_replace( '_', '-', get_user_locale() ) ) ); +$admin_color = get_user_option( 'admin_color' ); +$body_class .= ' admin-color-' . sanitize_html_class( is_string( $admin_color ) ? $admin_color : '', 'modern' ); if ( wp_use_widgets_block_editor() ) { $body_class .= ' wp-embed-responsive'; @@ -153,8 +159,8 @@ ?> <?php echo esc_html( $admin_title ); ?> - @@ -188,34 +194,31 @@ theme()->get( 'RequiresWP' ) ); $compatible_php = is_php_version_compatible( $wp_customize->theme()->get( 'RequiresPHP' ) ); - $fse_safe = true; - - // Check if the theme requires the Gutenberg plugin to work correctly. - $theme_tags = $wp_customize->theme()->get( 'Tags' ); - - if ( ! empty( $theme_tags ) && in_array( 'full-site-editing', $theme_tags, true ) && ! function_exists( 'gutenberg_is_fse_theme' ) ) { - $fse_safe = false; - } ?> - + is_theme_active() ? __( 'Publish' ) : __( 'Activate & Publish' ); ?>
    - - + +
    - +
    - + + +
    @@ -230,20 +233,32 @@
      -
      +
      - +

      ' . get_bloginfo( 'name', 'display' ) . '' ); ?> - - +

      +
      - +

      + +

      +

      + Documentation on Customizer' ); + ?> +

      @@ -255,7 +270,7 @@ $_content_editor_dfw, 'drag_drop_upload' => true, - 'tabfocus_elements' => 'content-html,save-post', 'editor_height' => 300, 'tinymce' => array( - 'resize' => false, - 'wp_autoresize_on' => $_wp_editor_expand, - 'add_unload_trigger' => false, - 'wp_keep_scroll_position' => ! $is_IE, + 'resize' => false, + 'wp_autoresize_on' => $_wp_editor_expand, + 'add_unload_trigger' => false, ), ) ); ?> -
      +
      + - - + + - - + + - - + + - - + + - - + + - - + + - - + - - + + + + + want your site home page to be different from your WordPress installation directory.' ), - __( 'https://wordpress.org/support/article/giving-wordpress-its-own-directory/' ) + __( 'Enter the same address here unless you want your site home page to be different from your WordPress installation directory.' ), + __( 'https://developer.wordpress.org/advanced-administration/server/wordpress-in-directory/' ) ); ?>

      @@ -111,36 +264,38 @@ - - + - - + @@ -149,7 +304,28 @@ @@ -206,13 +382,13 @@ $check_zone_info = true; // Remove old Etc mappings. Fallback to gmt_offset. -if ( false !== strpos( $tzstring, 'Etc/GMT' ) ) { +if ( str_contains( $tzstring, 'Etc/GMT' ) ) { $tzstring = ''; } if ( empty( $tzstring ) ) { // Create a UTC+- zone if no timezone string exists. $check_zone_info = false; - if ( 0 == $current_offset ) { + if ( 0 === (int) $current_offset ) { $tzstring = 'UTC+0'; } elseif ( $current_offset < 0 ) { $tzstring = 'UTC' . $current_offset; @@ -277,7 +453,7 @@ ?>
      ' . wp_date( __( 'F j, Y' ) . ' ' . __( 'g:i a' ), $transitions[1]['ts'] ) . '' + /* translators: Localized date and time format, see https://www.php.net/manual/datetime.format.php */ + '' . wp_date( __( 'F j, Y g:i a' ), $transitions[1]['ts'] ) . '' ); } else { _e( 'This timezone does not observe daylight saving time.' ); @@ -301,22 +478,24 @@

      - + + - + + + - + @@ -390,7 +583,7 @@ global $wp_locale; for ( $day_index = 0; $day_index <= 6; $day_index++ ) : - $selected = ( get_option( 'start_of_week' ) == $day_index ) ? 'selected="selected"' : ''; + $selected = ( (int) get_option( 'start_of_week' ) === $day_index ) ? 'selected="selected"' : ''; echo "\n\t'; endfor; ?> diff --git a/src/wp-admin/options-head.php b/src/wp-admin/options-head.php index d96978b438d2b..c951b77419505 100644 --- a/src/wp-admin/options-head.php +++ b/src/wp-admin/options-head.php @@ -8,7 +8,12 @@ * @subpackage Administration */ -wp_reset_vars( array( 'action' ) ); +// Don't load directly. +if ( ! defined( 'ABSPATH' ) ) { + die( '-1' ); +} + +$action = ! empty( $_REQUEST['action'] ) ? sanitize_text_field( $_REQUEST['action'] ) : ''; if ( isset( $_GET['updated'] ) && isset( $_GET['page'] ) ) { // For back-compat with plugins that don't use the Settings API and just set updated=1 in the redirect. diff --git a/src/wp-admin/options-media.php b/src/wp-admin/options-media.php index 79f4389e886db..81ebc9223f543 100644 --- a/src/wp-admin/options-media.php +++ b/src/wp-admin/options-media.php @@ -38,8 +38,8 @@ get_current_screen()->set_help_sidebar( '

      ' . __( 'For more information:' ) . '

      ' . - '

      ' . __( 'Documentation on Media Settings' ) . '

      ' . - '

      ' . __( 'Support' ) . '

      ' + '

      ' . __( 'Documentation on Media Settings' ) . '

      ' . + '

      ' . __( 'Support forums' ) . '

      ' ); require_once ABSPATH . 'wp-admin/admin-header.php'; @@ -56,23 +56,25 @@

      + - - + + - - + + - - +

      +#errors +(1,7): expected-doctype-but-got-start-tag +(1,20): unexpected-end-tag-implies-table-voodoo +(1,20): unexpected-end-tag +(1,24): unexpected-end-tag-implies-table-voodoo +(1,24): unexpected-end-tag +(1,29): unexpected-end-tag-implies-table-voodoo +(1,29): unexpected-end-tag +(1,33): unexpected-end-tag-implies-table-voodoo +(1,33): unexpected-end-tag +(1,37): unexpected-end-tag-implies-table-voodoo +(1,37): unexpected-end-tag +(1,46): unexpected-end-tag-implies-table-voodoo +(1,46): unexpected-end-tag +(1,50): unexpected-end-tag-implies-table-voodoo +(1,50): unexpected-end-tag +(1,58): unexpected-end-tag-implies-table-voodoo +(1,58): unexpected-end-tag +(1,63): unexpected-end-tag-implies-table-voodoo +(1,63): unexpected-end-tag +(1,69): unexpected-end-tag-implies-table-voodoo +(1,69): end-tag-too-early +(1,75): unexpected-end-tag-implies-table-voodoo +(1,75): unexpected-end-tag +(1,83): unexpected-end-tag-implies-table-voodoo +(1,83): unexpected-end-tag +(1,90): unexpected-end-tag-implies-table-voodoo +(1,90): unexpected-end-tag +(1,99): unexpected-end-tag-implies-table-voodoo +(1,99): unexpected-end-tag +(1,104): unexpected-end-tag-implies-table-voodoo +(1,104): end-tag-too-early +(1,109): unexpected-end-tag-implies-table-voodoo +(1,109): end-tag-too-early +(1,114): unexpected-end-tag-implies-table-voodoo +(1,114): end-tag-too-early +(1,119): unexpected-end-tag-implies-table-voodoo +(1,119): end-tag-too-early +(1,124): unexpected-end-tag-implies-table-voodoo +(1,124): end-tag-too-early +(1,129): unexpected-end-tag-implies-table-voodoo +(1,129): end-tag-too-early +(1,136): unexpected-end-tag-in-table-row +(1,141): unexpected-end-tag-implies-table-voodoo +(1,141): unexpected-end-tag-treated-as +(1,145): unexpected-end-tag-implies-table-voodoo +(1,145): unexpected-end-tag +(1,151): unexpected-end-tag-implies-table-voodoo +(1,151): unexpected-end-tag +(1,159): unexpected-end-tag-implies-table-voodoo +(1,159): unexpected-end-tag +(1,166): unexpected-end-tag-implies-table-voodoo +(1,166): unexpected-end-tag +(1,174): unexpected-end-tag-implies-table-voodoo +(1,174): unexpected-end-tag +(1,183): unexpected-end-tag-implies-table-voodoo +(1,183): unexpected-end-tag +(1,196): unexpected-end-tag +(1,201): unexpected-end-tag +(1,206): unexpected-end-tag +(1,214): unexpected-end-tag +(1,221): unexpected-end-tag +(1,228): unexpected-end-tag +(1,236): unexpected-end-tag +(1,241): unexpected-end-tag +(1,249): unexpected-end-tag +(1,255): unexpected-end-tag +(1,262): unexpected-end-tag +(1,269): unexpected-end-tag +(1,280): unexpected-end-tag +(1,290): unexpected-end-tag +(1,298): unexpected-end-tag +(1,307): unexpected-end-tag +(1,311): unexpected-end-tag +(1,316): unexpected-end-tag +(1,321): unexpected-end-tag +(1,331): unexpected-end-tag +(1,342): unexpected-end-tag +(1,350): unexpected-end-tag +(1,358): unexpected-end-tag +(1,366): unexpected-end-tag +(1,376): end-tag-too-early +(1,389): end-tag-too-early +(1,398): end-tag-too-early +(1,404): end-tag-too-early +(1,410): end-tag-too-early +(1,415): end-tag-too-early +(1,426): end-tag-too-early +(1,436): end-tag-too-early +(1,443): end-tag-too-early +(1,448): end-tag-too-early +(1,453): end-tag-too-early +(1,458): unexpected-end-tag +(1,465): unexpected-end-tag +(1,471): unexpected-end-tag +(1,478): unexpected-end-tag +(1,487): end-tag-too-early +(1,497): end-tag-too-early +(1,506): end-tag-too-early +(1,524): expected-eof-but-got-end-tag +(1,524): unexpected-end-tag +(1,531): unexpected-end-tag +(1,540): unexpected-end-tag +(1,548): unexpected-end-tag +(1,558): unexpected-end-tag +(1,568): unexpected-end-tag +(1,579): unexpected-end-tag +(1,590): unexpected-end-tag +(1,601): unexpected-end-tag +(1,610): unexpected-end-tag +(1,622): unexpected-end-tag +(1,633): unexpected-end-tag +#document +| +| +| +|
      +| +| +| +|

      + +#data + +#errors +(1,10): expected-doctype-but-got-start-tag +(1,10): eof-in-frameset +#document +| +| +| diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests10.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests10.dat new file mode 100644 index 0000000000000..f84e2d546fab6 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests10.dat @@ -0,0 +1,849 @@ +#data + +#errors +#document +| +| +| +| +| + +#data +a +#errors +(1,28) expected-dashes-or-doctype +#new-errors +(1:35) cdata-in-html-content +#document +| +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| + +#data + +#errors +(1,34) unexpected-start-tag-in-select +(1,40) unexpected-end-tag-in-select +#document +| +| +| +| +| +#errors +(1,42) unexpected-start-tag-in-select +(1,48) unexpected-end-tag-in-select +#document +| +| +| +| +|

      +#errors +(1,33) foster-parenting-start-tag +#document +| +| +| +| +| +| + +#data +
      foo
      +#errors +(1,33) foster-parenting-start-tag +#document +| +| +| +| +| +| +| "foo" +| + +#data +
      foobar
      +#errors +(1,33) foster-parenting-start-tag +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +| + +#data +
      foobar
      +#errors +(1,40) foster-parenting-start-tag +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +| +| + +#data +
      foobar
      +#errors +(1,44) foster-parenting-start-tag +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +| +| +| + +#data +
      foobar
      +#errors +#document +| +| +| +| +| +| +| +|
      +| +| +| "foo" +| +| "bar" + +#data +
      foobar

      baz

      +#errors +#document +| +| +| +| +| +| +| +|
      +| +| +| "foo" +| +| "bar" +|

      +| "baz" + +#data +
      foobar

      baz

      +#errors +#document +| +| +| +| +| +|
      +| +| +| "foo" +| +| "bar" +|

      +| "baz" + +#data +
      foobar

      baz

      quux +#errors +(1,65) unexpected-html-element-in-foreign-content +#document +| +| +| +| +| +|
      +| +| +| "foo" +| +| "bar" +|

      +| "baz" +|

      +| "quux" + +#data +
      foobarbaz

      quux +#errors +(1,73) unexpected-end-tag +(1,73) expected-one-end-tag-but-got-another +#document +| +| +| +| +| +|
      +| +| +| "foo" +| +| "bar" +| "baz" +|

      +| "quux" + +#data +foobar

      baz

      quux +#errors +(1,43) foster-parenting-start-tag svg +(1,66) unexpected HTML-like start tag token in foreign content +(1,66) foster-parenting-start-tag +(1,67) foster-parenting-character +(1,68) foster-parenting-character +(1,69) foster-parenting-character +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +|

      +| "baz" +| +| +|

      +| "quux" + +#data +

      quux +#errors +(1,49) unexpected-start-tag-in-select +(1,52) unexpected-start-tag-in-select +(1,59) unexpected-end-tag-in-select +(1,62) unexpected-start-tag-in-select +(1,69) unexpected-end-tag-in-select +(1,72) unexpected-start-tag-in-select +(1,83) unexpected-table-element-end-tag-in-select-in-table +#document +| +| +| +| +| +| +| +|
      +|

      quux +#errors +(1,36) unexpected-start-tag-implies-table-voodoo +(1,41) unexpected-start-tag-in-select +(1,44) unexpected-start-tag-in-select +(1,51) unexpected-end-tag-in-select +(1,54) unexpected-start-tag-in-select +(1,61) unexpected-end-tag-in-select +(1,64) unexpected-start-tag-in-select +(1,75) unexpected-table-element-end-tag-in-select-in-table +#document +| +| +| +| +| +|

      +| "quux" + +#data +foobar

      baz +#errors +(1,40) expected-eof-but-got-start-tag +(1,63) unexpected-html-element-in-foreign-content +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +|

      +| "baz" + +#data +foobar

      baz +#errors +(1,33) unexpected-start-tag-after-body +(1,56) unexpected-html-element-in-foreign-content +#document +| +| +| +| +| +| +| "foo" +| +| "bar" +|

      +| "baz" + +#data +

      +#errors +(1,30) unexpected-start-tag-in-frameset +(1,33) unexpected-start-tag-in-frameset +(1,37) unexpected-end-tag-in-frameset +(1,40) unexpected-start-tag-in-frameset +(1,44) unexpected-end-tag-in-frameset +(1,47) unexpected-start-tag-in-frameset +(1,53) unexpected-start-tag-in-frameset +(1,53) eof-in-frameset +#document +| +| +| +| + +#data +

      +#errors +(1,41) unexpected-start-tag-after-frameset +(1,44) unexpected-start-tag-after-frameset +(1,48) unexpected-end-tag-after-frameset +(1,51) unexpected-start-tag-after-frameset +(1,55) unexpected-end-tag-after-frameset +(1,58) unexpected-start-tag-after-frameset +(1,64) unexpected-start-tag-after-frameset +#document +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| xlink:href="foo" +| +| xlink href="foo" + +#data + +#errors +#document +| +| +| +| +| xlink:href="foo" +| xml:lang="en" +| +| +| xlink href="foo" +| xml lang="en" + +#data + +#errors +#document +| +| +| +| +| xlink:href="foo" +| xml:lang="en" +| +| +| xlink href="foo" +| xml lang="en" + +#data +bar +#errors +#document +| +| +| +| +| xlink:href="foo" +| xml:lang="en" +| +| +| xlink href="foo" +| xml lang="en" +| "bar" + +#data + +#errors +(1,5) expected-doctype-but-got-start-tag +(1,12) unexpected-end-tag +(1,12) unexpected-end-tag +(1,12) expected-closing-tag-but-got-eof +#document +| +| +| +| + +#data +

      a +#errors +(1,5) expected-doctype-but-got-start-tag +(1,16) unexpected-end-tag +(1,16) end-tag-too-early +#document +| +| +| +|
      +| +| "a" + +#data +
      a +#errors +(1,5) expected-doctype-but-got-start-tag +(1,22) unexpected-end-tag +(1,22) end-tag-too-early +#document +| +| +| +|
      +| +| +| "a" + +#data +
      +#errors +(1,5) expected-doctype-but-got-start-tag +(1,22) unexpected-end-tag +(1,28) expected-closing-tag-but-got-eof +#document +| +| +| +|
      +| +| +| + +#data +
      a +#errors +(1,5) expected-doctype-but-got-start-tag +(1,43) unexpected-end-tag +(1,43) end-tag-too-early +(1,44) expected-closing-tag-but-got-eof +#document +| +| +| +|
      +| +| +| +| +| "a" + +#data +

      a +#errors +(1,5) expected-doctype-but-got-start-tag +(1,40) end-tag-too-early +(1,41) expected-closing-tag-but-got-eof +#document +| +| +| +|

      +| +| +| +|

      +| "a" + +#data +
        a +#errors +(1,40) unexpected-html-element-in-foreign-content +(1,41) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +|
        +| +|
          +| "a" + +#data +
            a +#errors +(1,35) unexpected-html-element-in-foreign-content +(1,36) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +| +|
              +| "a" + +#data +

              +#errors +(1,32) expected-closing-tag-but-got-eof +#document +| +| +| +| +|

              +| +| +|

              + +#data +

              +#errors +(1,33) expected-closing-tag-but-got-eof +#document +| +| +| +| +|

              +| +| +|

              + +#data +

              +#errors +(1,5) expected-doctype-but-got-start-tag +(1,50) unexpected-end-tag +(1,53) expected-closing-tag-but-got-eof +#document +| +| +| +|

              +| +| +| +|

              +|

              + +#data +
              +#errors +(1,6) expected-doctype-but-got-start-tag +(1,71) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +|
              +| +|
              +| +| + +#data +
              +#errors +(1,6) expected-doctype-but-got-start-tag +(1,83) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +| +|
              +|
              +| + +#data + +#errors +(1,5) expected-doctype-but-got-start-tag +(1,28) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data +

      +#errors +(1,7) expected-doctype-but-got-start-tag +(1,12) unexpected-start-tag-implies-table-voodoo +(1,22) eof-in-table +#document +| +| +| +| +|
      +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,18) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,22) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,18) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,22) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,18) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,22) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,18) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,22) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,21) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,25) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,54) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +| + +#data +
      +#errors +(1,6) expected-doctype-but-got-start-tag +(1,144) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +| +|
      +| +| +| +| +| + +#data + +#errors +(1,6) expected-doctype-but-got-start-tag +(1,153) expected-closing-tag-but-got-eof +#document +| +| +| +| +| +| +| +| +| +| +| +| +| +| diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests11.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests11.dat new file mode 100644 index 0000000000000..b9901e79ec405 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests11.dat @@ -0,0 +1,523 @@ +#data + +#errors +#document +| +| +| +| +| +| attributeName="" +| attributeType="" +| baseFrequency="" +| baseProfile="" +| calcMode="" +| clipPathUnits="" +| diffuseConstant="" +| edgeMode="" +| filterUnits="" +| glyphRef="" +| gradientTransform="" +| gradientUnits="" +| kernelMatrix="" +| kernelUnitLength="" +| keyPoints="" +| keySplines="" +| keyTimes="" +| lengthAdjust="" +| limitingConeAngle="" +| markerHeight="" +| markerUnits="" +| markerWidth="" +| maskContentUnits="" +| maskUnits="" +| numOctaves="" +| pathLength="" +| patternContentUnits="" +| patternTransform="" +| patternUnits="" +| pointsAtX="" +| pointsAtY="" +| pointsAtZ="" +| preserveAlpha="" +| preserveAspectRatio="" +| primitiveUnits="" +| refX="" +| refY="" +| repeatCount="" +| repeatDur="" +| requiredExtensions="" +| requiredFeatures="" +| specularConstant="" +| specularExponent="" +| spreadMethod="" +| startOffset="" +| stdDeviation="" +| stitchTiles="" +| surfaceScale="" +| systemLanguage="" +| tableValues="" +| targetX="" +| targetY="" +| textLength="" +| viewBox="" +| viewTarget="" +| xChannelSelector="" +| yChannelSelector="" +| zoomAndPan="" + +#data + +#errors +#document +| +| +| +| +| +| attributeName="" +| attributeType="" +| baseFrequency="" +| baseProfile="" +| calcMode="" +| clipPathUnits="" +| diffuseConstant="" +| edgeMode="" +| filterUnits="" +| glyphRef="" +| gradientTransform="" +| gradientUnits="" +| kernelMatrix="" +| kernelUnitLength="" +| keyPoints="" +| keySplines="" +| keyTimes="" +| lengthAdjust="" +| limitingConeAngle="" +| markerHeight="" +| markerUnits="" +| markerWidth="" +| maskContentUnits="" +| maskUnits="" +| numOctaves="" +| pathLength="" +| patternContentUnits="" +| patternTransform="" +| patternUnits="" +| pointsAtX="" +| pointsAtY="" +| pointsAtZ="" +| preserveAlpha="" +| preserveAspectRatio="" +| primitiveUnits="" +| refX="" +| refY="" +| repeatCount="" +| repeatDur="" +| requiredExtensions="" +| requiredFeatures="" +| specularConstant="" +| specularExponent="" +| spreadMethod="" +| startOffset="" +| stdDeviation="" +| stitchTiles="" +| surfaceScale="" +| systemLanguage="" +| tableValues="" +| targetX="" +| targetY="" +| textLength="" +| viewBox="" +| viewTarget="" +| xChannelSelector="" +| yChannelSelector="" +| zoomAndPan="" + +#data + +#errors +#document +| +| +| +| +| +| attributeName="" +| attributeType="" +| baseFrequency="" +| baseProfile="" +| calcMode="" +| clipPathUnits="" +| diffuseConstant="" +| edgeMode="" +| filterUnits="" +| filterres="" +| glyphRef="" +| gradientTransform="" +| gradientUnits="" +| kernelMatrix="" +| kernelUnitLength="" +| keyPoints="" +| keySplines="" +| keyTimes="" +| lengthAdjust="" +| limitingConeAngle="" +| markerHeight="" +| markerUnits="" +| markerWidth="" +| maskContentUnits="" +| maskUnits="" +| numOctaves="" +| pathLength="" +| patternContentUnits="" +| patternTransform="" +| patternUnits="" +| pointsAtX="" +| pointsAtY="" +| pointsAtZ="" +| preserveAlpha="" +| preserveAspectRatio="" +| primitiveUnits="" +| refX="" +| refY="" +| repeatCount="" +| repeatDur="" +| requiredExtensions="" +| requiredFeatures="" +| specularConstant="" +| specularExponent="" +| spreadMethod="" +| startOffset="" +| stdDeviation="" +| stitchTiles="" +| surfaceScale="" +| systemLanguage="" +| tableValues="" +| targetX="" +| targetY="" +| textLength="" +| viewBox="" +| viewTarget="" +| xChannelSelector="" +| yChannelSelector="" +| zoomAndPan="" + +#data + +#errors +#document +| +| +| +| +| +| attributename="" +| attributetype="" +| basefrequency="" +| baseprofile="" +| calcmode="" +| clippathunits="" +| diffuseconstant="" +| edgemode="" +| filterunits="" +| glyphref="" +| gradienttransform="" +| gradientunits="" +| kernelmatrix="" +| kernelunitlength="" +| keypoints="" +| keysplines="" +| keytimes="" +| lengthadjust="" +| limitingconeangle="" +| markerheight="" +| markerunits="" +| markerwidth="" +| maskcontentunits="" +| maskunits="" +| numoctaves="" +| pathlength="" +| patterncontentunits="" +| patterntransform="" +| patternunits="" +| pointsatx="" +| pointsaty="" +| pointsatz="" +| preservealpha="" +| preserveaspectratio="" +| primitiveunits="" +| refx="" +| refy="" +| repeatcount="" +| repeatdur="" +| requiredextensions="" +| requiredfeatures="" +| specularconstant="" +| specularexponent="" +| spreadmethod="" +| startoffset="" +| stddeviation="" +| stitchtiles="" +| surfacescale="" +| systemlanguage="" +| tablevalues="" +| targetx="" +| targety="" +| textlength="" +| viewbox="" +| viewtarget="" +| xchannelselector="" +| ychannelselector="" +| zoomandpan="" + +#data + +#errors +#document +| +| +| +| +| +| contentscripttype="" +| contentstyletype="" +| externalresourcesrequired="" +| filterres="" + +#data + +#errors +#document +| +| +| +| +| +| contentscripttype="" +| contentstyletype="" +| externalresourcesrequired="" +| filterres="" + +#data + +#errors +#document +| +| +| +| +| +| contentscripttype="" +| contentstyletype="" +| externalresourcesrequired="" +| filterres="" + +#data + +#errors +#document +| +| +| +| +| +| contentscripttype="" +| contentstyletype="" +| externalresourcesrequired="" +| filterres="" + +#data + +#errors +#document +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| +| diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests12.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests12.dat new file mode 100644 index 0000000000000..63107d277b6a7 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests12.dat @@ -0,0 +1,62 @@ +#data +

      foobazeggs

      spam

      quuxbar +#errors +#document +| +| +| +| +|

      +| "foo" +| +| +| +| "baz" +| +| +| +| +| "eggs" +| +| +|

      +| "spam" +| +| +| +|
      +| +| +| "quux" +| "bar" + +#data +foobazeggs

      spam
      quuxbar +#errors +#document +| +| +| +| +| "foo" +| +| +| +| "baz" +| +| +| +| +| "eggs" +| +| +|

      +| "spam" +| +| +| +|
      +| +| +| "quux" +| "bar" diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests14.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests14.dat new file mode 100644 index 0000000000000..a08b7649ebab7 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests14.dat @@ -0,0 +1,75 @@ +#data + +#errors +#document +| +| +| +| +| + +#data + +#errors +#document +| +| +| +| +| +| + +#data + +#errors +(1,38): non-html-root +#document +| +| +| abc:def="gh" +| +| +| + +#data + +#errors +(1,53): non-html-root +#document +| +| +| xml:lang="bar" +| +| + +#data + +#errors +#document +| +| +| 123="456" +| +| + +#data + +#errors +(1,43): non-html-root +#document +| +| +| 123="456" +| 789="012" +| +| + +#data + +#errors +#document +| +| +| +| +| 789="012" diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests15.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests15.dat new file mode 100644 index 0000000000000..93d06a871708a --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests15.dat @@ -0,0 +1,216 @@ +#data +

      X +#errors +(1,31): unexpected-end-tag +(1,36): expected-closing-tag-but-got-eof +#document +| +| +| +| +|

      +| +| +| +| +| +| +| " " +|

      +| "X" + +#data +

      +

      X +#errors +(1,3): expected-doctype-but-got-start-tag +(1,16): unexpected-end-tag +(2,4): expected-closing-tag-but-got-eof +#document +| +| +| +|

      +| +| +| +| +| +| +| " +" +|

      +| "X" + +#data + +#errors +(1,29): expected-eof-but-got-start-tag +(1,29): unexpected-start-tag-ignored +#document +| +| +| +| +| " " + +#data + +#errors +(1,28): unexpected-start-tag-after-body +#document +| +| +| +| +| + +#data + +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| +| +| +| + +#data +X +#errors +(1,29): unexpected-start-tag-after-body +#document +| +| +| +| +| +| "X" + +#data +<!doctype html><table> X<meta></table> +#errors +(1,23): foster-parenting-character +(1,24): foster-parenting-character +(1,30): foster-parenting-start-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| " X" +| <meta> +| <table> + +#data +<!doctype html><table> x</table> +#errors +(1,23): foster-parenting-character +(1,24): foster-parenting-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| " x" +| <table> + +#data +<!doctype html><table> x </table> +#errors +(1,23): foster-parenting-character +(1,24): foster-parenting-character +(1,25): foster-parenting-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| " x " +| <table> + +#data +<!doctype html><table><tr> x</table> +#errors +(1,27): foster-parenting-character +(1,28): foster-parenting-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| " x" +| <table> +| <tbody> +| <tr> + +#data +<!doctype html><table>X<style> <tr>x </style> </table> +#errors +(1,23): foster-parenting-character +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "X" +| <table> +| <style> +| " <tr>x " +| " " + +#data +<!doctype html><div><table><a>foo</a> <tr><td>bar</td> </tr></table></div> +#errors +(1,30): foster-parenting-start-tag +(1,31): foster-parenting-character +(1,32): foster-parenting-character +(1,33): foster-parenting-character +(1,37): foster-parenting-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <div> +| <a> +| "foo" +| <table> +| " " +| <tbody> +| <tr> +| <td> +| "bar" +| " " + +#data +<frame></frame></frame><frameset><frame><frameset><frame></frameset><noframes></frameset><noframes> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,7): unexpected-start-tag-ignored +(1,15): unexpected-end-tag +(1,23): unexpected-end-tag +(1,33): unexpected-start-tag +(1,99): expected-named-closing-tag-but-got-eof +(1,99): eof-in-frameset +#document +| <html> +| <head> +| <frameset> +| <frame> +| <frameset> +| <frame> +| <noframes> +| "</frameset><noframes>" + +#data +<!DOCTYPE html><object></html> +#errors +(1,30): expected-body-in-scope +(1,30): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <object> diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests16.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests16.dat new file mode 100644 index 0000000000000..05f34c13e33d3 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests16.dat @@ -0,0 +1,2602 @@ +#data +<!doctype html><script> +#errors +(1,23): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| <body> + +#data +<!doctype html><script>a +#errors +(1,24): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "a" +| <body> + +#data +<!doctype html><script>< +#errors +(1,24): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<" +| <body> + +#data +<!doctype html><script></ +#errors +(1,25): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</" +| <body> + +#data +<!doctype html><script></S +#errors +(1,26): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</S" +| <body> + +#data +<!doctype html><script></SC +#errors +(1,27): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</SC" +| <body> + +#data +<!doctype html><script></SCR +#errors +(1,28): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</SCR" +| <body> + +#data +<!doctype html><script></SCRI +#errors +(1,29): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</SCRI" +| <body> + +#data +<!doctype html><script></SCRIP +#errors +(1,30): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</SCRIP" +| <body> + +#data +<!doctype html><script></SCRIPT +#errors +(1,31): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</SCRIPT" +| <body> + +#data +<!doctype html><script></SCRIPT +#errors +(1,32): expected-attribute-name-but-got-eof +(1,32): expected-named-closing-tag-but-got-eof +#new-errors +(1:33) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| <body> + +#data +<!doctype html><script></s +#errors +(1,26): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</s" +| <body> + +#data +<!doctype html><script></sc +#errors +(1,27): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</sc" +| <body> + +#data +<!doctype html><script></scr +#errors +(1,28): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</scr" +| <body> + +#data +<!doctype html><script></scri +#errors +(1,29): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</scri" +| <body> + +#data +<!doctype html><script></scrip +#errors +(1,30): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</scrip" +| <body> + +#data +<!doctype html><script></script +#errors +(1,31): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "</script" +| <body> + +#data +<!doctype html><script></script +#errors +(1,32): expected-attribute-name-but-got-eof +(1,32): expected-named-closing-tag-but-got-eof +#new-errors +(1:33) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| <body> + +#data +<!doctype html><script><! +#errors +(1,25): expected-script-data-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!" +| <body> + +#data +<!doctype html><script><!a +#errors +(1,26): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!a" +| <body> + +#data +<!doctype html><script><!- +#errors +(1,26): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!-" +| <body> + +#data +<!doctype html><script><!-a +#errors +(1,27): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!-a" +| <body> + +#data +<!doctype html><script><!-- +#errors +(1,27): expected-named-closing-tag-but-got-eof +(1,27): unexpected-eof-in-text-mode +#new-errors +(1:28) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--" +| <body> + +#data +<!doctype html><script><!--a +#errors +(1,28): expected-named-closing-tag-but-got-eof +(1,28): unexpected-eof-in-text-mode +#new-errors +(1:29) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--a" +| <body> + +#data +<!doctype html><script><!--< +#errors +(1,28): expected-named-closing-tag-but-got-eof +(1,28): unexpected-eof-in-text-mode +#new-errors +(1:29) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<" +| <body> + +#data +<!doctype html><script><!--<a +#errors +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<a" +| <body> + +#data +<!doctype html><script><!--</ +#errors +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--</" +| <body> + +#data +<!doctype html><script><!--</script +#errors +(1,35): expected-named-closing-tag-but-got-eof +(1,35): unexpected-eof-in-text-mode +#new-errors +(1:36) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--</script" +| <body> + +#data +<!doctype html><script><!--</script +#errors +(1,36): expected-attribute-name-but-got-eof +(1,36): expected-named-closing-tag-but-got-eof +#new-errors +(1:37) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--" +| <body> + +#data +<!doctype html><script><!--<s +#errors +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<s" +| <body> + +#data +<!doctype html><script><!--<script +#errors +(1,34): expected-named-closing-tag-but-got-eof +(1,34): unexpected-eof-in-text-mode +#new-errors +(1:35) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script" +| <body> + +#data +<!doctype html><script><!--<script +#errors +(1,35): eof-in-script-in-script +(1,35): expected-named-closing-tag-but-got-eof +#new-errors +(1:36) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script " +| <body> + +#data +<!doctype html><script><!--<script < +#errors +(1,36): eof-in-script-in-script +(1,36): expected-named-closing-tag-but-got-eof +#new-errors +(1:37) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script <" +| <body> + +#data +<!doctype html><script><!--<script <a +#errors +(1,37): eof-in-script-in-script +(1,37): expected-named-closing-tag-but-got-eof +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script <a" +| <body> + +#data +<!doctype html><script><!--<script </ +#errors +(1,37): eof-in-script-in-script +(1,37): expected-named-closing-tag-but-got-eof +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </" +| <body> + +#data +<!doctype html><script><!--<script </s +#errors +(1,38): eof-in-script-in-script +(1,38): expected-named-closing-tag-but-got-eof +#new-errors +(1:39) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </s" +| <body> + +#data +<!doctype html><script><!--<script </script +#errors +(1,43): eof-in-script-in-script +(1,43): expected-named-closing-tag-but-got-eof +#new-errors +(1:44) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script" +| <body> + +#data +<!doctype html><script><!--<script </scripta +#errors +(1,44): eof-in-script-in-script +(1,44): expected-named-closing-tag-but-got-eof +#new-errors +(1:45) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </scripta" +| <body> + +#data +<!doctype html><script><!--<script </script +#errors +(1,44): expected-named-closing-tag-but-got-eof +(1,44): unexpected-eof-in-text-mode +#new-errors +(1:45) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<!doctype html><script><!--<script </script> +#errors +(1,44): expected-named-closing-tag-but-got-eof +(1,44): unexpected-eof-in-text-mode +#new-errors +(1:45) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script>" +| <body> + +#data +<!doctype html><script><!--<script </script/ +#errors +(1,44): expected-named-closing-tag-but-got-eof +(1,44): unexpected-eof-in-text-mode +#new-errors +(1:45) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script/" +| <body> + +#data +<!doctype html><script><!--<script </script < +#errors +(1,45): expected-named-closing-tag-but-got-eof +(1,45): unexpected-eof-in-text-mode +#new-errors +(1:46) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script <" +| <body> + +#data +<!doctype html><script><!--<script </script <a +#errors +(1,46): expected-named-closing-tag-but-got-eof +(1,46): unexpected-eof-in-text-mode +#new-errors +(1:47) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script <a" +| <body> + +#data +<!doctype html><script><!--<script </script </ +#errors +(1,46): expected-named-closing-tag-but-got-eof +(1,46): unexpected-eof-in-text-mode +#new-errors +(1:47) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script </" +| <body> + +#data +<!doctype html><script><!--<script </script </script +#errors +(1,52): expected-named-closing-tag-but-got-eof +(1,52): unexpected-eof-in-text-mode +#new-errors +(1:53) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script </script" +| <body> + +#data +<!doctype html><script><!--<script </script </script +#errors +(1,53): expected-attribute-name-but-got-eof +(1,53): expected-named-closing-tag-but-got-eof +#new-errors +(1:54) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<!doctype html><script><!--<script </script </script/ +#errors +(1,53): unexpected-EOF-after-solidus-in-tag +(1,53): expected-named-closing-tag-but-got-eof +#new-errors +(1:54) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<!doctype html><script><!--<script </script </script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<!doctype html><script><!--<script - +#errors +(1,36): eof-in-script-in-script +(1,36): expected-named-closing-tag-but-got-eof +#new-errors +(1:37) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -" +| <body> + +#data +<!doctype html><script><!--<script -a +#errors +(1,37): eof-in-script-in-script +(1,37): expected-named-closing-tag-but-got-eof +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -a" +| <body> + +#data +<!doctype html><script><!--<script -< +#errors +(1,37): eof-in-script-in-script +(1,37): expected-named-closing-tag-but-got-eof +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -<" +| <body> + +#data +<!doctype html><script><!--<script -- +#errors +(1,37): eof-in-script-in-script +(1,37): expected-named-closing-tag-but-got-eof +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --" +| <body> + +#data +<!doctype html><script><!--<script --a +#errors +(1,38): eof-in-script-in-script +(1,38): expected-named-closing-tag-but-got-eof +#new-errors +(1:39) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --a" +| <body> + +#data +<!doctype html><script><!--<script --< +#errors +(1,38): eof-in-script-in-script +(1,38): expected-named-closing-tag-but-got-eof +#new-errors +(1:39) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --<" +| <body> + +#data +<!doctype html><script><!--<script --> +#errors +(1,38): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<!doctype html><script><!--<script -->< +#errors +(1,39): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --><" +| <body> + +#data +<!doctype html><script><!--<script --></ +#errors +(1,40): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --></" +| <body> + +#data +<!doctype html><script><!--<script --></script +#errors +(1,46): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script --></script" +| <body> + +#data +<!doctype html><script><!--<script --></script +#errors +(1,47): expected-attribute-name-but-got-eof +(1,47): expected-named-closing-tag-but-got-eof +#new-errors +(1:48) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<!doctype html><script><!--<script --></script/ +#errors +(1,47): unexpected-EOF-after-solidus-in-tag +(1,47): expected-named-closing-tag-but-got-eof +#new-errors +(1:48) eof-in-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<!doctype html><script><!--<script --></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<!doctype html><script><!--<script><\/script>--></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script><\/script>-->" +| <body> + +#data +<!doctype html><script><!--<script></scr'+'ipt>--></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></scr'+'ipt>-->" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script>--><!--</script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>--><!--" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script>-- ></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>-- >" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script>- -></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>- ->" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script>- - ></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>- - >" +| <body> + +#data +<!doctype html><script><!--<script></script><script></script>-></script> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>->" +| <body> + +#data +<!doctype html><script><!--<script>--!></script>X +#errors +(1,49): expected-named-closing-tag-but-got-eof +(1,49): unexpected-EOF-in-text-mode +#new-errors +(1:50) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script>--!></script>X" +| <body> + +#data +<!doctype html><script><!--<scr'+'ipt></script>--></script> +#errors +(1,59): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<scr'+'ipt>" +| <body> +| "-->" + +#data +<!doctype html><script><!--<script></scr'+'ipt></script>X +#errors +(1,57): expected-named-closing-tag-but-got-eof +(1,57): unexpected-eof-in-text-mode +#new-errors +(1:58) eof-in-script-html-comment-like-text +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| "<!--<script></scr'+'ipt></script>X" +| <body> + +#data +<!doctype html><style><!--<style></style>--></style> +#errors +(1,52): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--<style>" +| <body> +| "-->" + +#data +<!doctype html><style><!--</style>X +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--" +| <body> +| "X" + +#data +<!doctype html><style><!--...</style>...--></style> +#errors +(1,51): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--..." +| <body> +| "...-->" + +#data +<!doctype html><style><!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style></style>X +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style>" +| <body> +| "X" + +#data +<!doctype html><style><!--...<style><!--...--!></style>--></style> +#errors +(1,66): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--...<style><!--...--!>" +| <body> +| "-->" + +#data +<!doctype html><style><!--...</style><!-- --><style>@import ...</style> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "<!--..." +| <!-- --> +| <style> +| "@import ..." +| <body> + +#data +<!doctype html><style>...<style><!--...</style><!-- --></style> +#errors +(1,63): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "...<style><!--..." +| <!-- --> +| <body> + +#data +<!doctype html><style>...<!--[if IE]><style>...</style>X +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <style> +| "...<!--[if IE]><style>..." +| <body> +| "X" + +#data +<!doctype html><title><!--<title>--> +#errors +(1,52): unexpected-end-tag +#document +| +| +| +| +| "<!--<title>" +| <body> +| "-->" + +#data +<!doctype html><title></title> +#errors +#document +| +| +| +| +| "" +| + +#data +foo/title><link></head><body>X +#errors +(1,52): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <title> +| "foo/title><link></head><body>X" +| <body> + +#data +<!doctype html><noscript><!--<noscript></noscript>--></noscript> +#errors +(1,64): unexpected-end-tag +#script-on +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| "<!--<noscript>" +| <body> +| "-->" + +#data +<!doctype html><noscript><!--<noscript></noscript>--></noscript> +#errors +#script-off +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| <!-- <noscript></noscript> --> +| <body> + +#data +<!doctype html><noscript><!--</noscript>X<noscript>--></noscript> +#errors +#script-on +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| "<!--" +| <body> +| "X" +| <noscript> +| "-->" + +#data +<!doctype html><noscript><!--</noscript>X<noscript>--></noscript> +#errors +#script-off +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| <!-- </noscript>X<noscript> --> +| <body> + +#data +<!doctype html><noscript><iframe></noscript>X +#errors +#script-on +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| "<iframe>" +| <body> +| "X" + +#data +<!doctype html><noscript><iframe></noscript>X +#errors + * (1,34) unexpected token in head noscript + * (1,46) unexpected EOF +#script-off +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| <body> +| <iframe> +| "</noscript>X" + +#data +<!doctype html><noframes><!--<noframes></noframes>--></noframes> +#errors +(1,64): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noframes> +| "<!--<noframes>" +| <body> +| "-->" + +#data +<!doctype html><noframes><body><script><!--...</script></body></noframes></html> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noframes> +| "<body><script><!--...</script></body>" +| <body> + +#data +<!doctype html><textarea><!--<textarea></textarea>--></textarea> +#errors +(1,64): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <textarea> +| "<!--<textarea>" +| "-->" + +#data +<!doctype html><textarea></textarea></textarea> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <textarea> +| "</textarea>" + +#data +<!doctype html><textarea><</textarea> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <textarea> +| "<" + +#data +<!doctype html><textarea>a<b</textarea> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <textarea> +| "a<b" + +#data +<!doctype html><iframe><!--<iframe></iframe>--></iframe> +#errors +(1,56): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <iframe> +| "<!--<iframe>" +| "-->" + +#data +<!doctype html><iframe>...<!--X->...<!--/X->...</iframe> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <iframe> +| "...<!--X->...<!--/X->..." + +#data +<!doctype html><xmp><!--<xmp></xmp>--></xmp> +#errors +(1,44): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <xmp> +| "<!--<xmp>" +| "-->" + +#data +<!doctype html><noembed><!--<noembed></noembed>--></noembed> +#errors +(1,60): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <noembed> +| "<!--<noembed>" +| "-->" + +#data +<script> +#errors +(1,8): expected-doctype-but-got-start-tag +(1,8): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| <body> + +#data +<script>a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,9): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "a" +| <body> + +#data +<script>< +#errors +(1,8): expected-doctype-but-got-start-tag +(1,9): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<" +| <body> + +#data +<script></ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,10): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</" +| <body> + +#data +<script></S +#errors +(1,8): expected-doctype-but-got-start-tag +(1,11): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</S" +| <body> + +#data +<script></SC +#errors +(1,8): expected-doctype-but-got-start-tag +(1,12): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</SC" +| <body> + +#data +<script></SCR +#errors +(1,8): expected-doctype-but-got-start-tag +(1,13): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</SCR" +| <body> + +#data +<script></SCRI +#errors +(1,8): expected-doctype-but-got-start-tag +(1,14): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</SCRI" +| <body> + +#data +<script></SCRIP +#errors +(1,8): expected-doctype-but-got-start-tag +(1,15): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</SCRIP" +| <body> + +#data +<script></SCRIPT +#errors +(1,8): expected-doctype-but-got-start-tag +(1,16): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</SCRIPT" +| <body> + +#data +<script></SCRIPT +#errors +(1,8): expected-doctype-but-got-start-tag +(1,17): expected-attribute-name-but-got-eof +(1,17): expected-named-closing-tag-but-got-eof +#new-errors +(1:18) eof-in-tag +#document +| <html> +| <head> +| <script> +| <body> + +#data +<script></s +#errors +(1,8): expected-doctype-but-got-start-tag +(1,11): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</s" +| <body> + +#data +<script></sc +#errors +(1,8): expected-doctype-but-got-start-tag +(1,12): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</sc" +| <body> + +#data +<script></scr +#errors +(1,8): expected-doctype-but-got-start-tag +(1,13): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</scr" +| <body> + +#data +<script></scri +#errors +(1,8): expected-doctype-but-got-start-tag +(1,14): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</scri" +| <body> + +#data +<script></scrip +#errors +(1,8): expected-doctype-but-got-start-tag +(1,15): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</scrip" +| <body> + +#data +<script></script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,16): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</script" +| <body> + +#data +<script></script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,17): expected-attribute-name-but-got-eof +(1,17): expected-named-closing-tag-but-got-eof +#new-errors +(1:18) eof-in-tag +#document +| <html> +| <head> +| <script> +| <body> + +#data +<script><! +#errors +(1,8): expected-doctype-but-got-start-tag +(1,10): expected-script-data-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!" +| <body> + +#data +<script><!a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,11): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!a" +| <body> + +#data +<script><!- +#errors +(1,8): expected-doctype-but-got-start-tag +(1,11): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!-" +| <body> + +#data +<script><!-a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,12): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!-a" +| <body> + +#data +<script><!-- +#errors +(1,8): expected-doctype-but-got-start-tag +(1,12): expected-named-closing-tag-but-got-eof +(1,12): unexpected-eof-in-text-mode +#new-errors +(1:13) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--" +| <body> + +#data +<script><!--a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,13): expected-named-closing-tag-but-got-eof +(1,13): unexpected-eof-in-text-mode +#new-errors +(1:14) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--a" +| <body> + +#data +<script><!--< +#errors +(1,8): expected-doctype-but-got-start-tag +(1,13): expected-named-closing-tag-but-got-eof +(1,13): unexpected-eof-in-text-mode +#new-errors +(1:14) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<" +| <body> + +#data +<script><!--<a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,14): expected-named-closing-tag-but-got-eof +(1,14): unexpected-eof-in-text-mode +#new-errors +(1:15) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<a" +| <body> + +#data +<script><!--</ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,14): expected-named-closing-tag-but-got-eof +(1,14): unexpected-eof-in-text-mode +#new-errors +(1:15) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--</" +| <body> + +#data +<script><!--</script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,20): expected-named-closing-tag-but-got-eof +(1,20): unexpected-eof-in-text-mode +#new-errors +(1:21) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--</script" +| <body> + +#data +<script><!--</script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,21): expected-attribute-name-but-got-eof +(1,21): expected-named-closing-tag-but-got-eof +#new-errors +(1:22) eof-in-tag +#document +| <html> +| <head> +| <script> +| "<!--" +| <body> + +#data +<script><!--<s +#errors +(1,8): expected-doctype-but-got-start-tag +(1,14): expected-named-closing-tag-but-got-eof +(1,14): unexpected-eof-in-text-mode +#new-errors +(1:15) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<s" +| <body> + +#data +<script><!--<script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,19): expected-named-closing-tag-but-got-eof +(1,19): unexpected-eof-in-text-mode +#new-errors +(1:20) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script" +| <body> + +#data +<script><!--<script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,20): eof-in-script-in-script +(1,20): expected-named-closing-tag-but-got-eof +#new-errors +(1:21) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script " +| <body> + +#data +<script><!--<script < +#errors +(1,8): expected-doctype-but-got-start-tag +(1,21): eof-in-script-in-script +(1,21): expected-named-closing-tag-but-got-eof +#new-errors +(1:22) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script <" +| <body> + +#data +<script><!--<script <a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,22): eof-in-script-in-script +(1,22): expected-named-closing-tag-but-got-eof +#new-errors +(1:23) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script <a" +| <body> + +#data +<script><!--<script </ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,22): eof-in-script-in-script +(1,22): expected-named-closing-tag-but-got-eof +#new-errors +(1:23) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </" +| <body> + +#data +<script><!--<script </s +#errors +(1,8): expected-doctype-but-got-start-tag +(1,23): eof-in-script-in-script +(1,23): expected-named-closing-tag-but-got-eof +#new-errors +(1:24) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </s" +| <body> + +#data +<script><!--<script </script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,28): eof-in-script-in-script +(1,28): expected-named-closing-tag-but-got-eof +#new-errors +(1:29) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script" +| <body> + +#data +<script><!--<script </scripta +#errors +(1,8): expected-doctype-but-got-start-tag +(1,29): eof-in-script-in-script +(1,29): expected-named-closing-tag-but-got-eof +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </scripta" +| <body> + +#data +<script><!--<script </script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<script><!--<script </script> +#errors +(1,8): expected-doctype-but-got-start-tag +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script>" +| <body> + +#data +<script><!--<script </script/ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,29): expected-named-closing-tag-but-got-eof +(1,29): unexpected-eof-in-text-mode +#new-errors +(1:30) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script/" +| <body> + +#data +<script><!--<script </script < +#errors +(1,8): expected-doctype-but-got-start-tag +(1,30): expected-named-closing-tag-but-got-eof +(1,30): unexpected-eof-in-text-mode +#new-errors +(1:31) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script <" +| <body> + +#data +<script><!--<script </script <a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,31): expected-named-closing-tag-but-got-eof +(1,31): unexpected-eof-in-text-mode +#new-errors +(1:32) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script <a" +| <body> + +#data +<script><!--<script </script </ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,31): expected-named-closing-tag-but-got-eof +(1,31): unexpected-eof-in-text-mode +#new-errors +(1:32) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script </" +| <body> + +#data +<script><!--<script </script </script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,37): expected-named-closing-tag-but-got-eof +(1,37): unexpected-eof-in-text-mode +#new-errors +(1:38) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script </script </script" +| <body> + +#data +<script><!--<script </script </script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,38): expected-attribute-name-but-got-eof +(1,38): expected-named-closing-tag-but-got-eof +#new-errors +(1:39) eof-in-tag +#document +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<script><!--<script </script </script/ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,38): unexpected-EOF-after-solidus-in-tag +(1,38): expected-named-closing-tag-but-got-eof +#new-errors +(1:39) eof-in-tag +#document +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<script><!--<script </script </script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script </script " +| <body> + +#data +<script><!--<script - +#errors +(1,8): expected-doctype-but-got-start-tag +(1,21): eof-in-script-in-script +(1,21): expected-named-closing-tag-but-got-eof +#new-errors +(1:22) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script -" +| <body> + +#data +<script><!--<script -a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,22): eof-in-script-in-script +(1,22): expected-named-closing-tag-but-got-eof +#new-errors +(1:23) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script -a" +| <body> + +#data +<script><!--<script -- +#errors +(1,8): expected-doctype-but-got-start-tag +(1,22): eof-in-script-in-script +(1,22): expected-named-closing-tag-but-got-eof +#new-errors +(1:23) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script --" +| <body> + +#data +<script><!--<script --a +#errors +(1,8): expected-doctype-but-got-start-tag +(1,23): eof-in-script-in-script +(1,23): expected-named-closing-tag-but-got-eof +#new-errors +(1:24) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script --a" +| <body> + +#data +<script><!--<script --> +#errors +(1,8): expected-doctype-but-got-start-tag +(1,23): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<script><!--<script -->< +#errors +(1,8): expected-doctype-but-got-start-tag +(1,24): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!--<script --><" +| <body> + +#data +<script><!--<script --></ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,25): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!--<script --></" +| <body> + +#data +<script><!--<script --></script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,31): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "<!--<script --></script" +| <body> + +#data +<script><!--<script --></script +#errors +(1,8): expected-doctype-but-got-start-tag +(1,32): expected-attribute-name-but-got-eof +(1,32): expected-named-closing-tag-but-got-eof +#new-errors +(1:33) eof-in-tag +#document +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<script><!--<script --></script/ +#errors +(1,8): expected-doctype-but-got-start-tag +(1,32): unexpected-EOF-after-solidus-in-tag +(1,32): expected-named-closing-tag-but-got-eof +#new-errors +(1:33) eof-in-tag +#document +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<script><!--<script --></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script -->" +| <body> + +#data +<script><!--<script><\/script>--></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script><\/script>-->" +| <body> + +#data +<script><!--<script></scr'+'ipt>--></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></scr'+'ipt>-->" +| <body> + +#data +<script><!--<script></script><script></script></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>" +| <body> + +#data +<script><!--<script></script><script></script>--><!--</script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>--><!--" +| <body> + +#data +<script><!--<script></script><script></script>-- ></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>-- >" +| <body> + +#data +<script><!--<script></script><script></script>- -></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>- ->" +| <body> + +#data +<script><!--<script></script><script></script>- - ></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>- - >" +| <body> + +#data +<script><!--<script></script><script></script>-></script> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <script> +| "<!--<script></script><script></script>->" +| <body> + +#data +<script><!--<script>--!></script>X +#errors +(1,8): expected-doctype-but-got-start-tag +(1,34): expected-named-closing-tag-but-got-eof +(1,34): unexpected-eof-in-text-mode +#new-errors +(1:35) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script>--!></script>X" +| <body> + +#data +<script><!--<scr'+'ipt></script>--></script> +#errors +(1,8): expected-doctype-but-got-start-tag +(1,44): unexpected-end-tag +#document +| <html> +| <head> +| <script> +| "<!--<scr'+'ipt>" +| <body> +| "-->" + +#data +<script><!--<script></scr'+'ipt></script>X +#errors +(1,8): expected-doctype-but-got-start-tag +(1,42): expected-named-closing-tag-but-got-eof +(1,42): unexpected-eof-in-text-mode +#new-errors +(1:43) eof-in-script-html-comment-like-text +#document +| <html> +| <head> +| <script> +| "<!--<script></scr'+'ipt></script>X" +| <body> + +#data +<style><!--<style></style>--></style> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,37): unexpected-end-tag +#document +| <html> +| <head> +| <style> +| "<!--<style>" +| <body> +| "-->" + +#data +<style><!--</style>X +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <style> +| "<!--" +| <body> +| "X" + +#data +<style><!--...</style>...--></style> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,36): unexpected-end-tag +#document +| <html> +| <head> +| <style> +| "<!--..." +| <body> +| "...-->" + +#data +<style><!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style></style>X +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <style> +| "<!--<br><html xmlns:v="urn:schemas-microsoft-com:vml"><!--[if !mso]><style>" +| <body> +| "X" + +#data +<style><!--...<style><!--...--!></style>--></style> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,51): unexpected-end-tag +#document +| <html> +| <head> +| <style> +| "<!--...<style><!--...--!>" +| <body> +| "-->" + +#data +<style><!--...</style><!-- --><style>@import ...</style> +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <style> +| "<!--..." +| <!-- --> +| <style> +| "@import ..." +| <body> + +#data +<style>...<style><!--...</style><!-- --></style> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,48): unexpected-end-tag +#document +| <html> +| <head> +| <style> +| "...<style><!--..." +| <!-- --> +| <body> + +#data +<style>...<!--[if IE]><style>...</style>X +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <style> +| "...<!--[if IE]><style>..." +| <body> +| "X" + +#data +<title><!--<title>--> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,37): unexpected-end-tag +#document +| +| +| +| "<!--<title>" +| <body> +| "-->" + +#data +<title></title> +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| +| +| +| "" +| + +#data +foo/title><link></head><body>X +#errors +(1,7): expected-doctype-but-got-start-tag +(1,37): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <title> +| "foo/title><link></head><body>X" +| <body> + +#data +<noscript><!--<noscript></noscript>--></noscript> +#errors +(1,10): expected-doctype-but-got-start-tag +(1,49): unexpected-end-tag +#script-on +#document +| <html> +| <head> +| <noscript> +| "<!--<noscript>" +| <body> +| "-->" + +#data +<noscript><!--<noscript></noscript>--></noscript> +#errors + * (1,11) missing DOCTYPE +#script-off +#document +| <html> +| <head> +| <noscript> +| <!-- <noscript></noscript> --> +| <body> + +#data +<noscript><!--</noscript>X<noscript>--></noscript> +#errors +(1,10): expected-doctype-but-got-start-tag +#script-on +#document +| <html> +| <head> +| <noscript> +| "<!--" +| <body> +| "X" +| <noscript> +| "-->" + +#data +<noscript><!--</noscript>X<noscript>--></noscript> +#errors +(1,10): expected-doctype-but-got-start-tag +#script-off +#document +| <html> +| <head> +| <noscript> +| <!-- </noscript>X<noscript> --> +| <body> + +#data +<noscript><iframe></noscript>X +#errors +(1,10): expected-doctype-but-got-start-tag +#script-on +#document +| <html> +| <head> +| <noscript> +| "<iframe>" +| <body> +| "X" + +#data +<noscript><iframe></noscript>X +#errors + * (1,11) missing DOCTYPE + * (1,19) unexpected token in head noscript + * (1,31) unexpected EOF +#script-off +#document +| <html> +| <head> +| <noscript> +| <body> +| <iframe> +| "</noscript>X" + +#data +<noframes><!--<noframes></noframes>--></noframes> +#errors +(1,10): expected-doctype-but-got-start-tag +(1,49): unexpected-end-tag +#document +| <html> +| <head> +| <noframes> +| "<!--<noframes>" +| <body> +| "-->" + +#data +<noframes><body><script><!--...</script></body></noframes></html> +#errors +(1,10): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <noframes> +| "<body><script><!--...</script></body>" +| <body> + +#data +<textarea><!--<textarea></textarea>--></textarea> +#errors +(1,10): expected-doctype-but-got-start-tag +(1,49): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| <textarea> +| "<!--<textarea>" +| "-->" + +#data +<textarea></textarea></textarea> +#errors +(1,10): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <textarea> +| "</textarea>" + +#data +<iframe><!--<iframe></iframe>--></iframe> +#errors +(1,8): expected-doctype-but-got-start-tag +(1,41): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| <iframe> +| "<!--<iframe>" +| "-->" + +#data +<iframe>...<!--X->...<!--/X->...</iframe> +#errors +(1,8): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <iframe> +| "...<!--X->...<!--/X->..." + +#data +<xmp><!--<xmp></xmp>--></xmp> +#errors +(1,5): expected-doctype-but-got-start-tag +(1,29): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| <xmp> +| "<!--<xmp>" +| "-->" + +#data +<noembed><!--<noembed></noembed>--></noembed> +#errors +(1,9): expected-doctype-but-got-start-tag +(1,45): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| <noembed> +| "<!--<noembed>" +| "-->" + +#data +<!doctype html><table> + +#errors +(2,0): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| " +" + +#data +<!doctype html><table><td><span><font></span><span> +#errors +(1,26): unexpected-cell-in-table-body +(1,45): unexpected-end-tag +(1,51): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <span> +| <font> +| <font> +| <span> + +#data +<!doctype html><form><table></form><form></table></form> +#errors +(1,35): unexpected-end-tag-implies-table-voodoo +(1,35): unexpected-end-tag +(1,41): unexpected-form-in-table +(1,56): unexpected-end-tag +(1,56): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <form> +| <table> +| <form> diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests17.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests17.dat new file mode 100644 index 0000000000000..e49bcf03142dc --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests17.dat @@ -0,0 +1,179 @@ +#data +<!doctype html><table><tbody><select><tr> +#errors +(1,37): unexpected-start-tag-implies-table-voodoo +(1,41): unexpected-table-element-start-tag-in-select-in-table +(1,41): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <table> +| <tbody> +| <tr> + +#data +<!doctype html><table><tr><select><td> +#errors +(1,34): unexpected-start-tag-implies-table-voodoo +(1,38): unexpected-table-element-start-tag-in-select-in-table +(1,38): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <table> +| <tbody> +| <tr> +| <td> + +#data +<!doctype html><table><tr><td><select><td> +#errors +(1,42): unexpected-table-element-start-tag-in-select-in-table +(1,42): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <select> +| <td> + +#data +<!doctype html><table><tr><th><select><td> +#errors +(1,42): unexpected-table-element-start-tag-in-select-in-table +(1,42): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <th> +| <select> +| <td> + +#data +<!doctype html><table><caption><select><tr> +#errors +(1,43): unexpected-table-element-start-tag-in-select-in-table +(1,43): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <caption> +| <select> +| <tbody> +| <tr> + +#data +<!doctype html><select><tr> +#errors +(1,27): unexpected-start-tag-in-select +(1,27): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><td> +#errors +(1,27): unexpected-start-tag-in-select +(1,27): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><th> +#errors +(1,27): unexpected-start-tag-in-select +(1,27): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><tbody> +#errors +(1,30): unexpected-start-tag-in-select +(1,30): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><thead> +#errors +(1,30): unexpected-start-tag-in-select +(1,30): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><tfoot> +#errors +(1,30): unexpected-start-tag-in-select +(1,30): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><select><caption> +#errors +(1,32): unexpected-start-tag-in-select +(1,32): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><table><tr></table>a +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| "a" diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests18.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests18.dat new file mode 100644 index 0000000000000..0b6d5dc404a9d --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests18.dat @@ -0,0 +1,558 @@ +#data +<plaintext></plaintext> +#errors +11: Start tag seen without seeing a doctype first. Expected “<!DOCTYPE html>”. +23: End of file seen and there were open elements. +#document +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><plaintext></plaintext> +#errors +(1,38): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><html><plaintext></plaintext> +#errors +44: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><head><plaintext></plaintext> +#errors +44: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><html><noscript><plaintext></plaintext> +#errors +42: Bad start tag in “plaintext” in “head”. +54: End of file seen and there were open elements. +#script-off +#document +| <!DOCTYPE html> +| <html> +| <head> +| <noscript> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html></head><plaintext></plaintext> +#errors +45: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><body><plaintext></plaintext> +#errors +44: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><table><plaintext></plaintext> +#errors +(1,33): foster-parenting-start-tag +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): foster-parenting-character +(1,46): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" +| <table> + +#data +<!doctype html><table><tbody><plaintext></plaintext> +#errors +(1,40): foster-parenting-start-tag +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): foster-parenting-character +(1,53): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" +| <table> +| <tbody> + +#data +<!doctype html><table><tbody><tr><plaintext></plaintext> +#errors +(1,44): foster-parenting-start-tag +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): foster-parenting-character +(1,57): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" +| <table> +| <tbody> +| <tr> + +#data +<!doctype html><table><td><plaintext></plaintext> +#errors +(1,26): unexpected-cell-in-table-body +(1,49): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><table><caption><plaintext></plaintext> +#errors +(1,54): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <caption> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><table><colgroup><plaintext></plaintext> +#errors +(1,43): foster-parenting-start-tag +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +(1,56): foster-parenting-character +55: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" +| <table> +| <colgroup> + +#data +<!doctype html><select><plaintext></plaintext>X +#errors +34: Stray start tag “plaintext”. +46: Stray end tag “plaintext”. +47: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| "X" + +#data +<!doctype html><table><select><plaintext>a<caption>b +#errors +30: Start tag “select” seen in “table”. +41: Stray start tag “plaintext”. +51: “caption” start tag with “select” open. +52: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| "a" +| <table> +| <caption> +| "b" + +#data +<!doctype html><template><plaintext>a</template>b +#errors +49: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <template> +| content +| <plaintext> +| "a</template>b" +| <body> + +#data +<!doctype html><body></body><plaintext></plaintext> +#errors +39: Stray start tag “plaintext”. +51: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><frameset><plaintext></plaintext> +#errors +36: Stray start tag “plaintext”. +48: Stray end tag “plaintext”. +48: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><frameset></frameset><plaintext></plaintext> +#errors +47: Stray start tag “plaintext”. +59: Stray end tag “plaintext”. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><body></body></html><plaintext></plaintext> +#errors +46: Stray start tag “plaintext”. +58: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!doctype html><frameset></frameset></html><plaintext></plaintext> +#errors +54: Stray start tag “plaintext”. +66: Stray end tag “plaintext”. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><svg><plaintext>a</plaintext>b +#errors +45: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <svg svg> +| <svg plaintext> +| "a" +| "b" + +#data +<!doctype html><svg><title><plaintext>a</plaintext>b +#errors +52: End of file seen and there were open elements. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <svg svg> +| <svg title> +| <plaintext> +| "a</plaintext>b" + +#data +<!doctype html><table><tr><style></script></style>abc +#errors +(1,51): foster-parenting-character +(1,52): foster-parenting-character +(1,53): foster-parenting-character +(1,53): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "abc" +| <table> +| <tbody> +| <tr> +| <style> +| "</script>" + +#data +<!doctype html><table><tr><script></style></script>abc +#errors +(1,52): foster-parenting-character +(1,53): foster-parenting-character +(1,54): foster-parenting-character +(1,54): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "abc" +| <table> +| <tbody> +| <tr> +| <script> +| "</style>" + +#data +<!doctype html><table><caption><style></script></style>abc +#errors +(1,58): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <caption> +| <style> +| "</script>" +| "abc" + +#data +<!doctype html><table><td><style></script></style>abc +#errors +(1,26): unexpected-cell-in-table-body +(1,53): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <style> +| "</script>" +| "abc" + +#data +<!doctype html><select><script></style></script>abc +#errors +(1,51): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <script> +| "</style>" +| "abc" + +#data +<!doctype html><table><select><script></style></script>abc +#errors +(1,30): unexpected-start-tag-implies-table-voodoo +(1,58): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <script> +| "</style>" +| "abc" +| <table> + +#data +<!doctype html><table><tr><select><script></style></script>abc +#errors +(1,34): unexpected-start-tag-implies-table-voodoo +(1,62): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <script> +| "</style>" +| "abc" +| <table> +| <tbody> +| <tr> + +#data +<!doctype html><frameset></frameset><noframes>abc +#errors +(1,49): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <noframes> +| "abc" + +#data +<!doctype html><frameset></frameset><noframes>abc</noframes><!--abc--> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <noframes> +| "abc" +| <!-- abc --> + +#data +<!doctype html><frameset></frameset></html><noframes>abc +#errors +(1,56): expected-named-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <noframes> +| "abc" + +#data +<!doctype html><frameset></frameset></html><noframes>abc</noframes><!--abc--> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <noframes> +| "abc" +| <!-- abc --> + +#data +<!doctype html><table><tr></tbody><tfoot> +#errors +(1,41): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <tfoot> + +#data +<!doctype html><table><td><svg></svg>abc<td> +#errors +(1,26): unexpected-cell-in-table-body +(1,44): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <svg svg> +| "abc" +| <td> diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests19.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests19.dat new file mode 100644 index 0000000000000..20cdeabcc83c6 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests19.dat @@ -0,0 +1,1398 @@ +#data +<!doctype html><math><mn DefinitionUrl="foo"> +#errors +(1,45): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <math math> +| <math mn> +| definitionURL="foo" + +#data +<!doctype html><html></p><!--foo--> +#errors +(1,25): end-tag-after-implied-root +#document +| <!DOCTYPE html> +| <html> +| <!-- foo --> +| <head> +| <body> + +#data +<!doctype html><head></head></p><!--foo--> +#errors +(1,32): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <!-- foo --> +| <body> + +#data +<!doctype html><body><p><pre> +#errors +(1,29): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <pre> + +#data +<!doctype html><body><p><listing> +#errors +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <listing> + +#data +<!doctype html><p><plaintext> +#errors +(1,29): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <plaintext> + +#data +<!doctype html><p><h1> +#errors +(1,22): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <h1> + +#data +<!doctype html><isindex type="hidden"> +#errors +(1,38): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <isindex> +| type="hidden" + +#data +<!doctype html><ruby><p><rp> +#errors +(1,28): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <p> +| <rp> + +#data +<!doctype html><ruby><div><span><rp> +#errors +(1,36): XXX-undefined-error +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <div> +| <span> +| <rp> + +#data +<!doctype html><ruby><div><p><rp> +#errors +(1,33): XXX-undefined-error +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <div> +| <p> +| <rp> + +#data +<!doctype html><ruby><p><rt> +#errors +(1,28): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <p> +| <rt> + +#data +<!doctype html><ruby><div><span><rt> +#errors +(1,36): XXX-undefined-error +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <div> +| <span> +| <rt> + +#data +<!doctype html><ruby><div><p><rt> +#errors +(1,33): XXX-undefined-error +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <ruby> +| <div> +| <p> +| <rt> + +#data +<html><ruby>a<rb>b<rt></ruby></html> +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <ruby> +| "a" +| <rb> +| "b" +| <rt> + +#data +<html><ruby>a<rp>b<rt></ruby></html> +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <ruby> +| "a" +| <rp> +| "b" +| <rt> + +#data +<html><ruby>a<rt>b<rt></ruby></html> +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <ruby> +| "a" +| <rt> +| "b" +| <rt> + +#data +<html><ruby>a<rtc>b<rt>c<rb>d</ruby></html> +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| <html> +| <head> +| <body> +| <ruby> +| "a" +| <rtc> +| "b" +| <rt> +| "c" +| <rb> +| "d" + +#data +<!doctype html><math/><foo> +#errors +(1,27): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <math math> +| <foo> + +#data +<!doctype html><svg/><foo> +#errors +(1,26): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <svg svg> +| <foo> + +#data +<!doctype html><div></body><!--foo--> +#errors +(1,27): expected-one-end-tag-but-got-another +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <div> +| <!-- foo --> + +#data +<!doctype html><h1><div><h3><span></h1>foo +#errors +(1,39): end-tag-too-early +(1,42): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <h1> +| <div> +| <h3> +| <span> +| "foo" + +#data +<!doctype html><p></h3>foo +#errors +(1,23): end-tag-too-early +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| "foo" + +#data +<!doctype html><h3><li>abc</h2>foo +#errors +(1,31): end-tag-too-early +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <h3> +| <li> +| "abc" +| "foo" + +#data +<!doctype html><table>abc<!--foo--> +#errors +(1,23): foster-parenting-character +(1,24): foster-parenting-character +(1,25): foster-parenting-character +(1,35): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "abc" +| <table> +| <!-- foo --> + +#data +<!doctype html><table> <!--foo--> +#errors +(1,34): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| " " +| <!-- foo --> + +#data +<!doctype html><table> b <!--foo--> +#errors +(1,23): foster-parenting-character +(1,24): foster-parenting-character +(1,25): foster-parenting-character +(1,35): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| " b " +| <table> +| <!-- foo --> + +#data +<!doctype html><select><option><option> +#errors +(1,39): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <option> +| <option> + +#data +<!doctype html><select><option></optgroup> +#errors +(1,42): unexpected-end-tag-in-select +(1,42): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <option> + +#data +<!doctype html><dd><optgroup><dd> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <dd> +| <optgroup> +| <dd> + +#data +<!doctype html><p><math><mi><p><h1> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math mi> +| <p> +| <h1> + +#data +<!doctype html><p><math><mo><p><h1> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math mo> +| <p> +| <h1> + +#data +<!doctype html><p><math><mn><p><h1> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math mn> +| <p> +| <h1> + +#data +<!doctype html><p><math><ms><p><h1> +#errors +(1,35): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math ms> +| <p> +| <h1> + +#data +<!doctype html><p><math><mtext><p><h1> +#errors +(1,38): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math mtext> +| <p> +| <h1> + +#data +<!doctype html><frameset></noframes> +#errors +(1,36): unexpected-end-tag-in-frameset +(1,36): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><html c=d><body></html><html a=b> +#errors +(1,48): non-html-root +#document +| <!DOCTYPE html> +| <html> +| a="b" +| c="d" +| <head> +| <body> + +#data +<!doctype html><html c=d><frameset></frameset></html><html a=b> +#errors +(1,63): non-html-root +#document +| <!DOCTYPE html> +| <html> +| a="b" +| c="d" +| <head> +| <frameset> + +#data +<!doctype html><html><frameset></frameset></html><!--foo--> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <!-- foo --> + +#data +<!doctype html><html><frameset></frameset></html> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| " " + +#data +<!doctype html><html><frameset></frameset></html>abc +#errors +(1,50): expected-eof-but-got-char +(1,51): expected-eof-but-got-char +(1,52): expected-eof-but-got-char +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><html><frameset></frameset></html><p> +#errors +(1,52): expected-eof-but-got-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><html><frameset></frameset></html></p> +#errors +(1,53): expected-eof-but-got-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<html><frameset></frameset></html><!doctype html> +#errors +(1,6): expected-doctype-but-got-start-tag +(1,49): unexpected-doctype +#document +| <html> +| <head> +| <frameset> + +#data +<!doctype html><body><frameset> +#errors +(1,31): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> + +#data +<!doctype html><p><frameset><frame> +#errors +(1,28): unexpected-start-tag +(1,35): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<!doctype html><p>a<frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| "a" + +#data +<!doctype html><p> <frameset><frame> +#errors +(1,29): unexpected-start-tag +(1,36): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<!doctype html><pre><frameset> +#errors +(1,30): unexpected-start-tag +(1,30): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <pre> + +#data +<!doctype html><listing><frameset> +#errors +(1,34): unexpected-start-tag +(1,34): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <listing> + +#data +<!doctype html><li><frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <li> + +#data +<!doctype html><dd><frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <dd> + +#data +<!doctype html><dt><frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <dt> + +#data +<!doctype html><button><frameset> +#errors +(1,33): unexpected-start-tag +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <button> + +#data +<!doctype html><applet><frameset> +#errors +(1,33): unexpected-start-tag +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <applet> + +#data +<!doctype html><marquee><frameset> +#errors +(1,34): unexpected-start-tag +(1,34): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <marquee> + +#data +<!doctype html><object><frameset> +#errors +(1,33): unexpected-start-tag +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <object> + +#data +<!doctype html><table><frameset> +#errors +(1,32): unexpected-start-tag-implies-table-voodoo +(1,32): unexpected-start-tag +(1,32): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> + +#data +<!doctype html><area><frameset> +#errors +(1,31): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <area> + +#data +<!doctype html><basefont><frameset> +#errors +(1,35): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <basefont> +| <frameset> + +#data +<!doctype html><bgsound><frameset> +#errors +(1,34): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <bgsound> +| <frameset> + +#data +<!doctype html><br><frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <br> + +#data +<!doctype html><embed><frameset> +#errors +(1,32): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <embed> + +#data +<!doctype html><img><frameset> +#errors +(1,30): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <img> + +#data +<!doctype html><input><frameset> +#errors +(1,32): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <input> + +#data +<!doctype html><keygen><frameset> +#errors +(1,33): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <keygen> + +#data +<!doctype html><wbr><frameset> +#errors +(1,30): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <wbr> + +#data +<!doctype html><hr><frameset> +#errors +(1,29): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <hr> + +#data +<!doctype html><textarea></textarea><frameset> +#errors +(1,46): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <textarea> + +#data +<!doctype html><xmp></xmp><frameset> +#errors +(1,36): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <xmp> + +#data +<!doctype html><iframe></iframe><frameset> +#errors +(1,42): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <iframe> + +#data +<!doctype html><select></select><frameset> +#errors +(1,42): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> + +#data +<!doctype html><svg></svg><frameset><frame> +#errors +(1,36): unexpected-start-tag +(1,43): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<!doctype html><math></math><frameset><frame> +#errors +(1,38): unexpected-start-tag +(1,45): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<!doctype html><svg><foreignObject><div> <frameset><frame> +#errors +(1,51): unexpected-start-tag +(1,58): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<!doctype html><svg>a</svg><frameset><frame> +#errors +(1,37): unexpected-start-tag +(1,44): unexpected-start-tag-ignored +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <svg svg> +| "a" + +#data +<!doctype html><svg> </svg><frameset><frame> +#errors +(1,37): unexpected-start-tag +(1,44): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| <frame> + +#data +<html>aaa<frameset></frameset> +#errors +(1,6): expected-doctype-but-got-start-tag +(1,19): unexpected-start-tag +(1,30): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| "aaa" + +#data +<html> a <frameset></frameset> +#errors +(1,6): expected-doctype-but-got-start-tag +(1,19): unexpected-start-tag +(1,30): unexpected-end-tag +#document +| <html> +| <head> +| <body> +| "a " + +#data +<!doctype html><div><frameset> +#errors +(1,30): unexpected-start-tag +(1,30): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!doctype html><div><body><frameset> +#errors +(1,26): unexpected-start-tag +(1,36): unexpected-start-tag +(1,36): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <div> + +#data +<!doctype html><p><math></p>a +#errors +(1,28): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| "a" + +#data +<!doctype html><p><math><mn><span></p>a +#errors +(1,38): unexpected-end-tag +(1,39): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <math math> +| <math mn> +| <span> +| <p> +| "a" + +#data +<!doctype html><math></html> +#errors +(1,28): unexpected-end-tag +(1,28): expected-one-end-tag-but-got-another +(1,28): unexpected-end-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <math math> + +#data +<!doctype html><meta charset="ascii"> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <meta> +| charset="ascii" +| <body> + +#data +<!doctype html><meta http-equiv="content-type" content="text/html;charset=ascii"> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <meta> +| content="text/html;charset=ascii" +| http-equiv="content-type" +| <body> + +#data +<!doctype html><head><!--aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa--><meta charset="utf8"> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <!-- aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa --> +| <meta> +| charset="utf8" +| <body> + +#data +<!doctype html><html a=b><head></head><html c=d> +#errors +(1,48): non-html-root +#document +| <!DOCTYPE html> +| <html> +| a="b" +| c="d" +| <head> +| <body> + +#data +<!doctype html><image/> +#errors +(1,23): image-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <img> + +#data +<!doctype html>a<i>b<table>c<b>d</i>e</b>f +#errors +(1,28): foster-parenting-character +(1,31): foster-parenting-start-tag +(1,32): foster-parenting-character +(1,36): foster-parenting-end-tag +(1,36): adoption-agency-1.3 +(1,37): foster-parenting-character +(1,41): foster-parenting-end-tag +(1,42): foster-parenting-character +(1,42): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "a" +| <i> +| "bc" +| <b> +| "de" +| "f" +| <table> + +#data +<!doctype html><table><i>a<b>b<div>c<a>d</i>e</b>f +#errors +(1,25): foster-parenting-start-tag +(1,26): foster-parenting-character +(1,29): foster-parenting-start-tag +(1,30): foster-parenting-character +(1,35): foster-parenting-start-tag +(1,36): foster-parenting-character +(1,39): foster-parenting-start-tag +(1,40): foster-parenting-character +(1,44): foster-parenting-end-tag +(1,44): adoption-agency-1.3 +(1,44): adoption-agency-1.3 +(1,45): foster-parenting-character +(1,49): foster-parenting-end-tag +(1,49): adoption-agency-1.3 +(1,49): adoption-agency-1.3 +(1,50): foster-parenting-character +(1,50): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <i> +| "a" +| <b> +| "b" +| <b> +| <div> +| <b> +| <i> +| "c" +| <a> +| "d" +| <a> +| "e" +| <a> +| "f" +| <table> + +#data +<!doctype html><i>a<b>b<div>c<a>d</i>e</b>f +#errors +(1,37): adoption-agency-1.3 +(1,37): adoption-agency-1.3 +(1,42): adoption-agency-1.3 +(1,42): adoption-agency-1.3 +(1,43): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <i> +| "a" +| <b> +| "b" +| <b> +| <div> +| <b> +| <i> +| "c" +| <a> +| "d" +| <a> +| "e" +| <a> +| "f" + +#data +<!doctype html><table><i>a<b>b<div>c</i> +#errors +(1,25): foster-parenting-start-tag +(1,26): foster-parenting-character +(1,29): foster-parenting-start-tag +(1,30): foster-parenting-character +(1,35): foster-parenting-start-tag +(1,36): foster-parenting-character +(1,40): foster-parenting-end-tag +(1,40): adoption-agency-1.3 +(1,40): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <i> +| "a" +| <b> +| "b" +| <b> +| <div> +| <i> +| "c" +| <table> + +#data +<!doctype html><table><i>a<div>b<tr>c<b>d</i>e +#errors +(1,25): foster-parenting-start-tag +(1,26): foster-parenting-character +(1,31): foster-parenting-start-tag +(1,32): foster-parenting-character +(1,37): foster-parenting-character +(1,40): foster-parenting-start-tag +(1,41): foster-parenting-character +(1,45): foster-parenting-end-tag +(1,45): adoption-agency-1.3 +(1,46): foster-parenting-character +(1,46): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <i> +| "a" +| <div> +| "b" +| <i> +| "c" +| <b> +| "d" +| <b> +| "e" +| <table> +| <tbody> +| <tr> + +#data +<!doctype html><table><td><table><i>a<div>b<b>c</i>d +#errors +(1,26): unexpected-cell-in-table-body +(1,36): foster-parenting-start-tag +(1,37): foster-parenting-character +(1,42): foster-parenting-start-tag +(1,43): foster-parenting-character +(1,46): foster-parenting-start-tag +(1,47): foster-parenting-character +(1,51): foster-parenting-end-tag +(1,51): adoption-agency-1.3 +(1,51): adoption-agency-1.3 +(1,52): foster-parenting-character +(1,52): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| <i> +| "a" +| <div> +| <i> +| "b" +| <b> +| "c" +| <b> +| "d" +| <table> + +#data +<!doctype html><body><bgsound> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <bgsound> + +#data +<!doctype html><body><basefont> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <basefont> + +#data +<!doctype html><a><b></a><basefont> +#errors +(1,25): adoption-agency-1.3 +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <a> +| <b> +| <basefont> + +#data +<!doctype html><a><b></a><bgsound> +#errors +(1,25): adoption-agency-1.3 +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <a> +| <b> +| <bgsound> + +#data +<!doctype html><figcaption><article></figcaption>a +#errors +(1,49): end-tag-too-early +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <figcaption> +| <article> +| "a" + +#data +<!doctype html><summary><article></summary>a +#errors +(1,43): end-tag-too-early +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <summary> +| <article> +| "a" + +#data +<!doctype html><p><a><plaintext>b +#errors +(1,32): unexpected-end-tag +(1,33): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <a> +| <plaintext> +| <a> +| "b" + +#data +<!DOCTYPE html><div>a<a></div>b<p>c</p>d +#errors +(1,30): end-tag-too-early +(1,40): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <div> +| "a" +| <a> +| <a> +| "b" +| <p> +| "c" +| "d" diff --git a/tests/phpunit/data/html5lib-tests/tree-construction/tests2.dat b/tests/phpunit/data/html5lib-tests/tree-construction/tests2.dat new file mode 100644 index 0000000000000..11ef9b1643ff4 --- /dev/null +++ b/tests/phpunit/data/html5lib-tests/tree-construction/tests2.dat @@ -0,0 +1,831 @@ +#data +<!DOCTYPE html>Test +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "Test" + +#data +<textarea>test</div>test +#errors +(1,10): expected-doctype-but-got-start-tag +(1,24): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <textarea> +| "test</div>test" + +#data +<table><td> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,11): unexpected-cell-in-table-body +(1,11): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> + +#data +<table><td>test</tbody></table> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,11): unexpected-cell-in-table-body +#document +| <html> +| <head> +| <body> +| <table> +| <tbody> +| <tr> +| <td> +| "test" + +#data +<frame>test +#errors +(1,7): expected-doctype-but-got-start-tag +(1,7): unexpected-start-tag-ignored +#document +| <html> +| <head> +| <body> +| "test" + +#data +<!DOCTYPE html><frameset>test +#errors +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!DOCTYPE html><frameset> te st +#errors +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): unexpected-char-in-frameset +(1,29): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| " " + +#data +<!DOCTYPE html><frameset></frameset> te st +#errors +(1,29): unexpected-char-after-frameset +(1,29): unexpected-char-after-frameset +(1,29): unexpected-char-after-frameset +(1,29): unexpected-char-after-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> +| " " + +#data +<!DOCTYPE html><frameset><!DOCTYPE html> +#errors +(1,40): unexpected-doctype +(1,40): eof-in-frameset +#document +| <!DOCTYPE html> +| <html> +| <head> +| <frameset> + +#data +<!DOCTYPE html><font><p><b>test</font> +#errors +(1,38): adoption-agency-1.3 +(1,38): adoption-agency-1.3 +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <font> +| <p> +| <font> +| <b> +| "test" + +#data +<!DOCTYPE html><dt><div><dd> +#errors +(1,28): end-tag-too-early +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <dt> +| <div> +| <dd> + +#data +<script></x +#errors +(1,8): expected-doctype-but-got-start-tag +(1,11): expected-named-closing-tag-but-got-eof +#document +| <html> +| <head> +| <script> +| "</x" +| <body> + +#data +<table><plaintext><td> +#errors +(1,7): expected-doctype-but-got-start-tag +(1,18): unexpected-start-tag-implies-table-voodoo +(1,22): foster-parenting-character-in-table +(1,22): foster-parenting-character-in-table +(1,22): foster-parenting-character-in-table +(1,22): foster-parenting-character-in-table +(1,22): eof-in-table +#document +| <html> +| <head> +| <body> +| <plaintext> +| "<td>" +| <table> + +#data +<plaintext></plaintext> +#errors +(1,11): expected-doctype-but-got-start-tag +(1,23): expected-closing-tag-but-got-eof +#document +| <html> +| <head> +| <body> +| <plaintext> +| "</plaintext>" + +#data +<!DOCTYPE html><table><tr>TEST +#errors +(1,30): foster-parenting-character-in-table +(1,30): foster-parenting-character-in-table +(1,30): foster-parenting-character-in-table +(1,30): foster-parenting-character-in-table +(1,30): eof-in-table +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "TEST" +| <table> +| <tbody> +| <tr> + +#data +<!DOCTYPE html><body t1=1><body t2=2><body t3=3 t4=4> +#errors +(1,37): unexpected-start-tag +(1,53): unexpected-start-tag +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| t1="1" +| t2="2" +| t3="3" +| t4="4" + +#data +</b test +#errors +(1,8): eof-in-attribute-name +(1,8): expected-doctype-but-got-eof +#new-errors +(1:9) eof-in-tag +#document +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html></b test<b &=&>X +#errors +(1,24): invalid-character-in-attribute-name +(1,32): named-entity-without-semicolon +(1,33): attributes-in-end-tag +(1,33): unexpected-end-tag-before-html +#new-errors +(1:24) unexpected-character-in-attribute-name +(1:33) missing-semicolon-after-character-reference +(1:33) end-tag-with-attributes +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "X" + +#data +<!doctypehtml><scrIPt type=text/x-foobar;baz>X</SCRipt +#errors +(1,9): need-space-after-doctype +(1,54): expected-named-closing-tag-but-got-eof +#new-errors +(1:10) missing-whitespace-before-doctype-name +#document +| <!DOCTYPE html> +| <html> +| <head> +| <script> +| type="text/x-foobar;baz" +| "X</SCRipt" +| <body> + +#data +& +#errors +(1,1): expected-doctype-but-got-chars +#document +| <html> +| <head> +| <body> +| "&" + +#data +&# +#errors +(1,2): expected-numeric-entity +(1,2): expected-doctype-but-got-chars +#new-errors +(1:3) absence-of-digits-in-numeric-character-reference +#document +| <html> +| <head> +| <body> +| "&#" + +#data +&#X +#errors +(1,3): expected-numeric-entity +(1,3): expected-doctype-but-got-chars +#new-errors +(1:4) absence-of-digits-in-numeric-character-reference +#document +| <html> +| <head> +| <body> +| "&#X" + +#data +&#x +#errors +(1,3): expected-numeric-entity +(1,3): expected-doctype-but-got-chars +#new-errors +(1:4) absence-of-digits-in-numeric-character-reference +#document +| <html> +| <head> +| <body> +| "&#x" + +#data +- +#errors +(1,4): numeric-entity-without-semicolon +(1,4): expected-doctype-but-got-chars +#new-errors +(1:5) missing-semicolon-after-character-reference +#document +| <html> +| <head> +| <body> +| "-" + +#data +&x-test +#errors +(1,2): expected-doctype-but-got-chars +#document +| <html> +| <head> +| <body> +| "&x-test" + +#data +<!doctypehtml><p><li> +#errors +(1,9): need-space-after-doctype +#new-errors +(1:10) missing-whitespace-before-doctype-name +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <li> + +#data +<!doctypehtml><p><dt> +#errors +(1,9): need-space-after-doctype +#new-errors +(1:10) missing-whitespace-before-doctype-name +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <dt> + +#data +<!doctypehtml><p><dd> +#errors +(1,9): need-space-after-doctype +#new-errors +(1:10) missing-whitespace-before-doctype-name +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <dd> + +#data +<!doctypehtml><p><form> +#errors +(1,9): need-space-after-doctype +(1,23): expected-closing-tag-but-got-eof +#new-errors +(1:10) missing-whitespace-before-doctype-name +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| <form> + +#data +<!DOCTYPE html><p></P>X +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <p> +| "X" + +#data +& +#errors +(1,4): named-entity-without-semicolon +(1,4): expected-doctype-but-got-chars +#new-errors +(1:5) missing-semicolon-after-character-reference +#document +| <html> +| <head> +| <body> +| "&" + +#data +&AMp; +#errors +(1,3): expected-named-entity +(1,3): expected-doctype-but-got-chars +#new-errors +(1:5) unknown-named-character-reference +#document +| <html> +| <head> +| <body> +| "&AMp;" + +#data +<!DOCTYPE html><html><head></head><body><thisISasillyTESTelementNameToMakeSureCrazyTagNamesArePARSEDcorrectLY> +#errors +(1,110): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <thisisasillytestelementnametomakesurecrazytagnamesareparsedcorrectly> + +#data +<!DOCTYPE html>X</body>X +#errors +(1,24): unexpected-char-after-body +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| "XX" + +#data +<!DOCTYPE html><!-- X +#errors +(1,21): eof-in-comment +#new-errors +(1:22) eof-in-comment +#document +| <!DOCTYPE html> +| <!-- X --> +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html><table><caption>test TEST</caption><td>test +#errors +(1,54): unexpected-cell-in-table-body +(1,58): expected-closing-tag-but-got-eof +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <table> +| <caption> +| "test TEST" +| <tbody> +| <tr> +| <td> +| "test" + +#data +<!DOCTYPE html><select><option><optgroup> +#errors +(1,41): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <option> +| <optgroup> + +#data +<!DOCTYPE html><select><optgroup><option></optgroup><option><select><option> +#errors +(1,68): unexpected-select-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <optgroup> +| <option> +| <option> +| <option> + +#data +<!DOCTYPE html><select><optgroup><option><optgroup> +#errors +(1,51): eof-in-select +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <optgroup> +| <option> +| <optgroup> + +#data +<!DOCTYPE html><datalist><option>foo</datalist>bar +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <datalist> +| <option> +| "foo" +| "bar" + +#data +<!DOCTYPE html><font><input><input></font> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <font> +| <input> +| <input> + +#data +<!DOCTYPE html><!-- XXX - XXX --> +#errors +#document +| <!DOCTYPE html> +| <!-- XXX - XXX --> +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html><!-- XXX - XXX +#errors +(1,29): eof-in-comment +#new-errors +(1:30) eof-in-comment +#document +| <!DOCTYPE html> +| <!-- XXX - XXX --> +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html><!-- XXX - XXX - XXX --> +#errors +#document +| <!DOCTYPE html> +| <!-- XXX - XXX - XXX --> +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html> <!DOCTYPE html> +#errors +Line: 1 Col: 31 Unexpected DOCTYPE. Ignored. +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> + +#data +test +test +#errors +(2,4): expected-doctype-but-got-chars +#document +| <html> +| <head> +| <body> +| "test +test" + +#data +<!DOCTYPE html><body><title>test</body> +#errors +#document +| +| +| +| +| +| "test</body>" + +#data +<!DOCTYPE html><body><title>X +#errors +#document +| +| +| +| +| +| "X" +| <meta> +| name="z" +| <link> +| rel="foo" +| <style> +| " +x { content:"</style" } " + +#data +<!DOCTYPE html><select><optgroup></optgroup></select> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> +| <select> +| <optgroup> + +#data + + +#errors +(2,1): expected-doctype-but-got-eof +#document +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html> <html> +#errors +#document +| <!DOCTYPE html> +| <html> +| <head> +| <body> + +#data +<!DOCTYPE html><script> +</script> <title>x +#errors +#document +| +| +| +| +#errors +(1,6): expected-doctype-but-got-start-tag +(1,21): unexpected-start-tag-out-of-my-head +#document +| +| +| +#errors +(1,6): expected-doctype-but-got-start-tag +(1,28): unexpected-start-tag-out-of-my-head +(1,52): unexpected-start-tag-out-of-my-head +#document +| +| +| +#errors +(1,6): expected-doctype-but-got-start-tag +#document +| +| +| +| +| "x" +| x +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| +| +| --> x +#errors +(1,7): expected-doctype-but-got-start-tag +(1,34): unexpected-end-tag +#document +| +| +| x +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| +| +| x +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| +| +| x +#errors +(1,7): expected-doctype-but-got-start-tag +#document +| +| +|

      +#errors +#document +| +| +| +| +| +| ddd +#errors +(1,6): expected-doctype-but-got-start-tag +(1,21): unexpected-start-tag-out-of-my-head +#document +| +| +| +#errors +(1,3): expected-doctype-but-got-start-tag +(1,41): adoption-agency-1.3 +#document +| +| +| +| +|
    • +| +| ', + ), + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpRenderDimensionsSupport.php b/tests/phpunit/tests/block-supports/wpRenderDimensionsSupport.php new file mode 100644 index 0000000000000..3e2894c5538bf --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpRenderDimensionsSupport.php @@ -0,0 +1,174 @@ +test_block_name = null; + $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + + // Clear up the filters to modify the theme root. + remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + /** + * Tests that dimensions block support works as expected. + * + * @ticket 60365 + * + * @covers ::wp_render_dimensions_support + * + * @dataProvider data_dimensions_block_support + * + * @param string $theme_name The theme to switch to. + * @param string $block_name The test block name to register. + * @param mixed $dimensions_settings The dimensions block support settings. + * @param mixed $dimensions_style The dimensions styles within the block attributes. + * @param string $expected_wrapper Expected markup for the block wrapper. + * @param string $wrapper Existing markup for the block wrapper. + */ + public function test_dimensions_block_support( $theme_name, $block_name, $dimensions_settings, $dimensions_style, $expected_wrapper, $wrapper ) { + switch_theme( $theme_name ); + $this->test_block_name = $block_name; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'dimensions' => $dimensions_settings, + ), + ) + ); + + $block = array( + 'blockName' => $block_name, + 'attrs' => array( + 'style' => array( + 'dimensions' => $dimensions_style, + ), + ), + ); + + $actual = wp_render_dimensions_support( $wrapper, $block ); + + $this->assertSame( + $expected_wrapper, + $actual, + 'Dimensions block wrapper markup should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_dimensions_block_support() { + return array( + 'aspect ratio style is applied, with min-height unset' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/dimensions-rules-are-output', + 'dimensions_settings' => array( + 'aspectRatio' => true, + ), + 'dimensions_style' => array( + 'aspectRatio' => '16/9', + ), + 'expected_wrapper' => '
      Content
      ', + 'wrapper' => '
      Content
      ', + ), + 'dimensions style is appended if a style attribute already exists' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/dimensions-rules-are-output', + 'dimensions_settings' => array( + 'aspectRatio' => true, + ), + 'dimensions_style' => array( + 'aspectRatio' => '16/9', + ), + 'expected_wrapper' => '
      Content
      ', + 'wrapper' => '
      Content
      ', + ), + 'aspect ratio style is unset if block has min-height set' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/dimensions-rules-are-output', + 'dimensions_settings' => array( + 'aspectRatio' => true, + ), + 'dimensions_style' => array( + 'minHeight' => '100px', + ), + 'expected_wrapper' => '
      Content
      ', + 'wrapper' => '
      Content
      ', + ), + 'aspect ratio style is not applied if the block does not support aspect ratio' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/background-rules-are-not-output', + 'dimensions_settings' => array( + 'aspectRatio' => false, + ), + 'dimensions_style' => array( + 'aspectRatio' => '16/9', + ), + 'expected_wrapper' => '
      Content
      ', + 'wrapper' => '
      Content
      ', + ), + ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php new file mode 100644 index 0000000000000..9103fcba90b66 --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupport.php @@ -0,0 +1,223 @@ +test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + /** + * Tests that block supports leaves block content alone if the block type + * isn't registered. + * + * @ticket 59578 + * + * @covers ::wp_render_elements_support + */ + public function test_leaves_block_content_alone_when_block_type_not_registered() { + $block = array( + 'blockName' => 'test/element-block-supports', + 'attrs' => array( + 'style' => array( + 'elements' => array( + 'button' => array( + 'color' => array( + 'text' => 'var:preset|color|vivid-red', + 'background' => '#fff', + ), + ), + ), + ), + ), + ); + + $block_markup = '

      Hello WordPress!

      '; + $actual = wp_render_elements_class_name( $block_markup, $block ); + + $this->assertSame( $block_markup, $actual, 'Expected to leave block content unmodified, but found changes.' ); + } + + /** + * Tests that elements block support applies the correct classname. + * + * @ticket 59555 + * + * @covers ::wp_render_elements_support + * + * @dataProvider data_elements_block_support_class + * + * @param array $color_settings The color block support settings used for elements support. + * @param array $elements_styles The elements styles within the block attributes. + * @param string $block_markup Original block markup. + * @param string $expected_markup Resulting markup after application of elements block support. + */ + public function test_elements_block_support_class( $color_settings, $elements_styles, $block_markup, $expected_markup ) { + $this->test_block_name = 'test/element-block-supports'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => $color_settings, + ), + ) + ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'elements' => $elements_styles, + ), + ), + ); + + /* + * To ensure a consistent elements class name it is generated within a + * `render_block_data` filter and stored in the `className` attribute. + * As a result, the block data needs to be passed through the same + * function for this test. + */ + $filtered_block = wp_render_elements_support_styles( $block ); + $actual = wp_render_elements_class_name( $block_markup, $filtered_block ); + + $this->assertMatchesRegularExpression( + $expected_markup, + $actual, + 'Block wrapper markup should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_elements_block_support_class() { + $color_styles = array( + 'text' => 'var:preset|color|vivid-red', + 'background' => '#fff', + ); + + return array( + // @ticket 59578 + 'empty block markup remains untouched' => array( + 'color_settings' => array( + 'button' => true, + ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'block_markup' => '', + 'expected_markup' => '/^$/', + ), + 'empty block markup remains untouched when no block attributes' => array( + 'color_settings' => array( + 'button' => true, + ), + 'elements_styles' => null, + 'block_markup' => '', + 'expected_markup' => '/^$/', + ), + 'block markup remains untouched when block has no attributes' => array( + 'color_settings' => array( + 'button' => true, + ), + 'elements_styles' => null, + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + // @ticket 5418 + 'button element styles with serialization skipped' => array( + 'color_settings' => array( + 'button' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'link element styles with serialization skipped' => array( + 'color_settings' => array( + 'link' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'heading element styles with serialization skipped' => array( + 'color_settings' => array( + 'heading' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'button element styles apply class to wrapper' => array( + 'color_settings' => array( 'button' => true ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'link element styles apply class to wrapper' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'heading element styles apply class to wrapper' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'element styles apply class to wrapper when it has other classes' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + 'element styles apply class to wrapper when it has other attributes' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( 'color' => $color_styles ), + ), + 'block_markup' => '

      Hello WordPress!

      ', + 'expected_markup' => '/^

      Hello WordPress<\/a>!<\/p>$/', + ), + ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php new file mode 100644 index 0000000000000..16ed26fc9c7bc --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpRenderElementsSupportStyles.php @@ -0,0 +1,171 @@ +test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + /** + * Tests that elements block support generates appropriate styles. + * + * @ticket 59555 + * @ticket 60557 + * + * @covers ::wp_render_elements_support_styles + * + * @dataProvider data_elements_block_support_styles + * + * @param mixed $color_settings The color block support settings used for elements support. + * @param mixed $elements_styles The elements styles within the block attributes. + * @param string $expected_styles Expected styles enqueued by the style engine. + */ + public function test_elements_block_support_styles( $color_settings, $elements_styles, $expected_styles ) { + $this->test_block_name = 'test/element-block-supports'; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 3, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'color' => $color_settings, + ), + ) + ); + + $block = array( + 'blockName' => $this->test_block_name, + 'attrs' => array( + 'style' => array( + 'elements' => $elements_styles, + ), + ), + ); + + wp_render_elements_support_styles( $block ); + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( 'block-supports', array( 'prettify' => false ) ); + + $this->assertMatchesRegularExpression( + $expected_styles, + $actual_stylesheet, + 'Elements style rules output should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_elements_block_support_styles() { + $color_styles = array( + 'text' => 'var:preset|color|vivid-red', + 'background' => '#fff', + ); + $color_css_rules = preg_quote( '{color:var(--wp--preset--color--vivid-red);background-color:#fff;}' ); + + return array( + 'button element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'button' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^$/', + ), + 'link element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'link' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'link' => array( + 'color' => $color_styles, + ':hover' => array( + 'color' => $color_styles, + ), + ), + ), + 'expected_styles' => '/^$/', + ), + 'heading element styles are not applied if serialization is skipped' => array( + 'color_settings' => array( + 'heading' => true, + '__experimentalSkipSerialization' => true, + ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + 'h1' => array( 'color' => $color_styles ), + 'h2' => array( 'color' => $color_styles ), + 'h3' => array( 'color' => $color_styles ), + 'h4' => array( 'color' => $color_styles ), + 'h5' => array( 'color' => $color_styles ), + 'h6' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^$/', + ), + 'button element styles are applied' => array( + 'color_settings' => array( 'button' => true ), + 'elements_styles' => array( + 'button' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} .wp-element-button, .wp-elements-[a-f0-9]{32} .wp-block-button__link' . $color_css_rules . '$/', + ), + 'link element styles are applied' => array( + 'color_settings' => array( 'link' => true ), + 'elements_styles' => array( + 'link' => array( + 'color' => $color_styles, + ':hover' => array( + 'color' => $color_styles, + ), + ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} a:where\(:not\(.wp-element-button\)\)' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} a:where\(:not\(.wp-element-button\)\):hover' . $color_css_rules . '$/', + ), + 'generic heading element styles are applied' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'heading' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1, .wp-elements-[a-f0-9]{32} h2, .wp-elements-[a-f0-9]{32} h3, .wp-elements-[a-f0-9]{32} h4, .wp-elements-[a-f0-9]{32} h5, .wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + ), + 'individual heading element styles are applied' => array( + 'color_settings' => array( 'heading' => true ), + 'elements_styles' => array( + 'h1' => array( 'color' => $color_styles ), + 'h2' => array( 'color' => $color_styles ), + 'h3' => array( 'color' => $color_styles ), + 'h4' => array( 'color' => $color_styles ), + 'h5' => array( 'color' => $color_styles ), + 'h6' => array( 'color' => $color_styles ), + ), + 'expected_styles' => '/^.wp-elements-[a-f0-9]{32} h1' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h2' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h3' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h4' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h5' . $color_css_rules . + '.wp-elements-[a-f0-9]{32} h6' . $color_css_rules . '$/', + ), + ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php new file mode 100644 index 0000000000000..f2d7d2ecf81a7 --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpRenderPositionSupport.php @@ -0,0 +1,184 @@ +test_block_name = null; + $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', $this->theme_root ); + + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + public function tear_down() { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + + // Clear up the filters to modify the theme root. + remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + unregister_block_type( $this->test_block_name ); + $this->test_block_name = null; + parent::tear_down(); + } + + public function filter_set_theme_root() { + return $this->theme_root; + } + + /** + * Tests that position block support works as expected. + * + * @ticket 57618 + * + * @covers ::wp_render_position_support + * + * @dataProvider data_position_block_support + * + * @param string $theme_name The theme to switch to. + * @param string $block_name The test block name to register. + * @param mixed $position_settings The position block support settings. + * @param mixed $position_style The position styles within the block attributes. + * @param string $expected_wrapper Expected markup for the block wrapper. + * @param string $expected_styles Expected styles enqueued by the style engine. + */ + public function test_position_block_support( $theme_name, $block_name, $position_settings, $position_style, $expected_wrapper, $expected_styles ) { + switch_theme( $theme_name ); + $this->test_block_name = $block_name; + + register_block_type( + $this->test_block_name, + array( + 'api_version' => 2, + 'attributes' => array( + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + 'position' => $position_settings, + ), + ) + ); + + $block = array( + 'blockName' => 'test/position-rules-are-output', + 'attrs' => array( + 'style' => array( + 'position' => $position_style, + ), + ), + ); + + $actual = wp_render_position_support( '

      ', $block ); + + $this->assertMatchesRegularExpression( + $expected_wrapper, + $actual, + 'Position block wrapper markup should be correct' + ); + + $actual_stylesheet = wp_style_engine_get_stylesheet_from_context( + 'block-supports', + array( + 'prettify' => false, + ) + ); + + $this->assertMatchesRegularExpression( + $expected_styles, + $actual_stylesheet, + 'Position style rules output should be correct' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_position_block_support() { + return array( + 'sticky position style is applied' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-are-output', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
      Content<\/div>$/', + 'expected_styles' => '/^.wp-container-\d+' . preg_quote( '{top:calc(0px + var(--wp-admin--admin-bar--position-offset, 0px));position:sticky;z-index:10;}' ) . '$/', + ), + 'sticky position style is not applied if theme does not support it' => array( + 'theme_name' => 'default', + 'block_name' => 'test/position-rules-without-theme-support', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
      Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + 'sticky position style is not applied if block does not support it' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-without-block-support', + 'position_settings' => false, + 'position_style' => array( + 'type' => 'sticky', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
      Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + 'sticky position style is not applied if type is not valid' => array( + 'theme_name' => 'block-theme-child-with-fluid-typography', + 'block_name' => 'test/position-rules-with-valid-type', + 'position_settings' => true, + 'position_style' => array( + 'type' => 'illegal-type', + 'top' => '0px', + ), + 'expected_wrapper' => '/^
      Content<\/div>$/', + 'expected_styles' => '/^$/', + ), + ); + } +} diff --git a/tests/phpunit/tests/block-supports/wpStripCustomCssFromBlocks.php b/tests/phpunit/tests/block-supports/wpStripCustomCssFromBlocks.php new file mode 100644 index 0000000000000..1b6076ee185a4 --- /dev/null +++ b/tests/phpunit/tests/block-supports/wpStripCustomCssFromBlocks.php @@ -0,0 +1,238 @@ +assertArrayNotHasKey( 'css', $blocks[0]['attrs']['style'] ?? array(), $message ); + $this->assertArrayNotHasKey( 'style', $blocks[0]['attrs'] ?? array(), 'style key should be fully removed when css was the only property.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_strips_css_from_blocks() { + return array( + 'single block' => array( + 'content' => '

      Hello

      ', + 'message' => 'style.css should be stripped from block attributes.', + ), + ); + } + + /** + * Tests that style.css is stripped from nested inner blocks. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_strips_css_from_inner_blocks() { + $content = '

      Hello

      '; + + $result = wp_unslash( wp_strip_custom_css_from_blocks( $content ) ); + $blocks = parse_blocks( $result ); + + $inner_block = $blocks[0]['innerBlocks'][0]; + $this->assertArrayNotHasKey( 'css', $inner_block['attrs']['style'] ?? array(), 'style.css should be stripped from inner block attributes.' ); + } + + /** + * Tests that content without blocks is returned unchanged. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_returns_non_block_content_unchanged() { + $content = '

      This is plain HTML content with no blocks.

      '; + + $result = wp_strip_custom_css_from_blocks( $content ); + + $this->assertSame( $content, $result, 'Non-block content should be returned unchanged.' ); + } + + /** + * Tests that content without style.css attributes is returned unchanged. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_returns_unchanged_when_no_css_attributes() { + $content = '

      Hello

      '; + + $result = wp_strip_custom_css_from_blocks( $content ); + + $this->assertSame( $content, $result, 'Content without style.css attributes should be returned unchanged.' ); + } + + /** + * Tests that other style properties are preserved when css is stripped. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_preserves_other_style_properties() { + $content = '

      Hello

      '; + + $result = wp_unslash( wp_strip_custom_css_from_blocks( $content ) ); + $blocks = parse_blocks( $result ); + + $this->assertArrayNotHasKey( 'css', $blocks[0]['attrs']['style'], 'style.css should be stripped.' ); + $this->assertSame( '#ff0000', $blocks[0]['attrs']['style']['color']['text'], 'Other style properties should be preserved.' ); + } + + /** + * Tests that empty style object is cleaned up after stripping css. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_cleans_up_empty_style_object() { + $content = '

      Hello

      '; + + $result = wp_unslash( wp_strip_custom_css_from_blocks( $content ) ); + $blocks = parse_blocks( $result ); + + $this->assertArrayNotHasKey( 'style', $blocks[0]['attrs'], 'Empty style object should be cleaned up after stripping css.' ); + } + + /** + * Tests that slashed content is handled correctly. + * + * @covers ::wp_strip_custom_css_from_blocks + * @ticket 64771 + */ + public function test_handles_slashed_content() { + $content = '

      Hello

      '; + $slashed = wp_slash( $content ); + + $result = wp_strip_custom_css_from_blocks( $slashed ); + $blocks = parse_blocks( wp_unslash( $result ) ); + + $this->assertArrayNotHasKey( 'css', $blocks[0]['attrs']['style'] ?? array(), 'style.css should be stripped even from slashed content.' ); + } + + /** + * Tests that the content_save_pre filter is added for a user without edit_css. + * + * @ticket 64771 + * + * @covers ::wp_custom_css_kses_init + * @covers ::wp_custom_css_kses_init_filters + */ + public function test_filter_added_for_user_without_edit_css() { + $author_id = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $author_id ); + wp_custom_css_kses_init(); + + $this->assertSame( 8, has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'content_save_pre filter should be added at priority 8 for users without edit_css.' ); + $this->assertSame( 8, has_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks' ), 'content_filtered_save_pre filter should be added at priority 8 for users without edit_css.' ); + + wp_set_current_user( 0 ); + wp_custom_css_remove_filters(); + } + + /** + * Tests that the content_save_pre filter is not added for a user with edit_css. + * + * @ticket 64771 + * + * @covers ::wp_custom_css_kses_init + * @covers ::wp_custom_css_remove_filters + */ + public function test_filter_not_added_for_user_with_edit_css() { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + if ( is_multisite() ) { + grant_super_admin( $admin_id ); + } + wp_set_current_user( $admin_id ); + wp_custom_css_kses_init(); + + $this->assertFalse( has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'content_save_pre filter should not be added for users with edit_css.' ); + $this->assertFalse( has_filter( 'content_filtered_save_pre', 'wp_strip_custom_css_from_blocks' ), 'content_filtered_save_pre filter should not be added for users with edit_css.' ); + + if ( is_multisite() ) { + revoke_super_admin( $admin_id ); + } + wp_set_current_user( 0 ); + wp_custom_css_remove_filters(); + } + + /** + * Tests that switching to a user with edit_css removes the filter via the set_current_user action. + * + * wp_custom_css_kses_init() is hooked to set_current_user, so wp_set_current_user() + * alone should update the filter state without a manual call. + * + * @ticket 64771 + * + * @covers ::wp_custom_css_kses_init + */ + public function test_set_current_user_action_triggers_reinit() { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $author_id = self::factory()->user->create( array( 'role' => 'author' ) ); + if ( is_multisite() ) { + grant_super_admin( $admin_id ); + } + + // Switching to a user without edit_css should add the filter via the set_current_user action. + wp_set_current_user( $author_id ); + $this->assertNotFalse( has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'Filter should be active for user without edit_css.' ); + + // Switching to a user with edit_css should remove the filter via the set_current_user action. + wp_set_current_user( $admin_id ); + $this->assertFalse( has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'Filter should be removed after switching to a user with edit_css.' ); + + if ( is_multisite() ) { + revoke_super_admin( $admin_id ); + } + wp_set_current_user( 0 ); + wp_custom_css_remove_filters(); + } + + /** + * Tests that the filter is enabled during import regardless of user capability. + * + * @ticket 64771 + * + * @covers ::wp_custom_css_force_filtered_html_on_import_filter + */ + public function test_force_filtered_html_on_import_enables_filter_for_privileged_user() { + $admin_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + if ( is_multisite() ) { + grant_super_admin( $admin_id ); + } + wp_set_current_user( $admin_id ); + wp_custom_css_kses_init(); + + $this->assertFalse( has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'Filter should not be active for admin before import.' ); + + apply_filters( 'force_filtered_html_on_import', true ); + + $this->assertNotFalse( has_filter( 'content_save_pre', 'wp_strip_custom_css_from_blocks' ), 'Filter should be enabled during import regardless of user capability.' ); + + if ( is_multisite() ) { + revoke_super_admin( $admin_id ); + } + wp_set_current_user( 0 ); + wp_custom_css_remove_filters(); + } +} diff --git a/tests/phpunit/tests/block-template-utils.php b/tests/phpunit/tests/block-template-utils.php index a6242de61421d..d89aa35f06e72 100644 --- a/tests/phpunit/tests/block-template-utils.php +++ b/tests/phpunit/tests/block-template-utils.php @@ -1,53 +1,402 @@ post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + 'this-theme-should-not-resolve', + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, 'this-theme-should-not-resolve', 'wp_theme' ); - public static function wpSetUpBeforeClass() { // Set up template post. - $args = array( - 'post_type' => 'wp_template', - 'post_name' => 'my_template', - 'post_title' => 'My Template', - 'post_content' => 'Content', - 'post_excerpt' => 'Description of my template', - 'tax_input' => array( - 'wp_theme' => array( - get_stylesheet(), + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), ), - ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) ); - self::$post = self::factory()->post->create_and_get( $args ); - wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' ); + + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); } public static function wpTearDownAfterClass() { - wp_delete_post( self::$post->ID ); + wp_delete_post( self::$template_post->ID ); + } + + public function set_up() { + parent::set_up(); + switch_theme( self::TEST_THEME ); } - public function test_build_template_result_from_post() { - $template = _build_template_result_from_post( - self::$post, - 'wp_template' + /** + * Tear down after each test. + * + * @since 6.5.0 + */ + public function tear_down() { + if ( WP_Block_Type_Registry::get_instance()->is_registered( 'tests/hooked-block' ) ) { + unregister_block_type( 'tests/hooked-block' ); + } + + parent::tear_down(); + } + + /** + * @ticket 59338 + * + * @covers ::_inject_theme_attribute_in_template_part_block + */ + public function test_inject_theme_attribute_in_template_part_block() { + $template_part_block = array( + 'blockName' => 'core/template-part', + 'attrs' => array( + 'slug' => 'header', + 'align' => 'full', + 'tagName' => 'header', + 'className' => 'site-header', + ), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), ); - $this->assertNotWPError( $template ); - $this->assertSame( get_stylesheet() . '//my_template', $template->id ); + _inject_theme_attribute_in_template_part_block( $template_part_block ); + $expected = array( + 'blockName' => 'core/template-part', + 'attrs' => array( + 'slug' => 'header', + 'align' => 'full', + 'tagName' => 'header', + 'className' => 'site-header', + 'theme' => get_stylesheet(), + ), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), + ); + $this->assertSame( + $expected, + $template_part_block, + '`theme` attribute was not correctly injected in template part block.' + ); + } + + /** + * @ticket 59338 + * + * @covers ::_inject_theme_attribute_in_template_part_block + */ + public function test_not_inject_theme_attribute_in_template_part_block_theme_attribute_exists() { + $template_part_block = array( + 'blockName' => 'core/template-part', + 'attrs' => array( + 'slug' => 'header', + 'align' => 'full', + 'tagName' => 'header', + 'className' => 'site-header', + 'theme' => 'fake-theme', + ), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), + ); + + $expected = $template_part_block; + _inject_theme_attribute_in_template_part_block( $template_part_block ); + $this->assertSame( + $expected, + $template_part_block, + 'Existing `theme` attribute in template part block was not respected by attribute injection.' + ); + } + + /** + * @ticket 59338 + * + * @covers ::_inject_theme_attribute_in_template_part_block + */ + public function test_not_inject_theme_attribute_non_template_part_block() { + $non_template_part_block = array( + 'blockName' => 'core/post-content', + 'attrs' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + 'innerBlocks' => array(), + ); + + $expected = $non_template_part_block; + _inject_theme_attribute_in_template_part_block( $non_template_part_block ); + $this->assertSame( + $expected, + $non_template_part_block, + '`theme` attribute injection modified non-template-part block.' + ); + } + + /** + * @ticket 59452 + * + * @covers ::_inject_theme_attribute_in_block_template_content + * + * @expectedDeprecated _inject_theme_attribute_in_block_template_content + */ + public function test_inject_theme_attribute_in_block_template_content() { + $theme = get_stylesheet(); + $content_without_theme_attribute = ''; + $template_content = _inject_theme_attribute_in_block_template_content( + $content_without_theme_attribute, + $theme + ); + $expected = sprintf( + '', + get_stylesheet() + ); + $this->assertSame( $expected, $template_content ); + + $content_without_theme_attribute_nested = ''; + $template_content = _inject_theme_attribute_in_block_template_content( + $content_without_theme_attribute_nested, + $theme + ); + $expected = sprintf( + '', + get_stylesheet() + ); + $this->assertSame( $expected, $template_content ); + + // Does not inject theme when there is an existing theme attribute. + $content_with_existing_theme_attribute = ''; + $template_content = _inject_theme_attribute_in_block_template_content( + $content_with_existing_theme_attribute, + $theme + ); + $this->assertSame( $content_with_existing_theme_attribute, $template_content ); + + // Does not inject theme when there is no template part. + $content_with_no_template_part = ''; + $template_content = _inject_theme_attribute_in_block_template_content( + $content_with_no_template_part, + $theme + ); + $this->assertSame( $content_with_no_template_part, $template_content ); + } + + /** + * @ticket 54448 + * @ticket 59460 + * + * @dataProvider data_remove_theme_attribute_in_block_template_content + * + * @expectedDeprecated _remove_theme_attribute_in_block_template_content + */ + public function test_remove_theme_attribute_in_block_template_content( $template_content, $expected ) { + $this->assertSame( $expected, _remove_theme_attribute_in_block_template_content( $template_content ) ); + } + + /** + * @ticket 59460 + * + * @covers ::_remove_theme_attribute_from_template_part_block + * @covers ::traverse_and_serialize_blocks + * + * @dataProvider data_remove_theme_attribute_in_block_template_content + * + * @param string $template_content The template markup. + * @param string $expected The expected markup after removing the theme attribute from Template Part blocks. + */ + public function test_remove_theme_attribute_from_template_part_block( $template_content, $expected ) { + $template_content_parsed_blocks = parse_blocks( $template_content ); + + $this->assertSame( + $expected, + traverse_and_serialize_blocks( + $template_content_parsed_blocks, + '_remove_theme_attribute_from_template_part_block' + ) + ); + } + + public function data_remove_theme_attribute_in_block_template_content() { + return array( + array( + '', + '', + ), + array( + '', + '', + ), + // Does not modify content when there is no existing theme attribute. + array( + '', + '', + ), + // Does not remove theme when there is no template part. + array( + '', + '', + ), + ); + } + + /** + * Tests that a skip link is added and a MAIN element without an ID receives the default ID. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_inserts_link_and_adds_main_id_when_missing() { + $template_html = '
      Content
      '; + $expected = + '
      ' . + '
      Content
      '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that an existing MAIN ID is reused for the skip link. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_uses_existing_main_id() { + $template_html = '
      Content
      '; + $expected = + '' . + '
      Content
      '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that a boolean MAIN ID is treated as missing and replaced with the default. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_handles_boolean_main_id() { + $template_html = '
      Content
      '; + $expected = + '' . + '
      Content
      '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that a MAIN ID containing whitespace is preserved and used for the skip link. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_preserves_whitespace_main_id() { + $template_html = '
      Content
      '; + $expected = + '' . + '
      Content
      '; + + $this->assertEqualHTML( $expected, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Tests that no changes are made when there is no MAIN element. + * + * @ticket 64361 + * + * @covers ::_block_template_add_skip_link + */ + public function test_block_template_add_skip_link_does_not_modify_when_main_missing() { + $template_html = '
      Content
      '; + + $this->assertSame( $template_html, _block_template_add_skip_link( $template_html ) ); + } + + /** + * Should retrieve the template from the theme files. + */ + public function test_get_block_template_from_file() { + $id = get_stylesheet() . '//' . 'index'; + $template = get_block_template( $id, 'wp_template' ); + $this->assertSame( $id, $template->id ); $this->assertSame( get_stylesheet(), $template->theme ); - $this->assertSame( 'my_template', $template->slug ); + $this->assertSame( 'index', $template->slug ); $this->assertSame( 'publish', $template->status ); - $this->assertSame( 'custom', $template->source ); - $this->assertSame( 'My Template', $template->title ); - $this->assertSame( 'Description of my template', $template->description ); + $this->assertSame( 'theme', $template->source ); $this->assertSame( 'wp_template', $template->type ); + + // Test template parts. + $id = get_stylesheet() . '//' . 'small-header'; + $template = get_block_template( $id, 'wp_template_part' ); + $this->assertSame( $id, $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'small-header', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'theme', $template->source ); + $this->assertSame( 'wp_template_part', $template->type ); + $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template->area ); } /** @@ -62,36 +411,73 @@ public function test_get_block_template_from_post() { $this->assertSame( 'publish', $template->status ); $this->assertSame( 'custom', $template->source ); $this->assertSame( 'wp_template', $template->type ); + + // Test template parts. + $id = get_stylesheet() . '//' . 'my_template_part'; + $template = get_block_template( $id, 'wp_template_part' ); + $this->assertSame( $id, $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template_part', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'wp_template_part', $template->type ); + $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template->area ); } /** - * Should retrieve block templates. + * Should flatten nested blocks */ - public function test_get_block_templates() { - function get_template_ids( $templates ) { - return array_map( - static function( $template ) { - return $template->id; - }, - $templates - ); - } + public function test_flatten_blocks() { + $content_template_part_inside_group = ''; + $blocks = parse_blocks( $content_template_part_inside_group ); + $actual = _flatten_blocks( $blocks ); + $expected = array( $blocks[0], $blocks[0]['innerBlocks'][0] ); + $this->assertSame( $expected, $actual ); - // All results. - $templates = get_block_templates( array(), 'wp_template' ); - $template_ids = get_template_ids( $templates ); + $content_template_part_inside_group_inside_group = ''; + $blocks = parse_blocks( $content_template_part_inside_group_inside_group ); + $actual = _flatten_blocks( $blocks ); + $expected = array( $blocks[0], $blocks[0]['innerBlocks'][0], $blocks[0]['innerBlocks'][0]['innerBlocks'][0] ); + $this->assertSame( $expected, $actual ); - // Avoid testing the entire array because the theme might add/remove templates. - $this->assertContains( get_stylesheet() . '//' . 'my_template', $template_ids ); + $content_without_inner_blocks = ''; + $blocks = parse_blocks( $content_without_inner_blocks ); + $actual = _flatten_blocks( $blocks ); + $expected = array( $blocks[0] ); + $this->assertSame( $expected, $actual ); + } - // Filter by slug. - $templates = get_block_templates( array( 'slug__in' => array( 'my_template' ) ), 'wp_template' ); - $template_ids = get_template_ids( $templates ); - $this->assertSame( array( get_stylesheet() . '//' . 'my_template' ), $template_ids ); + /** + * Should generate block templates export file. + * + * @ticket 54448 + * @requires extension zip + */ + public function test_wp_generate_block_templates_export_file() { + $filename = wp_generate_block_templates_export_file(); + $this->assertFileExists( $filename, 'zip file is created at the specified path' ); + $this->assertGreaterThan( 0, filesize( $filename ), 'zip file is larger than 0 bytes' ); + + // Open ZIP file and make sure the directories exist. + $zip = new ZipArchive(); + $zip->open( $filename ); + $has_theme_json = $zip->locateName( 'theme.json' ) !== false; + $has_block_templates_dir = $zip->locateName( 'templates/' ) !== false; + $has_block_template_parts_dir = $zip->locateName( 'parts/' ) !== false; + $this->assertTrue( $has_theme_json, 'theme.json exists' ); + $this->assertTrue( $has_block_templates_dir, 'theme/templates directory exists' ); + $this->assertTrue( $has_block_template_parts_dir, 'theme/parts directory exists' ); - // Filter by CPT ID. - $templates = get_block_templates( array( 'wp_id' => self::$post->ID ), 'wp_template' ); - $template_ids = get_template_ids( $templates ); - $this->assertSame( array( get_stylesheet() . '//' . 'my_template' ), $template_ids ); + // ZIP file contains at least one HTML file. + $has_html_files = false; + $num_files = $zip->numFiles; + for ( $i = 0; $i < $num_files; $i++ ) { + $filename = $zip->getNameIndex( $i ); + if ( '.html' === substr( $filename, -5 ) ) { + $has_html_files = true; + break; + } + } + $this->assertTrue( $has_html_files, 'contains at least one html file' ); } } diff --git a/tests/phpunit/tests/block-template.php b/tests/phpunit/tests/block-template.php index 5b475567bde0a..e0307b5782165 100644 --- a/tests/phpunit/tests/block-template.php +++ b/tests/phpunit/tests/block-template.php @@ -1,43 +1,126 @@ 'wp_template', - 'post_name' => 'wp-custom-template-my-block-template', - 'post_title' => 'My Custom Block Template', - 'post_content' => 'Content', - 'post_excerpt' => 'Description of my block template', - 'tax_input' => array( - 'wp_theme' => array( - get_stylesheet(), - ), - ), + public function set_up() { + parent::set_up(); + switch_theme( 'block-theme' ); + do_action( 'setup_theme' ); + do_action( 'after_setup_theme' ); + } + + public function tear_down() { + global $_wp_current_template_id, $_wp_current_template_content; + unset( $_wp_current_template_id, $_wp_current_template_content ); + + parent::tear_down(); + } + + public function test_page_home_block_template_takes_precedence_over_less_specific_block_templates() { + global $_wp_current_template_content; + $type = 'page'; + $templates = array( + 'page-home.php', + 'page-1.php', + 'page.php', ); - self::$post = self::factory()->post->create_and_get( $args ); - wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' ); + $resolved_template_path = locate_block_template( get_stylesheet_directory() . '/page-home.php', $type, $templates ); + $this->assertSame( self::$template_canvas_path, $resolved_template_path ); + $this->assertStringEqualsFile( get_stylesheet_directory() . '/templates/page-home.html', $_wp_current_template_content ); } - public static function wpTearDownAfterClass() { - wp_delete_post( self::$post->ID ); + public function test_page_block_template_takes_precedence() { + global $_wp_current_template_content; + $type = 'page'; + $templates = array( + 'page-slug-doesnt-exist.php', + 'page-1.php', + 'page.php', + ); + $resolved_template_path = locate_block_template( get_stylesheet_directory() . '/page.php', $type, $templates ); + $this->assertSame( self::$template_canvas_path, $resolved_template_path ); + $this->assertStringEqualsFile( get_stylesheet_directory() . '/templates/page.html', $_wp_current_template_content ); } - public function tear_down() { + public function test_block_template_takes_precedence_over_equally_specific_php_template() { global $_wp_current_template_content; - unset( $_wp_current_template_content ); + $type = 'index'; + $templates = array( + 'index.php', + ); + $resolved_template_path = locate_block_template( get_stylesheet_directory() . '/index.php', $type, $templates ); + $this->assertSame( self::$template_canvas_path, $resolved_template_path ); + $this->assertStringEqualsFile( get_stylesheet_directory() . '/templates/index.html', $_wp_current_template_content ); + } + + /** + * In a hybrid theme, a PHP template of higher specificity will take precedence over a block template + * with lower specificity. + * + * Covers https://github.com/WordPress/gutenberg/pull/29026. + */ + public function test_more_specific_php_template_takes_precedence_over_less_specific_block_template() { + $page_id_template = 'page-1.php'; + $page_id_template_path = get_stylesheet_directory() . '/' . $page_id_template; + $type = 'page'; + $templates = array( + 'page-slug-doesnt-exist.php', + 'page-1.php', + 'page.php', + ); + $resolved_template_path = locate_block_template( $page_id_template_path, $type, $templates ); + $this->assertSame( $page_id_template_path, $resolved_template_path ); + } + + /** + * If a theme is a child of a block-based parent theme but has php templates for some of its pages, + * a php template of the child will take precedence over the parent's block template if they have + * otherwise equal specificity. + * + * Covers https://github.com/WordPress/gutenberg/pull/31123. + * Covers https://core.trac.wordpress.org/ticket/54515. + * + */ + public function test_child_theme_php_template_takes_precedence_over_equally_specific_parent_theme_block_template() { + switch_theme( 'block-theme-child' ); + + $page_slug_template = 'page-home.php'; + $page_slug_template_path = get_stylesheet_directory() . '/' . $page_slug_template; + $type = 'page'; + $templates = array( + 'page-home.php', + 'page-1.php', + 'page.php', + ); + $resolved_template_path = locate_block_template( $page_slug_template_path, $type, $templates ); + $this->assertSame( $page_slug_template_path, $resolved_template_path ); + } + + public function test_child_theme_block_template_takes_precedence_over_equally_specific_parent_theme_php_template() { + global $_wp_current_template_content; + + switch_theme( 'block-theme-child' ); + + $page_template = 'page-1.php'; + $parent_theme_page_template_path = get_template_directory() . '/' . $page_template; + $type = 'page'; + $templates = array( + 'page-slug-doesnt-exist.php', + 'page-1.php', + 'page.php', + ); + $resolved_template_path = locate_block_template( $parent_theme_page_template_path, $type, $templates ); + $this->assertSame( self::$template_canvas_path, $resolved_template_path ); + $this->assertStringEqualsFile( get_stylesheet_directory() . '/templates/page-1.html', $_wp_current_template_content ); } /** @@ -63,6 +146,22 @@ public function test_custom_page_php_template_takes_precedence_over_all_other_te public function test_custom_page_block_template_takes_precedence_over_all_other_templates() { global $_wp_current_template_content; + // Set up custom template post. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'wp-custom-template-my-block-template', + 'post_title' => 'My Custom Block Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my block template', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + ), + ); + $post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( $post->ID, get_stylesheet(), 'wp_theme' ); + $custom_page_block_template = 'wp-custom-template-my-block-template'; $page_template_path = get_stylesheet_directory() . '/' . 'page.php'; $type = 'page'; @@ -74,7 +173,9 @@ public function test_custom_page_block_template_takes_precedence_over_all_other_ ); $resolved_template_path = locate_block_template( $page_template_path, $type, $templates ); $this->assertSame( self::$template_canvas_path, $resolved_template_path ); - $this->assertSame( self::$post->post_content, $_wp_current_template_content ); + $this->assertSame( $post->post_content, $_wp_current_template_content ); + + wp_delete_post( $post->ID ); } /** @@ -84,4 +185,420 @@ public function test_template_remains_unchanged_if_templates_array_is_empty() { $resolved_template_path = locate_block_template( '', 'search', array() ); $this->assertSame( '', $resolved_template_path ); } + + /** + * Tests that `get_the_block_template_html()` wraps block parsing into the query loop when on a singular template. + * + * This is necessary since block themes do not include the necessary blocks to trigger the main query loop, and + * since there is only a single post in the main query loop in such cases anyway. + * + * @ticket 58154 + * @ticket 59736 + * @covers ::get_the_block_template_html + */ + public function test_get_the_block_template_html_enforces_singular_query_loop() { + global $_wp_current_template_id, $_wp_current_template_content, $wp_query, $wp_the_query; + + // Register test block to log `in_the_loop()` results. + $in_the_loop_logs = array(); + $this->register_in_the_loop_logger_block( $in_the_loop_logs ); + + // Set main query to single post. + $post_id = self::factory()->post->create( array( 'post_title' => 'A single post' ) ); + $wp_query = new WP_Query( array( 'p' => $post_id ) ); + $wp_the_query = $wp_query; + + // Force a template ID that is for the current stylesheet. + $_wp_current_template_id = get_stylesheet() . '//single'; + // Use block template that just renders post title and the above test block. + $_wp_current_template_content = ''; + + $expected = '
      '; + $expected .= '

      A single post

      '; + $expected .= '
      '; + + $output = get_the_block_template_html(); + $this->unregister_in_the_loop_logger_block(); + $this->assertSame( $expected, $output, 'Unexpected block template output' ); + $this->assertSame( array( true ), $in_the_loop_logs, 'Main query loop was not triggered' ); + } + + /** + * Tests that `get_the_block_template_html()` does not start the main query loop generally. + * + * @ticket 58154 + * @covers ::get_the_block_template_html + */ + public function test_get_the_block_template_html_does_not_generally_enforce_loop() { + global $_wp_current_template_id, $_wp_current_template_content, $wp_query, $wp_the_query; + + // Register test block to log `in_the_loop()` results. + $in_the_loop_logs = array(); + $this->register_in_the_loop_logger_block( $in_the_loop_logs ); + + // Set main query to a general post query (i.e. not for a specific post). + $post_id = self::factory()->post->create( + array( + 'post_title' => 'A single post', + 'post_content' => 'The content.', + ) + ); + $wp_query = new WP_Query( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + $wp_the_query = $wp_query; + + // Force a template ID that is for the current stylesheet. + $_wp_current_template_id = get_stylesheet() . '//home'; + + /* + * Use block template that renders the above test block, followed by a main query loop. + * `get_the_block_template_html()` should not start the loop, but the `core/query` and `core/post-template` + * blocks should. + */ + $_wp_current_template_content = ''; + $_wp_current_template_content .= ''; + $_wp_current_template_content .= ''; + $_wp_current_template_content .= ''; + $_wp_current_template_content .= ''; + $_wp_current_template_content .= ''; + $_wp_current_template_content .= ''; + + $expected = '
      '; + $expected .= '
        '; + $expected .= '
      • '; + $expected .= '

        A single post

        '; + $expected .= '
        ' . wpautop( 'The content.' ) . '
        '; + $expected .= '
      • '; + $expected .= '
      '; + $expected .= '
      '; + + $output = get_the_block_template_html(); + $this->unregister_in_the_loop_logger_block(); + $this->assertSame( $expected, $output, 'Unexpected block template output' ); + $this->assertSame( array( false, true ), $in_the_loop_logs, 'Main query loop was triggered incorrectly' ); + } + + /** + * Tests that `get_the_block_template_html()` does not start the main query loop when on a template that is not from the current theme. + * + * @ticket 58154 + * @ticket 59736 + * @covers ::get_the_block_template_html + */ + public function test_get_the_block_template_html_skips_singular_query_loop_when_non_theme_template() { + global $_wp_current_template_id, $_wp_current_template_content, $wp_query, $wp_the_query; + + // Register test block to log `in_the_loop()` results. + $in_the_loop_logs = array(); + $this->register_in_the_loop_logger_block( $in_the_loop_logs ); + + // Set main query to single post. + $post_id = self::factory()->post->create( array( 'post_title' => 'A single post' ) ); + $wp_query = new WP_Query( array( 'p' => $post_id ) ); + $wp_the_query = $wp_query; + + // Force a template ID that is not for the current stylesheet. + $_wp_current_template_id = 'some-plugin-slug//single'; + // Use block template that just renders post title and the above test block. + $_wp_current_template_content = ''; + + $output = get_the_block_template_html(); + $this->unregister_in_the_loop_logger_block(); + $this->assertSame( array( false ), $in_the_loop_logs, 'Main query loop was triggered despite a custom block template outside the current theme being used' ); + } + + /** + * Tests that `get_the_block_template_html()` adds a skip link when a MAIN element is present. + * + * @ticket 64361 + * @covers ::get_the_block_template_html + */ + public function test_get_the_block_template_html_adds_skip_link_when_main_present() { + global $_wp_current_template_id, $_wp_current_template_content; + + $_wp_current_template_id = get_stylesheet() . '//index'; + $_wp_current_template_content = '
      Content
      '; + + $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() ); + $this->assertTrue( + $processor->next_tag( + array( + 'tag_name' => 'A', + 'class_name' => 'skip-link', + ) + ), + 'Expected skip link was not added to the block template HTML.' + ); + $this->assertSame( 'wp-skip-link', $processor->get_attribute( 'id' ), 'Unexpected ID on skip link.' ); + $this->assertTrue( $processor->has_class( 'screen-reader-text' ), 'Expected "screen-reader-text" class on skip link.' ); + } + + /** + * Tests that `get_the_block_template_html()` does not add a skip link when the skip-link action is unhooked. + * + * @ticket 64361 + * @covers ::get_the_block_template_html + * + * @dataProvider data_provider_skip_link_actions + */ + public function test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked( string $action, string $callback ) { + global $_wp_current_template_id, $_wp_current_template_content; + + $_wp_current_template_id = get_stylesheet() . '//index'; + $_wp_current_template_content = '
      Content
      '; + + remove_action( $action, $callback ); + + $processor = new WP_HTML_Tag_Processor( get_the_block_template_html() ); + $this->assertFalse( + $processor->next_tag( + array( + 'tag_name' => 'A', + 'class_name' => 'skip-link', + ) + ), + 'Unexpected skip link was added to the block template HTML when the action was unhooked.' + ); + } + + /** + * Data provider for test_get_the_block_template_html_does_not_add_skip_link_when_action_unhooked. + * + * @return array> + */ + public function data_provider_skip_link_actions(): array { + return array( + 'the_block_template_skip_link' => array( + 'action' => 'wp_footer', + 'callback' => 'the_block_template_skip_link', + ), + 'wp_enqueue_block_template_skip_link' => array( + 'action' => 'wp_enqueue_scripts', + 'callback' => 'wp_enqueue_block_template_skip_link', + ), + ); + } + + /** + * @ticket 58319 + * + * @covers ::get_block_theme_folders + * + * @dataProvider data_get_block_theme_folders + * + * @param string $theme The theme's stylesheet. + * @param string[] $expected The expected associative array of block theme folders. + */ + public function test_get_block_theme_folders( $theme, $expected ) { + $wp_theme = wp_get_theme( $theme ); + $wp_theme->cache_delete(); // Clear cache. + + $this->assertSame( $expected, get_block_theme_folders( $theme ), 'Incorrect block theme folders were retrieved.' ); + $reflection = new ReflectionMethod( $wp_theme, 'cache_get' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + $theme_cache = $reflection->invoke( $wp_theme, 'theme' ); + $cached_value = $theme_cache['block_template_folders']; + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( false ); + } + + $this->assertSame( $expected, $cached_value, 'The cached value is incorrect.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_get_block_theme_folders() { + return array( + 'block-theme' => array( + 'block-theme', + array( + 'wp_template' => 'templates', + 'wp_template_part' => 'parts', + ), + ), + 'block-theme-deprecated-path' => array( + 'block-theme-deprecated-path', + array( + 'wp_template' => 'block-templates', + 'wp_template_part' => 'block-template-parts', + ), + ), + 'block-theme-child' => array( + 'block-theme-child', + array( + 'wp_template' => 'templates', + 'wp_template_part' => 'parts', + ), + ), + 'block-theme-child-deprecated-path' => array( + 'block-theme-child-deprecated-path', + array( + 'wp_template' => 'block-templates', + 'wp_template_part' => 'block-template-parts', + ), + ), + 'this-is-an-invalid-theme' => array( + 'this-is-an-invalid-theme', + array( + 'wp_template' => 'templates', + 'wp_template_part' => 'parts', + ), + ), + 'null' => array( + null, + array( + 'wp_template' => 'templates', + 'wp_template_part' => 'parts', + ), + ), + ); + } + + /** + * Tests `_get_block_templates_paths()` for an invalid directory. + * + * @ticket 58196 + * + * @covers ::_get_block_templates_paths + */ + public function test_get_block_templates_paths_dir_exists() { + $theme_dir = $this->normalizeDirectorySeparatorsInPath( get_template_directory() ); + // Templates in the current theme. + $templates = array( + 'parts/small-header.html', + 'templates/custom-hero-template.html', + 'templates/custom-single-post-template.html', + 'templates/index.html', + 'templates/page-home.html', + 'templates/page.html', + 'templates/single.html', + ); + + $expected_template_paths = array_map( + static function ( $template ) use ( $theme_dir ) { + return $theme_dir . '/' . $template; + }, + $templates + ); + + $template_paths = _get_block_templates_paths( $theme_dir ); + $template_paths = array_map( array( $this, 'normalizeDirectorySeparatorsInPath' ), _get_block_templates_paths( $theme_dir ) ); + + $this->assertSameSets( $expected_template_paths, $template_paths ); + } + + /** + * Test _get_block_templates_paths() for a invalid dir. + * + * @ticket 58196 + * + * @covers ::_get_block_templates_paths + */ + public function test_get_block_templates_paths_dir_doesnt_exists() { + // Should return empty array for invalid path. + $template_paths = _get_block_templates_paths( '/tmp/random-invalid-theme-path' ); + $this->assertSame( array(), $template_paths ); + } + + /** + * Tests that get_block_templates() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers ::get_block_templates + */ + public function test_get_block_templates_from_registry() { + $template_name = 'test-plugin//test-template'; + + register_block_template( $template_name ); + + $templates = get_block_templates(); + + $this->assertArrayHasKey( $template_name, $templates ); + + unregister_block_template( $template_name ); + } + + /** + * Tests that get_block_template() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers ::get_block_template + */ + public function test_get_block_template_from_registry() { + $template_name = 'test-plugin//test-template'; + $args = array( + 'title' => 'Test Template', + ); + + register_block_template( $template_name, $args ); + + $template = get_block_template( 'block-theme//test-template' ); + + $this->assertSame( 'Test Template', $template->title ); + + unregister_block_template( $template_name ); + } + + /** + * Tests that unregister_block_template() returns a WP_Error when trying to unregister + * a non-registered template, and that the error message includes the template name. + * + * @ticket 64072 + * + * @covers ::unregister_block_template + */ + public function test_unregister_block_template_error_message_includes_template_name() { + $template_name = 'test-plugin//unregistered-template'; + + // Ensure template is not registered. + unregister_block_template( $template_name ); + + // Expect _doing_it_wrong() notice. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::unregister' ); + + // Try to unregister again, should return WP_Error. + $error = unregister_block_template( $template_name ); + + $this->assertInstanceOf( 'WP_Error', $error ); + $this->assertStringContainsString( + $template_name, + $error->get_error_message(), + 'Error message should include the template name.' + ); + } + + /** + * Registers a test block to log `in_the_loop()` results. + * + * @param array $in_the_loop_logs Array to log function results in. Passed by reference. + */ + private function register_in_the_loop_logger_block( array &$in_the_loop_logs ) { + register_block_type( + 'test/in-the-loop-logger', + array( + 'render_callback' => function () use ( &$in_the_loop_logs ) { + $in_the_loop_logs[] = in_the_loop(); + return ''; + }, + ) + ); + } + + /** + * Unregisters the test block registered by the `register_in_the_loop_logger_block()` method. + */ + private function unregister_in_the_loop_logger_block() { + unregister_block_type( 'test/in-the-loop-logger' ); + } } diff --git a/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php b/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php new file mode 100644 index 0000000000000..e4675ad210751 --- /dev/null +++ b/tests/phpunit/tests/block-templates/WpBlockTemplatesRegistry.php @@ -0,0 +1,326 @@ +register( $template_name ); + + $this->assertSame( $template->slug, 'test-template' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that register() returns an error if template name is not a string. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_invalid_name() { + // Try to register a template with invalid name (non-string). + $template_name = array( 'invalid-template-name' ); + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_name_no_string', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must be strings.', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template name contains + * uppercase characters. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_invalid_name_uppercase() { + // Try to register a template with uppercase characters in the name. + $template_name = 'test-plugin//Invalid-Template-Name'; + + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_name_no_uppercase', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must not contain uppercase characters.', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template name has no prefix. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_no_prefix() { + // Try to register a template without a namespace. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( 'template-no-plugin', array() ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_no_prefix', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertSame( 'Template names must contain a namespace prefix. Example: my-plugin//my-custom-template', $result->get_error_message(), 'Error message mismatch.' ); + } + + /** + * Tests that register() returns an error if template already exists. + * + * @ticket 61804 + * + * @covers ::register + */ + public function test_register_template_already_exists() { + // Register the template for the first time. + $template_name = 'test-plugin//duplicate-template'; + self::$registry->register( $template_name ); + + // Try to register the same template again. + $this->setExpectedIncorrectUsage( 'WP_Block_Templates_Registry::register' ); + $result = self::$registry->register( $template_name ); + + $this->assertWPError( $result, 'Template registration is expected to trigger an error.' ); + $this->assertSame( 'template_already_registered', $result->get_error_code(), 'Error code mismatch.' ); + $this->assertStringContainsString( 'Template "test-plugin//duplicate-template" is already registered.', $result->get_error_message(), 'Error message mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_all_registered() returns all registered templates. + * + * @ticket 61804 + * + * @covers ::get_all_registered + */ + public function test_get_all_registered() { + $template_name_1 = 'test-plugin//template-1'; + $template_name_2 = 'test-plugin//template-2'; + self::$registry->register( $template_name_1 ); + self::$registry->register( $template_name_2 ); + + $all_templates = self::$registry->get_all_registered(); + + $this->assertIsArray( $all_templates, 'Registered templates should be an array.' ); + $this->assertCount( 2, $all_templates, 'Registered templates should contain 2 items.' ); + $this->assertArrayHasKey( 'test-plugin//template-1', $all_templates, 'Registered templates should contain "test-plugin//template-1".' ); + $this->assertArrayHasKey( 'test-plugin//template-2', $all_templates, 'Registered templates should contain "test-plugin//template-2".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + /** + * Tests that get_registered() returns the correct registered template. + * + * @ticket 61804 + * + * @covers ::get_registered + */ + public function test_get_registered() { + $template_name = 'test-plugin//registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Registered Template', + 'description' => 'Description of registered template', + 'post_types' => array( 'post', 'page' ), + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_registered( $template_name ); + + $this->assertSame( 'default', $registered_template->theme, 'Template theme mismatch.' ); + $this->assertSame( 'registered-template', $registered_template->slug, 'Template slug mismatch.' ); + $this->assertSame( 'default//registered-template', $registered_template->id, 'Template ID mismatch.' ); + $this->assertSame( 'Registered Template', $registered_template->title, 'Template title mismatch.' ); + $this->assertSame( 'Template content', $registered_template->content, 'Template content mismatch.' ); + $this->assertSame( 'Description of registered template', $registered_template->description, 'Template description mismatch.' ); + $this->assertSame( 'plugin', $registered_template->source, "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $registered_template->origin, "Template origin should be 'plugin'." ); + $this->assertSameSets( array( 'post', 'page' ), $registered_template->post_types, 'Template post types mismatch.' ); + $this->assertSame( 'test-plugin', $registered_template->plugin, 'Plugin name mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_by_slug() returns the correct template by slug. + * + * @ticket 61804 + * + * @covers ::get_by_slug + */ + public function test_get_by_slug() { + $slug = 'slug-template'; + $template_name = 'test-plugin//' . $slug; + $args = array( + 'content' => 'Template content', + 'title' => 'Slug Template', + ); + self::$registry->register( $template_name, $args ); + + $registered_template = self::$registry->get_by_slug( $slug ); + + $this->assertNotNull( $registered_template, 'Registered template should not be null.' ); + $this->assertSame( $slug, $registered_template->slug, 'Template slug mismatch.' ); + + self::$registry->unregister( $template_name ); + } + + /** + * Tests that get_by_query() returns the correct templates based on the query. + * + * @ticket 61804 + * + * @covers ::get_by_query + */ + public function test_get_by_query() { + $template_name_1 = 'test-plugin//query-template-1'; + $template_name_2 = 'test-plugin//query-template-2'; + $args_1 = array( + 'content' => 'Template content 1', + 'title' => 'Query Template 1', + ); + $args_2 = array( + 'content' => 'Template content 2', + 'title' => 'Query Template 2', + ); + self::$registry->register( $template_name_1, $args_1 ); + self::$registry->register( $template_name_2, $args_2 ); + + $query = array( + 'slug__in' => array( 'query-template-1' ), + ); + $results = self::$registry->get_by_query( $query ); + + $this->assertCount( 1, $results, 'Query result should contain 1 item.' ); + $this->assertArrayHasKey( $template_name_1, $results, 'Query result should contain "test-plugin//query-template-1".' ); + + self::$registry->unregister( $template_name_1 ); + self::$registry->unregister( $template_name_2 ); + } + + /** + * Tests that is_registered() correctly identifies registered templates. + * + * @ticket 61804 + * + * @covers ::is_registered + */ + public function test_is_registered() { + $template_name = 'test-plugin//is-registered-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Is Registered Template', + ); + self::$registry->register( $template_name, $args ); + + $this->assertTrue( self::$registry->is_registered( $template_name ) ); + + self::$registry->unregister( $template_name ); + } + + /** + * @ticket 63957 + * + * @covers ::is_registered + */ + public function test_is_registered_with_null_template_name() { + $this->assertFalse( self::$registry->is_registered( null ) ); + } + + /** + * Tests that unregister() correctly unregisters a registered template. + * + * @ticket 61804 + * + * @covers ::unregister + */ + public function test_unregister() { + $template_name = 'test-plugin//unregister-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Unregister Template', + ); + $template = self::$registry->register( $template_name, $args ); + + $unregistered_template = self::$registry->unregister( $template_name ); + + $this->assertEquals( $template, $unregistered_template, 'Unregistered template should be the same as the registered one.' ); + $this->assertFalse( self::$registry->is_registered( $template_name ), 'Template should not be registered after unregistering.' ); + } + + /** + * Data provider for test_template_name_validation. + * + * @return array[] Test data. + */ + public static function data_template_name_validation() { + return array( + 'valid_simple_name' => array( + 'my-plugin//my-template', + true, + 'Valid template name with simple characters should be accepted', + ), + 'valid_with_underscores' => array( + 'my-plugin//my_template', + true, + 'Template name with underscores should be accepted', + ), + 'valid_cpt_archive' => array( + 'my-plugin//archive-my_post_type', + true, + 'Template name for CPT archive with underscore should be accepted', + ), + ); + } + + /** + * Tests template name validation with various inputs. + * + * @ticket 62523 + * + * @dataProvider data_template_name_validation + * + * @param string $template_name The template name to test. + * @param bool $expected Expected validation result. + * @param string $message Test assertion message. + */ + public function test_template_name_validation( $template_name, $expected, $message ) { + $result = self::$registry->register( $template_name, array() ); + + if ( $expected ) { + self::$registry->unregister( $template_name ); + $this->assertNotWPError( $result, $message ); + } else { + $this->assertWPError( $result, $message ); + } + } +} diff --git a/tests/phpunit/tests/block-templates/base.php b/tests/phpunit/tests/block-templates/base.php new file mode 100644 index 0000000000000..133e5a7ba05ff --- /dev/null +++ b/tests/phpunit/tests/block-templates/base.php @@ -0,0 +1,85 @@ +post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + 'this-theme-should-not-resolve', + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, 'this-theme-should-not-resolve', 'wp_theme' ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => '

      Template

      ', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => '

      Template Part

      ', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + } + + public static function wpTearDownAfterClass() { + wp_delete_post( self::$template_post->ID ); + wp_delete_post( self::$template_part_post->ID ); + } + + public function set_up() { + parent::set_up(); + switch_theme( self::TEST_THEME ); + } +} diff --git a/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromFile.php b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromFile.php new file mode 100644 index 0000000000000..673e35af5b097 --- /dev/null +++ b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromFile.php @@ -0,0 +1,269 @@ +is_registered( 'tests/my-block' ) ) { + $registry->unregister( 'tests/my-block' ); + } + + parent::tear_down(); + } + + /** + * @ticket 54335 + */ + public function test_should_build_template() { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'single', + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template' + ); + + $this->assertSame( get_stylesheet() . '//single', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'single', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'theme', $template->source ); + $this->assertSame( 'Single Posts', $template->title ); + $this->assertSame( 'Displays a single post on your website unless a custom template has been applied to that post or a dedicated template exists.', $template->description ); + $this->assertSame( 'wp_template', $template->type ); + $this->assertEmpty( $template->modified ); + } + + /** + * @ticket 59325 + */ + public function test_should_build_template_using_custom_properties() { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'custom', + 'title' => 'Custom Title', + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template' + ); + + $this->assertSame( 'custom', $template->slug ); + $this->assertSame( 'Custom Title', $template->title ); + $this->assertTrue( $template->is_custom ); + } + + /** + * @ticket 59325 + */ + public function test_should_enforce_default_properties_when_building_template() { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'single', + 'title' => 'Custom title', + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template' + ); + + $this->assertSame( 'single', $template->slug ); + $this->assertSame( 'Single Posts', $template->title ); + $this->assertSame( 'Displays a single post on your website unless a custom template has been applied to that post or a dedicated template exists.', $template->description ); + $this->assertFalse( $template->is_custom ); + } + + /** + * @ticket 59325 + */ + public function test_should_respect_post_types_property_when_building_template() { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'single', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template' + ); + + $this->assertSameSets( array( 'post' ), $template->post_types ); + } + + /** + * @ticket 59325 + * + * @dataProvider data_build_template_injects_theme_attribute + * + * @param string $filename The template's filename. + * @param string $expected The expected block markup. + */ + public function test_should_build_template_and_inject_theme_attribute( $filename, $expected ) { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'single', + 'path' => DIR_TESTDATA . "/templates/$filename", + ), + 'wp_template' + ); + $this->assertSame( $expected, $template->content ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_build_template_injects_theme_attribute() { + return array( + 'a template with a template part block' => array( + 'filename' => 'template-with-template-part.html', + 'expected' => sprintf( + '', + self::TEST_THEME + ), + ), + 'a template with a template part block nested inside another block' => array( + 'filename' => 'template-with-nested-template-part.html', + 'expected' => sprintf( + ' + +', + self::TEST_THEME + ), + ), + 'a template with a template part block with an existing theme attribute' => array( + 'filename' => 'template-with-template-part-with-existing-theme-attribute.html', + 'expected' => '', + ), + 'a template with no template part block' => array( + 'filename' => 'template.html', + 'expected' => ' +

      Just a paragraph

      +', + ), + ); + } + + /** + * @ticket 54335 + */ + public function test_should_build_template_part() { + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'path' => DIR_TESTDATA . '/templates/template.html', + 'area' => WP_TEMPLATE_PART_AREA_HEADER, + ), + 'wp_template_part' + ); + $this->assertSame( get_stylesheet() . '//header', $template_part->id ); + $this->assertSame( get_stylesheet(), $template_part->theme ); + $this->assertSame( 'header', $template_part->slug ); + $this->assertSame( 'publish', $template_part->status ); + $this->assertSame( 'theme', $template_part->source ); + $this->assertSame( 'header', $template_part->title ); + $this->assertSame( '', $template_part->description ); + $this->assertSame( 'wp_template_part', $template_part->type ); + $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area ); + $this->assertEmpty( $template_part->modified ); + } + + /** + * @ticket 59325 + */ + public function test_should_ignore_post_types_property_when_building_template_part() { + $template = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + + $this->assertEmpty( $template->post_types ); + } + + /** + * @ticket 60506 + */ + public function test_should_inject_hooked_block_into_template_part() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/paragraph' => 'after', + ), + ) + ); + + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } + + /** + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_first_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'first_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + $this->assertStringStartsWith( '', $template_part->content ); + } + + /** + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_last_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_file( + array( + 'slug' => 'header', + 'postTypes' => array( 'post' ), + 'path' => DIR_TESTDATA . '/templates/template.html', + ), + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } +} diff --git a/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php new file mode 100644 index 0000000000000..5dbf91d911f07 --- /dev/null +++ b/tests/phpunit/tests/block-templates/buildBlockTemplateResultFromPost.php @@ -0,0 +1,199 @@ +is_registered( 'tests/my-block' ) ) { + $registry->unregister( 'tests/my-block' ); + } + + if ( $registry->is_registered( 'tests/ignored' ) ) { + $registry->unregister( 'tests/ignored' ); + } + + parent::tear_down(); + } + + /** + * @ticket 54335 + */ + public function test_should_build_template() { + $template = _build_block_template_result_from_post( + self::$template_post, + 'wp_template' + ); + + $this->assertNotWPError( $template ); + $this->assertSame( get_stylesheet() . '//my_template', $template->id ); + $this->assertSame( get_stylesheet(), $template->theme ); + $this->assertSame( 'my_template', $template->slug ); + $this->assertSame( 'publish', $template->status ); + $this->assertSame( 'custom', $template->source ); + $this->assertSame( 'My Template', $template->title ); + $this->assertSame( 'Description of my template', $template->description ); + $this->assertSame( 'wp_template', $template->type ); + $this->assertSame( self::$template_post->post_modified, $template->modified, 'Template result properties match' ); + } + + /** + * @ticket 54335 + */ + public function test_should_build_template_part() { + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertNotWPError( $template_part ); + $this->assertSame( get_stylesheet() . '//my_template_part', $template_part->id ); + $this->assertSame( get_stylesheet(), $template_part->theme ); + $this->assertSame( 'my_template_part', $template_part->slug ); + $this->assertSame( 'publish', $template_part->status ); + $this->assertSame( 'custom', $template_part->source ); + $this->assertSame( 'My Template Part', $template_part->title ); + $this->assertSame( 'Description of my template part', $template_part->description ); + $this->assertSame( 'wp_template_part', $template_part->type ); + $this->assertSame( WP_TEMPLATE_PART_AREA_HEADER, $template_part->area ); + $this->assertSame( self::$template_part_post->post_modified, $template_part->modified, 'Template part result properties match' ); + } + + /** + * @ticket 59646 + * @ticket 60506 + */ + public function test_should_inject_hooked_block_into_template() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/heading' => 'before', + ), + ) + ); + + $template = _build_block_template_result_from_post( + self::$template_post, + 'wp_template' + ); + $this->assertStringStartsWith( '', $template->content ); + } + + /** + * @ticket 59646 + * @ticket 60506 + */ + public function test_should_inject_hooked_block_into_template_part() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } + + /** + * @ticket 59646 + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_first_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'first_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringStartsWith( '', $template_part->content ); + } + + /** + * @ticket 59646 + * @ticket 60506 + * @ticket 60854 + */ + public function test_should_injected_hooked_block_into_template_part_last_child() { + register_block_type( + 'tests/my-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringEndsWith( '', $template_part->content ); + } + + /** + * @ticket 59646 + * @ticket 60506 + */ + public function test_should_not_inject_ignored_hooked_block_into_template() { + register_block_type( + 'tests/ignored', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + $template = _build_block_template_result_from_post( + self::$template_post, + 'wp_template' + ); + $this->assertStringNotContainsString( '', $template->content ); + } + + /** + * @ticket 59646 + * @ticket 60506 + */ + public function test_should_not_inject_ignored_hooked_block_into_template_part() { + register_block_type( + 'tests/ignored', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + $template_part = _build_block_template_result_from_post( + self::$template_part_post, + 'wp_template_part' + ); + $this->assertStringNotContainsString( '', $template_part->content ); + } +} diff --git a/tests/phpunit/tests/block-templates/getTemplateHierarchy.php b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php new file mode 100644 index 0000000000000..1a22e04aa60e7 --- /dev/null +++ b/tests/phpunit/tests/block-templates/getTemplateHierarchy.php @@ -0,0 +1,210 @@ + true, + 'show_in_rest' => true, + ) + ); + register_taxonomy( 'book_type', 'custom_book' ); + register_taxonomy( 'books', 'custom_book' ); + } + + public function tear_down() { + unregister_post_type( 'custom_book' ); + unregister_taxonomy( 'book_type' ); + unregister_taxonomy( 'books' ); + parent::tear_down(); + } + + /** + * @dataProvider data_get_template_hierarchy + * + * @ticket 56467 + * + * @param array $args Test arguments. + * @param array $expected Expected results. + */ + public function test_get_template_hierarchy( array $args, array $expected ) { + $this->assertSame( $expected, get_template_hierarchy( ...$args ) ); + } + + /** + * @ticket 60846 + */ + public function test_get_template_hierarchy_with_hooks() { + add_filter( + 'date_template_hierarchy', + function ( $templates ) { + return array_merge( array( 'date-custom' ), $templates ); + } + ); + $expected = array( 'date-custom', 'date', 'archive', 'index' ); + $this->assertSame( $expected, get_template_hierarchy( 'date' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_template_hierarchy() { + return array( + 'front-page' => array( + 'args' => array( 'front-page' ), + 'expected' => array( 'front-page', 'home', 'index' ), + ), + 'custom template' => array( + 'args' => array( 'whatever-slug', true ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'page' => array( + 'args' => array( 'page' ), + 'expected' => array( 'page', 'singular', 'index' ), + ), + 'tag' => array( + 'args' => array( 'tag' ), + 'expected' => array( 'tag', 'archive', 'index' ), + ), + 'author' => array( + 'args' => array( 'author' ), + 'expected' => array( 'author', 'archive', 'index' ), + ), + 'date' => array( + 'args' => array( 'date' ), + 'expected' => array( 'date', 'archive', 'index' ), + ), + 'taxonomy' => array( + 'args' => array( 'taxonomy' ), + 'expected' => array( 'taxonomy', 'archive', 'index' ), + ), + 'attachment' => array( + 'args' => array( 'attachment' ), + 'expected' => array( 'attachment', 'single', 'singular', 'index' ), + ), + 'singular' => array( + 'args' => array( 'singular' ), + 'expected' => array( 'singular', 'index' ), + ), + 'single' => array( + 'args' => array( 'single' ), + 'expected' => array( 'single', 'singular', 'index' ), + ), + 'archive' => array( + 'args' => array( 'archive' ), + 'expected' => array( 'archive', 'index' ), + ), + 'index' => array( + 'args' => array( 'index' ), + 'expected' => array( 'index' ), + ), + 'specific taxonomies' => array( + 'args' => array( 'taxonomy-books', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word categories' => array( + 'args' => array( 'category-fruits', false, 'category' ), + 'expected' => array( 'category-fruits', 'category', 'archive', 'index' ), + ), + 'single word categories no prefix' => array( + 'args' => array( 'category-fruits', false ), + 'expected' => array( 'category-fruits', 'category', 'archive', 'index' ), + ), + 'multi word categories' => array( + 'args' => array( 'category-fruits-yellow', false, 'category' ), + 'expected' => array( 'category-fruits-yellow', 'category', 'archive', 'index' ), + ), + 'multi word categories no prefix' => array( + 'args' => array( 'category-fruits-yellow', false ), + 'expected' => array( 'category-fruits-yellow', 'category', 'archive', 'index' ), + ), + 'single word taxonomy and term' => array( + 'args' => array( 'taxonomy-books-action', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word taxonomy and term no prefix' => array( + 'args' => array( 'taxonomy-books-action', false ), + 'expected' => array( 'taxonomy-books-action', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word taxonomy and multi word term' => array( + 'args' => array( 'taxonomy-books-action-adventure', false, 'taxonomy-books' ), + 'expected' => array( 'taxonomy-books-action-adventure', 'taxonomy-books', 'taxonomy', 'archive', 'index' ), + ), + 'multi word taxonomy and term' => array( + 'args' => array( 'taxonomy-greek-books-action-adventure', false, 'taxonomy-greek-books' ), + 'expected' => array( 'taxonomy-greek-books-action-adventure', 'taxonomy-greek-books', 'taxonomy', 'archive', 'index' ), + ), + 'single word post type' => array( + 'args' => array( 'single-book', false, 'single-book' ), + 'expected' => array( 'single-book', 'single', 'singular', 'index' ), + ), + 'multi word post type' => array( + 'args' => array( 'single-art-project', false, 'single-art-project' ), + 'expected' => array( 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single post with multi word post type' => array( + 'args' => array( 'single-art-project-imagine', false, 'single-art-project' ), + 'expected' => array( 'single-art-project-imagine', 'single-art-project', 'single', 'singular', 'index' ), + ), + 'single page' => array( + 'args' => array( 'page-hi', false, 'page' ), + 'expected' => array( 'page-hi', 'page', 'singular', 'index' ), + ), + 'authors' => array( + 'args' => array( 'author-rigas', false, 'author' ), + 'expected' => array( 'author-rigas', 'author', 'archive', 'index' ), + ), + 'multiple word taxonomy no prefix' => array( + 'args' => array( 'taxonomy-book_type-adventure', false ), + 'expected' => array( 'taxonomy-book_type-adventure', 'taxonomy-book_type', 'taxonomy', 'archive', 'index' ), + ), + 'single post type no prefix' => array( + 'args' => array( 'single-custom_book', false ), + 'expected' => array( + 'single-custom_book', + 'single', + 'singular', + 'index', + ), + ), + 'single post and post type no prefix' => array( + 'args' => array( 'single-custom_book-book-1', false ), + 'expected' => array( + 'single-custom_book-book-1', + 'single-custom_book', + 'single', + 'singular', + 'index', + ), + ), + 'page no prefix' => array( + 'args' => array( 'page-hi', false ), + 'expected' => array( + 'page-hi', + 'page', + 'singular', + 'index', + ), + ), + 'post type archive no prefix' => array( + 'args' => array( 'archive-book', false ), + 'expected' => array( + 'archive-book', + 'archive', + 'index', + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php b/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php new file mode 100644 index 0000000000000..e9c03e00d9daf --- /dev/null +++ b/tests/phpunit/tests/block-templates/injectIgnoredHookedBlocksMetadataAttributes.php @@ -0,0 +1,679 @@ +is_registered( 'tests/hooked-block' ) ) { + unregister_block_type( 'tests/hooked-block' ); + } + delete_post_meta( self::$template_part_post->ID, '_wp_ignored_hooked_blocks' ); + + parent::tear_down(); + } + + /** + * @ticket 60754 + */ + public function test_hooked_block_types_filter_with_newly_created_template() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_type = 'wp_template'; + $changes->post_status = 'publish'; + $changes->post_content = 'Hello'; + $changes->tax_input = array( + 'wp_theme' => get_stylesheet(), + ); + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertSame( + $changes->post_type, + $context->type, + 'The type field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_status, + $context->status, + 'The status field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertFalse( + $context->has_theme_file, + 'The has_theme_file field of the context passed to the hooked_block_types filter isn\'t set to false.' + ); + } + + /** + * @ticket 60754 + * @ticket 60854 + */ + public function test_hooked_block_types_filter_with_newly_created_template_part() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_type = 'wp_template_part'; + $changes->post_status = 'publish'; + $changes->post_content = 'Hello'; + $changes->tax_input = array( + 'wp_theme' => get_stylesheet(), + 'wp_template_part_area' => WP_TEMPLATE_PART_AREA_HEADER, + ); + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); + $this->assertSame( + $changes->post_type, + $context->type, + 'The type field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_status, + $context->status, + 'The status field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertFalse( + $context->has_theme_file, + 'The has_theme_file field of the context passed to the hooked_block_types filter isn\'t set to false.' + ); + $this->assertSame( + $changes->tax_input['wp_template_part_area'], + $context->area, + 'The area field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + } + + /** + * @ticket 60754 + */ + public function test_hooked_block_types_filter_with_existing_template_file() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_name = 'index'; + $changes->post_type = 'wp_template'; + $changes->post_status = 'publish'; + $changes->post_content = 'Hello'; + $changes->meta_input = array( + 'origin' => 'theme', + ); + $changes->tax_input = array( + 'wp_theme' => get_stylesheet(), + ); + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertSame( + $changes->post_name, + $context->slug, + 'The slug field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_type, + $context->type, + 'The type field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_status, + $context->status, + 'The status field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertTrue( + $context->has_theme_file, + 'The has_theme_file field of the context passed to the hooked_block_types filter isn\'t set to true.' + ); + $this->assertSame( + $changes->meta_input['origin'], + $context->origin, + 'The origin field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + } + + /** + * @ticket 60754 + * @ticket 60854 + */ + public function test_hooked_block_types_filter_with_existing_template_part_file() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_name = 'small-header'; + $changes->post_type = 'wp_template_part'; + $changes->post_status = 'publish'; + $changes->post_content = 'Hello'; + $changes->meta_input = array( + 'origin' => 'theme', + ); + $changes->tax_input = array( + 'wp_theme' => get_stylesheet(), + 'wp_template_part_area' => WP_TEMPLATE_PART_AREA_HEADER, + ); + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); + $this->assertSame( + $changes->post_name, + $context->slug, + 'The slug field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_type, + $context->type, + 'The type field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_status, + $context->status, + 'The status field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertTrue( + $context->has_theme_file, + 'The has_theme_file field of the context passed to the hooked_block_types filter isn\'t set to true.' + ); + $this->assertSame( + $changes->meta_input['origin'], + $context->origin, + 'The origin field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->tax_input['wp_template_part_area'], + $context->area, + 'The area field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + } + + /** + * @ticket 60754 + */ + public function test_hooked_block_types_filter_with_existing_template_post() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_name = 'my-updated-template'; + $changes->ID = self::$template_post->ID; + $changes->post_content = 'Hello'; + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'tests/anchor-block', + 'tests/anchor-block', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertSame( + $changes->post_name, + $context->slug, + 'The slug field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->ID, + $context->wp_id, + 'The wp_id field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + 'publish', + $context->status, + 'The status field of the context passed to the hooked_block_types filter isn\'t set to publish.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + + $this->assertSame( + self::$template_post->post_title, + $context->title, + 'The title field of the context passed to the hooked_block_types filter doesn\'t match the template post object.' + ); + $this->assertSame( + self::$template_post->post_excerpt, + $context->description, + 'The description field of the context passed to the hooked_block_types filter doesn\'t match the template post object.' + ); + } + + /** + * @ticket 60754 + * @ticket 60854 + */ + public function test_hooked_block_types_filter_with_existing_template_part_post() { + $action = new MockAction(); + add_filter( 'hooked_block_types', array( $action, 'filter' ), 10, 4 ); + + $changes = new stdClass(); + $changes->post_name = 'my-updated-template-part'; + $changes->ID = self::$template_part_post->ID; + $changes->post_content = 'Hello'; + + $changes->tax_input = array( + 'wp_template_part_area' => WP_TEMPLATE_PART_AREA_FOOTER, + ); + + inject_ignored_hooked_blocks_metadata_attributes( $changes ); + + $args = $action->get_args(); + $relative_positions = array_column( $args, 1 ); + $anchor_block_types = array_column( $args, 2 ); + $contexts = array_column( $args, 3 ); + + $this->assertSame( + array( + 'before', + 'after', + 'first_child', + 'before', + 'after', + 'last_child', + ), + $relative_positions, + 'The relative positions passed to the hooked_block_types filter are incorrect.' + ); + + $this->assertSame( + array( + 'core/template-part', + 'core/template-part', + 'core/template-part', + 'tests/anchor-block', + 'tests/anchor-block', + 'core/template-part', + ), + $anchor_block_types, + 'The anchor block types passed to the hooked_block_types filter are incorrect.' + ); + + $context = $contexts[0]; + $this->assertSame( + array_fill( 0, count( $contexts ), $context ), + $contexts, + 'The context passed to the hooked_block_types filter should be the same for all calls.' + ); + $this->assertInstanceOf( + 'WP_Block_Template', + $context, + 'The context passed to the hooked_block_types filter is not an instance of WP_Block_Template.' + ); + $this->assertSame( + $changes->post_name, + $context->slug, + 'The slug field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->ID, + $context->wp_id, + 'The wp_id field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + 'publish', + $context->status, + 'The status field of the context passed to the hooked_block_types filter isn\'t set to publish.' + ); + $this->assertSame( + $changes->post_content, + $context->content, + 'The content field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + $this->assertSame( + $changes->tax_input['wp_template_part_area'], + $context->area, + 'The area field of the context passed to the hooked_block_types filter doesn\'t match the template changes.' + ); + + $this->assertSame( + self::$template_part_post->post_title, + $context->title, + 'The title field of the context passed to the hooked_block_types filter doesn\'t match the template post object.' + ); + $this->assertSame( + self::$template_part_post->post_excerpt, + $context->description, + 'The description field of the context passed to the hooked_block_types filter doesn\'t match the template post object.' + ); + } + + /** + * @ticket 60671 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'tests/anchor-block' => 'after', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template'; + $template = get_block_template( $id, 'wp_template' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + $changes->post_content = 'Hello'; + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertSame( + 'Hello', + $post->post_content, + 'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.' + ); + } + + /** + * @ticket 60671 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'tests/anchor-block' => 'after', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template_part'; + $template = get_block_template( $id, 'wp_template_part' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + $changes->post_content = 'Hello'; + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertSame( + 'Hello', + $post->post_content, + 'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.' + ); + } + + /** + * @ticket 60854 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part_postmeta() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/template-part' => 'last_child', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template_part'; + $template = get_block_template( $id, 'wp_template_part' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + $changes->post_content = 'Hello'; + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertSame( + array( 'tests/hooked-block' ), + json_decode( $post->meta_input['_wp_ignored_hooked_blocks'], true ), + 'The hooked block was not injected into the wp_template_part\'s _wp_ignored_hooked_blocks postmeta.' + ); + $this->assertSame( + $changes->post_content, + $post->post_content, + 'The template part\'s post content was modified.' + ); + } + + /** + * @ticket 61550 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_with_no_changes_to_post_content() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template'; + $template = get_block_template( $id, 'wp_template' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + + // Note that we're not setting `$changes->post_content`! + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertFalse( + isset( $post->post_content ), + "post_content shouldn't have been set." + ); + } + + /** + * @ticket 61550 + */ + public function test_inject_ignored_hooked_blocks_metadata_attributes_into_template_part_with_no_changes_to_post_content() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + $id = self::TEST_THEME . '//' . 'my_template_part'; + $template = get_block_template( $id, 'wp_template_part' ); + + $changes = new stdClass(); + $changes->ID = $template->wp_id; + // Note that we're not setting `$changes->post_content`! + + $post = inject_ignored_hooked_blocks_metadata_attributes( $changes ); + $this->assertFalse( + isset( $post->post_content ), + "post_content shouldn't have been set." + ); + } +} diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContent.php b/tests/phpunit/tests/blocks/applyBlockHooksToContent.php new file mode 100644 index 0000000000000..150560dbaba24 --- /dev/null +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContent.php @@ -0,0 +1,194 @@ + array( + 'core/post-content' => 'after', + ), + ) + ); + + register_block_type( + 'tests/hooked-block-with-multiple-false', + array( + 'block_hooks' => array( + 'tests/other-anchor-block' => 'after', + ), + 'supports' => array( + 'multiple' => false, + ), + ) + ); + + register_block_type( + 'tests/dynamically-hooked-block-with-multiple-false', + array( + 'supports' => array( + 'multiple' => false, + ), + ) + ); + } + + /** + * Tear down. + * + * @ticket 61902. + */ + public static function wpTearDownAfterClass() { + $registry = WP_Block_Type_Registry::get_instance(); + + $registry->unregister( 'tests/hooked-block' ); + $registry->unregister( 'tests/hooked-block-with-multiple-false' ); + $registry->unregister( 'tests/dynamically-hooked-block-with-multiple-false' ); + } + + /** + * @ticket 61902 + */ + public function test_apply_block_hooks_to_content_sets_theme_attribute_on_template_part_block() { + $context = new WP_Block_Template(); + $context->content = ''; + + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + $this->assertSame( + sprintf( '', get_stylesheet() ), + $actual + ); + } + + /** + * @ticket 61902 + * @ticket 63287 + */ + public function test_apply_block_hooks_to_content_inserts_hooked_block() { + $context = new WP_Block_Template(); + $context->content = ''; + + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + $this->assertSame( + '', + $actual + ); + } + + /** + * @ticket 61074 + * @ticket 63287 + */ + public function test_apply_block_hooks_to_content_with_context_set_to_null() { + $content = ''; + + /* + * apply_block_hooks_to_content() will fall back to the global $post object (via get_post()) + * if the $context parameter is null. However, we'd also like to ensure that the function + * works as expected even when get_post() returns null. + */ + $this->assertNull( get_post() ); + + $actual = apply_block_hooks_to_content( $content, null, 'insert_hooked_blocks' ); + $this->assertSame( + '', + $actual + ); + } + + /** + * @ticket 61902 + */ + public function test_apply_block_hooks_to_content_respect_multiple_false() { + $context = new WP_Block_Template(); + $context->content = ''; + + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + $this->assertSame( + '', + $actual + ); + } + + /** + * @ticket 61902 + */ + public function test_apply_block_hooks_to_content_respect_multiple_false_after_inserting_once() { + $context = new WP_Block_Template(); + $context->content = ''; + + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + $this->assertSame( + '', + $actual + ); + } + + /** + * @ticket 61902 + */ + public function test_apply_block_hooks_to_content_respect_multiple_false_with_filter() { + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) { + if ( 'tests/yet-another-anchor-block' === $anchor_block_type && 'after' === $relative_position ) { + $hooked_block_types[] = 'tests/dynamically-hooked-block-with-multiple-false'; + } + + return $hooked_block_types; + }; + + $context = new WP_Block_Template(); + $context->content = ''; + + add_filter( 'hooked_block_types', $filter, 10, 3 ); + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( + '', + $actual + ); + } + + /** + * @ticket 61902 + */ + public function test_apply_block_hooks_to_content_respect_multiple_false_after_inserting_once_with_filter() { + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) { + if ( 'tests/yet-another-anchor-block' === $anchor_block_type && 'after' === $relative_position ) { + $hooked_block_types[] = 'tests/dynamically-hooked-block-with-multiple-false'; + } + + return $hooked_block_types; + }; + + $context = new WP_Block_Template(); + $context->content = ''; + + add_filter( 'hooked_block_types', $filter, 10, 3 ); + $actual = apply_block_hooks_to_content( $context->content, $context, 'insert_hooked_blocks' ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( + '', + $actual + ); + } +} diff --git a/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php new file mode 100644 index 0000000000000..4f95727524c8c --- /dev/null +++ b/tests/phpunit/tests/blocks/applyBlockHooksToContentFromPostObject.php @@ -0,0 +1,242 @@ +post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

      Hello World!

      ', + ) + ); + + self::$post_with_ignored_hooked_block = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

      Hello World!

      ', + 'meta_input' => array( + '_wp_ignored_hooked_blocks' => '["tests/hooked-block-first-child"]', + ), + ) + ); + + self::$post_with_non_block_content = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_content' => '

      Hello World!

      ', + ) + ); + + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'core/heading' => 'after', + ), + ) + ); + + register_block_type( + 'tests/hooked-block-first-child', + array( + 'block_hooks' => array( + 'core/post-content' => 'first_child', + ), + ) + ); + + register_block_type( + 'tests/hooked-block-after-post-content', + array( + 'block_hooks' => array( + 'core/post-content' => 'after', + ), + ) + ); + + register_block_type( 'tests/dynamically-hooked-block-before-post-content' ); + } + + /** + * Tear down. + * + * @ticket 62716 + */ + public static function wpTearDownAfterClass() { + $registry = WP_Block_Type_Registry::get_instance(); + + $registry->unregister( 'tests/hooked-block' ); + $registry->unregister( 'tests/hooked-block-first-child' ); + $registry->unregister( 'tests/hooked-block-after-post-content' ); + $registry->unregister( 'tests/dynamically-hooked-block-before-post-content' ); + } + + /** + * @ticket 62716 + */ + public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block() { + $expected = '' . + self::$post->post_content . + ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post->post_content, + self::$post, + 'insert_hooked_blocks' + ); + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 65008 + */ + public function test_apply_block_hooks_to_content_from_post_object_sets_ignored_hooked_blocks() { + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . + '' . + '

      Hello World!

      ' . + '' . + ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post->post_content, + self::$post, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + $this->assertSame( $expected, $actual, "Markup wasn't updated correctly." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Hooked block added at 'first_child' position wasn't added to ignoredHookedBlocks metadata." + ); + } + + /** + * @ticket 62716 + * @ticket 65008 + */ + public function test_apply_block_hooks_to_content_from_post_object_respects_ignored_hooked_blocks_post_meta() { + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . + '

      Hello World!

      ' . + '' . + ''; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post_with_ignored_hooked_block->post_content, + self::$post_with_ignored_hooked_block, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + $this->assertSame( $expected, $actual ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Pre-existing ignored hooked block at root level wasn't reflected in metadata." + ); + } + + /** + * @ticket 63287 + * @ticket 65008 + */ + public function test_apply_block_hooks_to_content_from_post_object_does_not_insert_hooked_block_before_container_block() { + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) { + if ( 'core/post-content' === $anchor_block_type && 'before' === $relative_position ) { + $hooked_block_types[] = 'tests/dynamically-hooked-block-before-post-content'; + } + + return $hooked_block_types; + }; + + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . + '' . + '

      Hello World!

      ' . + '' . + ''; + + add_filter( 'hooked_block_types', $filter, 10, 3 ); + $actual = apply_block_hooks_to_content_from_post_object( + self::$post->post_content, + self::$post, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( $expected, $actual, "Hooked block added before 'core/post-content' block shouldn't be inserted." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "ignoredHookedBlocks metadata wasn't set correctly." + ); + } + + /** + * @ticket 62716 + * @ticket 65008 + */ + public function test_apply_block_hooks_to_content_from_post_object_inserts_hooked_block_if_content_contains_no_blocks() { + $ignored_hooked_blocks_at_root = array(); + + $expected = '' . self::$post_with_non_block_content->post_content; + $actual = apply_block_hooks_to_content_from_post_object( + self::$post_with_non_block_content->post_content, + self::$post_with_non_block_content, + 'insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata', + $ignored_hooked_blocks_at_root + ); + $this->assertSame( $expected, $actual, "Markup wasn't updated correctly." ); + $this->assertSame( + array( 'tests/hooked-block-first-child' ), + $ignored_hooked_blocks_at_root, + "Hooked block added at 'first_child' position wasn't added to ignoredHookedBlocks metadata." + ); + } +} diff --git a/tests/phpunit/tests/blocks/context.php b/tests/phpunit/tests/blocks/context.php deleted file mode 100644 index d1b321ab519a1..0000000000000 --- a/tests/phpunit/tests/blocks/context.php +++ /dev/null @@ -1,215 +0,0 @@ - 'example', - 'post_excerpt' => '', - ); - - $post = $this->factory()->post->create_and_get( $args ); - setup_postdata( $post ); - } - - /** - * Tear down each test method. - */ - public function tear_down() { - while ( ! empty( $this->registered_block_names ) ) { - $block_name = array_pop( $this->registered_block_names ); - unregister_block_type( $block_name ); - } - - parent::tear_down(); - } - - /** - * Registers a block type. - * - * @param string|WP_Block_Type $name Block type name including namespace, or alternatively a - * complete WP_Block_Type instance. In case a WP_Block_Type - * is provided, the $args parameter will be ignored. - * @param array $args { - * Optional. Array of block type arguments. Any arguments may be defined, however the - * ones described below are supported by default. Default empty array. - * - * @type callable $render_callback Callback used to render blocks of this block type. - * } - */ - protected function register_block_type( $name, $args ) { - register_block_type( $name, $args ); - - $this->registered_block_names[] = $name; - } - - /** - * Tests that a block which provides context makes that context available to - * its inner blocks. - * - * @ticket 49927 - */ - public function test_provides_block_context() { - $provided_context = array(); - - $this->register_block_type( - 'gutenberg/test-context-provider', - array( - 'attributes' => array( - 'contextWithAssigned' => array( - 'type' => 'number', - ), - 'contextWithDefault' => array( - 'type' => 'number', - 'default' => 0, - ), - 'contextWithoutDefault' => array( - 'type' => 'number', - ), - 'contextNotRequested' => array( - 'type' => 'number', - ), - ), - 'provides_context' => array( - 'gutenberg/contextWithAssigned' => 'contextWithAssigned', - 'gutenberg/contextWithDefault' => 'contextWithDefault', - 'gutenberg/contextWithoutDefault' => 'contextWithoutDefault', - 'gutenberg/contextNotRequested' => 'contextNotRequested', - ), - ) - ); - - $this->register_block_type( - 'gutenberg/test-context-consumer', - array( - 'uses_context' => array( - 'gutenberg/contextWithDefault', - 'gutenberg/contextWithAssigned', - 'gutenberg/contextWithoutDefault', - ), - 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) { - $provided_context[] = $block->context; - - return ''; - }, - ) - ); - - $parsed_blocks = parse_blocks( - '' . - '' . - '' - ); - - render_block( $parsed_blocks[0] ); - - $this->assertSame( - array( - 'gutenberg/contextWithDefault' => 0, - 'gutenberg/contextWithAssigned' => 10, - ), - $provided_context[0] - ); - } - - /** - * Tests that a block can receive default-provided context through - * render_block. - * - * @ticket 49927 - */ - public function test_provides_default_context() { - global $post; - - $provided_context = array(); - - $this->register_block_type( - 'gutenberg/test-context-consumer', - array( - 'uses_context' => array( 'postId', 'postType' ), - 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) { - $provided_context[] = $block->context; - - return ''; - }, - ) - ); - - $parsed_blocks = parse_blocks( '' ); - - render_block( $parsed_blocks[0] ); - - $this->assertSame( - array( - 'postId' => $post->ID, - 'postType' => $post->post_type, - ), - $provided_context[0] - ); - } - - /** - * Tests that default block context can be filtered. - * - * @ticket 49927 - */ - public function test_default_context_is_filterable() { - $provided_context = array(); - - $this->register_block_type( - 'gutenberg/test-context-consumer', - array( - 'uses_context' => array( 'example' ), - 'render_callback' => static function( $attributes, $content, $block ) use ( &$provided_context ) { - $provided_context[] = $block->context; - - return ''; - }, - ) - ); - - $filter_block_context = static function( $context ) { - $context['example'] = 'ok'; - return $context; - }; - - $parsed_blocks = parse_blocks( '' ); - - add_filter( 'render_block_context', $filter_block_context ); - - render_block( $parsed_blocks[0] ); - - remove_filter( 'render_block_context', $filter_block_context ); - - $this->assertSame( array( 'example' => 'ok' ), $provided_context[0] ); - } - -} diff --git a/tests/phpunit/tests/blocks/editor.php b/tests/phpunit/tests/blocks/editor.php index 410f6777ed98a..1de4dbf1719a6 100644 --- a/tests/phpunit/tests/blocks/editor.php +++ b/tests/phpunit/tests/blocks/editor.php @@ -1,21 +1,14 @@ 'Example', ); - $post = $this->factory()->post->create_and_get( $args ); + $post = self::factory()->post->create_and_get( $args ); global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); + + global $post_ID; + $post_ID = 1; + + global $wp_scripts, $wp_styles; + $this->original_wp_scripts = $wp_scripts; + $this->original_wp_styles = $wp_styles; + $wp_scripts = null; + $wp_styles = null; + wp_scripts(); + wp_styles(); } public function tear_down() { + global $wp_scripts, $wp_styles; + $wp_scripts = $this->original_wp_scripts; + $wp_styles = $this->original_wp_styles; + /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; $wp_rest_server = null; + global $post_ID; + $post_ID = null; parent::tear_down(); } + /** + * @var WP_Scripts|null + */ + protected $original_wp_scripts; + + /** + * @var WP_Styles|null + */ + protected $original_wp_styles; + public function filter_set_block_categories_post( $block_categories, $post ) { if ( empty( $post ) ) { return $block_categories; @@ -66,7 +88,7 @@ public function filter_set_allowed_block_types_post( $allowed_block_types, $post public function filter_set_block_editor_settings_post( $editor_settings, $post ) { if ( empty( $post ) ) { - return $allowed_block_types; + return $editor_settings; } return array( @@ -80,6 +102,7 @@ public function filter_set_block_editor_settings_post( $editor_settings, $post ) public function test_block_editor_context_no_settings() { $context = new WP_Block_Editor_Context(); + $this->assertSame( 'core/edit-post', $context->name ); $this->assertNull( $context->post ); } @@ -89,9 +112,40 @@ public function test_block_editor_context_no_settings() { public function test_block_editor_context_post() { $context = new WP_Block_Editor_Context( array( 'post' => get_post() ) ); + $this->assertSame( 'core/edit-post', $context->name ); $this->assertSame( get_post(), $context->post ); } + /** + * @ticket 55301 + */ + public function test_block_editor_context_widgets() { + $context = new WP_Block_Editor_Context( array( 'name' => 'core/edit-widgets' ) ); + + $this->assertSame( 'core/edit-widgets', $context->name ); + $this->assertNull( $context->post ); + } + + /** + * @ticket 55301 + */ + public function test_block_editor_context_widgets_customizer() { + $context = new WP_Block_Editor_Context( array( 'name' => 'core/customize-widgets' ) ); + + $this->assertSame( 'core/customize-widgets', $context->name ); + $this->assertNull( $context->post ); + } + + /** + * @ticket 55301 + */ + public function test_block_editor_context_site() { + $context = new WP_Block_Editor_Context( array( 'name' => 'core/edit-site' ) ); + + $this->assertSame( 'core/edit-site', $context->name ); + $this->assertNull( $context->post ); + } + /** * @ticket 52920 * @expectedDeprecated block_categories @@ -170,7 +224,7 @@ public function test_get_allowed_block_types_deprecated_filter_post_editor() { public function test_get_default_block_editor_settings() { $settings = get_default_block_editor_settings(); - $this->assertCount( 16, $settings ); + $this->assertCount( 20, $settings ); $this->assertFalse( $settings['alignWide'] ); $this->assertIsArray( $settings['allowedMimeTypes'] ); $this->assertTrue( $settings['allowedBlockTypes'] ); @@ -208,7 +262,7 @@ public function test_get_default_block_editor_settings() { ), array( 'slug' => 'reusable', - 'title' => 'Reusable Blocks', + 'title' => 'Patterns', 'icon' => null, ), ), @@ -217,6 +271,7 @@ public function test_get_default_block_editor_settings() { $this->assertFalse( $settings['disableCustomColors'] ); $this->assertFalse( $settings['disableCustomFontSizes'] ); $this->assertFalse( $settings['disableCustomGradients'] ); + $this->assertFalse( $settings['disableLayoutStyles'] ); $this->assertFalse( $settings['enableCustomLineHeight'] ); $this->assertFalse( $settings['enableCustomSpacing'] ); $this->assertFalse( $settings['enableCustomUnits'] ); @@ -265,6 +320,33 @@ public function test_get_default_block_editor_settings() { $settings['imageSizes'] ); $this->assertIsInt( $settings['maxUploadFileSize'] ); + $this->assertSame( admin_url( '/' ), $settings['__experimentalDashboardLink'] ); + $this->assertTrue( $settings['__unstableGalleryWithImageBlocks'] ); + } + + /** + * @ticket 56815 + */ + public function test_get_default_block_editor_settings_max_upload_file_size() { + // Force the return value of wp_max_upload_size() to be 500. + add_filter( + 'upload_size_limit', + static function () { + return 500; + } + ); + + // Expect 0 when user is not allowed to upload (as wp_max_upload_size() should not be called). + $settings = get_default_block_editor_settings(); + $this->assertSame( 0, $settings['maxUploadFileSize'] ); + + // Set up an administrator, as they can upload files. + $administrator = self::factory()->user->create( array( 'role' => 'administrator' ) ); + wp_set_current_user( $administrator ); + + // Expect the above 500 as the user is now allowed to upload. + $settings = get_default_block_editor_settings(); + $this->assertSame( 500, $settings['maxUploadFileSize'] ); } /** @@ -343,6 +425,65 @@ function filter_block_editor_settings_my_editor( $editor_settings ) { $this->assertSame( 12345, $settings['maxUploadFileSize'] ); } + /** + * @ticket 58534 + */ + public function test_wp_get_first_block() { + $block_name = 'core/paragraph'; + $blocks = array( + array( + 'blockName' => 'core/image', + ), + array( + 'blockName' => $block_name, + 'attrs' => array( + 'content' => 'Hello World!', + ), + ), + array( + 'blockName' => 'core/heading', + ), + array( + 'blockName' => $block_name, + ), + ); + $blocks_with_no_paragraph = array( + array( + 'blockName' => 'core/image', + ), + array( + 'blockName' => 'core/heading', + ), + ); + + $this->assertSame( $blocks[1], wp_get_first_block( $blocks, $block_name ) ); + + $this->assertSame( array(), wp_get_first_block( $blocks_with_no_paragraph, $block_name ) ); + } + + /** + * @ticket 58534 + */ + public function test_wp_get_post_content_block_attributes() { + $attributes_with_layout = array( + 'layout' => array( + 'type' => 'constrained', + ), + ); + // With no block theme, expect null. + $this->assertNull( wp_get_post_content_block_attributes() ); + + switch_theme( 'block-theme' ); + + $this->assertSame( $attributes_with_layout, wp_get_post_content_block_attributes() ); + } + + public function test_wp_get_post_content_block_attributes_no_layout() { + switch_theme( 'block-theme-post-content-default' ); + + $this->assertSame( array(), wp_get_post_content_block_attributes() ); + } + /** * @ticket 53458 */ @@ -403,10 +544,31 @@ public function test_get_block_editor_settings_theme_json_settings() { $this->assertSameSets( array( 'rem' ), $settings['enableCustomUnits'] ); // settings.spacing.customPadding $this->assertTrue( $settings['enableCustomSpacing'] ); + // settings.postContentAttributes + $this->assertSameSets( + array( + 'layout' => array( + 'type' => 'constrained', + ), + ), + $settings['postContentAttributes'] + ); switch_theme( WP_DEFAULT_THEME ); } + /** + * @ticket 59358 + */ + public function test_get_block_editor_settings_without_post_content_block() { + + $post_editor_context = new WP_Block_Editor_Context( array( 'post' => get_post() ) ); + + $settings = get_block_editor_settings( array(), $post_editor_context ); + + $this->assertArrayNotHasKey( 'postContentAttributes', $settings ); + } + /** * @ticket 52920 * @expectedDeprecated block_editor_settings @@ -421,7 +583,8 @@ public function test_get_block_editor_settings_deprecated_filter_post_editor() { $this->assertSameSets( array( - 'filter' => 'deprecated', + 'canEditCSS' => false, + 'filter' => 'deprecated', ), $settings ); @@ -490,22 +653,142 @@ function filter_add_preload_paths( $preload_paths, WP_Block_Editor_Context $cont $after = implode( '', wp_scripts()->registered['wp-api-fetch']->extra['after'] ); $this->assertStringContainsString( 'wp.apiFetch.createPreloadingMiddleware', $after ); - $this->assertStringContainsString( '"\/wp\/v2\/blocks"', $after ); - $this->assertStringContainsString( '"\/wp\/v2\/types"', $after ); + $this->assertStringContainsString( '"/wp/v2/blocks"', $after ); + $this->assertStringContainsString( '"/wp/v2/types"', $after ); } /** - * @ticket 53344 + * @ticket 54558 + * @dataProvider data_block_editor_rest_api_preload_adds_missing_leading_slash + * + * @covers ::block_editor_rest_api_preload + * + * @param array $preload_paths The paths to preload. + * @param string $expected The expected substring. */ - public function test_get_block_editor_theme_styles() { - $theme_styles = get_block_editor_theme_styles(); - $this->assertCount( 1, $theme_styles ); - $this->assertSameSets( + public function test_block_editor_rest_api_preload_adds_missing_leading_slash( array $preload_paths, $expected ) { + block_editor_rest_api_preload( $preload_paths, new WP_Block_Editor_Context() ); + $haystack = implode( '', wp_scripts()->registered['wp-api-fetch']->extra['after'] ); + $this->assertStringContainsString( $expected, $haystack ); + } + + /** + * @ticket 57547 + * + * @covers ::get_classic_theme_supports_block_editor_settings + */ + public function test_get_classic_theme_supports_block_editor_settings() { + $font_sizes = array( + array( + 'name' => 'Small', + 'size' => 12, + 'slug' => 'small', + ), array( - 'css' => 'body { font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Oxygen-Sans,Ubuntu,Cantarell,"Helvetica Neue",sans-serif }', - '__unstableType' => 'core', + 'name' => 'Regular', + 'size' => 16, + 'slug' => 'regular', + ), + ); + + add_theme_support( 'editor-font-sizes', $font_sizes ); + $settings = get_classic_theme_supports_block_editor_settings(); + remove_theme_support( 'editor-font-sizes' ); + + $this->assertFalse( $settings['disableCustomColors'], 'Value for array key "disableCustomColors" does not match expectations' ); + $this->assertFalse( $settings['disableCustomFontSizes'], 'Value for array key "disableCustomFontSizes" does not match expectations' ); + $this->assertFalse( $settings['disableCustomGradients'], 'Value for array key "disableCustomGradients" does not match expectations' ); + $this->assertFalse( $settings['disableLayoutStyles'], 'Value for array key "disableLayoutStyles" does not match expectations' ); + $this->assertFalse( $settings['enableCustomLineHeight'], 'Value for array key "enableCustomLineHeight" does not match expectations' ); + $this->assertFalse( $settings['enableCustomSpacing'], 'Value for array key "enableCustomSpacing" does not match expectations' ); + $this->assertFalse( $settings['enableCustomUnits'], 'Value for array key "enableCustomUnits" does not match expectations' ); + + $this->assertSame( + $font_sizes, + $settings['fontSizes'], + 'Value for array key "fontSizes" does not match expectations' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_block_editor_rest_api_preload_adds_missing_leading_slash() { + return array( + 'a string without a slash' => array( + 'preload_paths' => array( 'wp/v2/blocks' ), + 'expected' => '/wp/v2/blocks', + ), + 'a string with a slash' => array( + 'preload_paths' => array( '/wp/v2/blocks' ), + 'expected' => '/wp/v2/blocks', + ), + 'a string starting with a question mark' => array( + 'preload_paths' => array( '?context=edit' ), + 'expected' => '/?context=edit', ), - $theme_styles[0] + 'an array with a string without a slash' => array( + 'preload_paths' => array( array( 'wp/v2/blocks', 'OPTIONS' ) ), + 'expected' => '/wp/v2/blocks', + ), + 'an array with a string with a slash' => array( + 'preload_paths' => array( array( '/wp/v2/blocks', 'OPTIONS' ) ), + 'expected' => '/wp/v2/blocks', + ), + 'an array with a string starting with a question mark' => array( + 'preload_paths' => array( array( '?context=edit', 'OPTIONS' ) ), + 'expected' => '/?context=edit', + ), + ); + } + + /** + * @ticket 62797 + * + * @covers ::block_editor_rest_api_preload + * + * Some valid JSON-encoded data is dangerous to embed in HTML without appropriate + * escaping. This test includes an example of data that would prevent the enclosing + * `` tag from closing on its apparent closer and remain open. + */ + public function test_ensure_preload_data_script_tag_closes() { + add_theme_support( 'html5', array( 'script' ) ); + register_rest_route( + 'test/v0', + 'test-62797', + array( + 'methods' => 'GET', + 'callback' => function () { + return 'Unclosed comment and a script open tag ', + $template->content + ); + $this->assertStringContainsString( + '' + . '', + $template->content + ); + $this->assertStringNotContainsString( + '', + $template->content + ); + $this->assertStringNotContainsString( + '', + $template->content + ); + } + + /** + * @ticket 59313 + * @ticket 60008 + * @ticket 60506 + * + * @covers ::get_block_file_template + */ + public function test_loading_template_part_with_hooked_blocks() { + $this->switch_to_block_theme_hooked_blocks(); + + $template = get_block_file_template( get_stylesheet() . '//header', 'wp_template_part' ); + + $this->assertStringContainsString( + '' + . '', + $template->content + ); + $this->assertStringNotContainsString( + '', + $template->content + ); + $this->assertStringNotContainsString( + '', + $template->content + ); + $this->assertStringNotContainsString( + '', + $template->content + ); + } + + /** + * @ticket 59313 + * @ticket 60008 + * @ticket 60506 + * + * @covers WP_Block_Patterns_Registry::get_registered + */ + public function test_loading_pattern_with_hooked_blocks() { + $this->switch_to_block_theme_hooked_blocks(); + + $pattern = WP_Block_Patterns_Registry::get_instance()->get_registered( + get_stylesheet() . '/hidden-comments' + ); + + $this->assertStringNotContainsString( + '', + $pattern['content'] + ); + $this->assertStringNotContainsString( + '', + $pattern['content'] + ); + $this->assertStringContainsString( + '' + . '
      ' + . '', + str_replace( array( "\n", "\t" ), '', $pattern['content'] ) + ); + $this->assertStringContainsString( + '' + . '', + str_replace( array( "\n", "\t" ), '', $pattern['content'] ) + ); + } +} diff --git a/tests/phpunit/tests/blocks/insertHookedBlocks.php b/tests/phpunit/tests/blocks/insertHookedBlocks.php new file mode 100644 index 0000000000000..552e04f86102d --- /dev/null +++ b/tests/phpunit/tests/blocks/insertHookedBlocks.php @@ -0,0 +1,212 @@ + array( + 'after' => array( self::HOOKED_BLOCK_TYPE ), + 'before' => array( self::OTHER_HOOKED_BLOCK_TYPE ), + ), + ); + + /** + * @ticket 59572 + * @ticket 60126 + * @ticket 60506 + */ + public function test_insert_hooked_blocks_returns_correct_markup() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + ); + + $actual = insert_hooked_blocks( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + $this->assertSame( + '', + $actual, + "Markup for hooked block wasn't generated correctly." + ); + } + + /** + * @ticket 59572 + * @ticket 60126 + * @ticket 60506 + */ + public function test_insert_hooked_blocks_if_block_is_ignored() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array( + 'metadata' => array( + 'ignoredHookedBlocks' => array( self::HOOKED_BLOCK_TYPE ), + ), + ), + ); + + $actual = insert_hooked_blocks( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + $this->assertSame( + '', + $actual, + "No markup should've been generated for ignored hooked block." + ); + } + + /** + * @ticket 59572 + * @ticket 60126 + * @ticket 60506 + */ + public function test_insert_hooked_blocks_if_other_block_is_ignored() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array( + 'metadata' => array( + 'ignoredHookedBlocks' => array( self::HOOKED_BLOCK_TYPE ), + ), + ), + ); + + $actual = insert_hooked_blocks( $anchor_block, 'before', self::HOOKED_BLOCKS, array() ); + $this->assertSame( + '', + $actual, + "Markup for newly hooked block should've been generated." + ); + } + + /** + * @ticket 59572 + * @ticket 60126 + * @ticket 60506 + */ + public function test_insert_hooked_blocks_filter_can_set_attributes() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + 'attrs' => array( + 'layout' => array( + 'type' => 'constrained', + ), + ), + 'innerContent' => array(), + ); + + $filter = function ( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) { + // Is the hooked block adjacent to the anchor block? + if ( 'before' !== $relative_position && 'after' !== $relative_position ) { + return $parsed_hooked_block; + } + + // Does the anchor block have a layout attribute? + if ( isset( $parsed_anchor_block['attrs']['layout'] ) ) { + // Copy the anchor block's layout attribute to the hooked block. + $parsed_hooked_block['attrs']['layout'] = $parsed_anchor_block['attrs']['layout']; + } + + return $parsed_hooked_block; + }; + add_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter, 10, 4 ); + $actual = insert_hooked_blocks( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + remove_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter ); + + $this->assertSame( + '', + $actual, + "Markup wasn't generated correctly for hooked block with attribute set by filter." + ); + } + + /** + * @ticket 59572 + * @ticket 60126 + * @ticket 60506 + */ + public function test_insert_hooked_blocks_filter_can_wrap_block() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + 'attrs' => array( + 'layout' => array( + 'type' => 'constrained', + ), + ), + 'innerContent' => array(), + ); + + $filter = function ( $parsed_hooked_block ) { + if ( self::HOOKED_BLOCK_TYPE !== $parsed_hooked_block['blockName'] ) { + return $parsed_hooked_block; + } + + // Wrap the block in a Group block. + return array( + 'blockName' => 'core/group', + 'attrs' => array(), + 'innerBlocks' => array( $parsed_hooked_block ), + 'innerContent' => array( + '
      ', + null, + '
      ', + ), + ); + }; + add_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter, 10, 3 ); + $actual = insert_hooked_blocks( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + remove_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter ); + + $this->assertSame( + '
      ', + $actual, + "Markup wasn't generated correctly for hooked block wrapped in Group block by filter." + ); + } + + /** + * @ticket 60580 + * + */ + public function test_insert_hooked_blocks_filter_can_suppress_hooked_block() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + 'attrs' => array( + 'layout' => array( + 'type' => 'flex', + ), + ), + 'innerContent' => array(), + ); + + $filter = function ( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) { + // Is the hooked block adjacent to the anchor block? + if ( 'before' !== $relative_position && 'after' !== $relative_position ) { + return $parsed_hooked_block; + } + + if ( + isset( $parsed_anchor_block['attrs']['layout']['type'] ) && + 'flex' === $parsed_anchor_block['attrs']['layout']['type'] + ) { + return null; + } + + return $parsed_hooked_block; + }; + add_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter, 10, 4 ); + $actual = insert_hooked_blocks( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + remove_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter ); + + $this->assertSame( '', $actual, "No markup should've been generated for hooked block suppressed by filter." ); + } +} diff --git a/tests/phpunit/tests/blocks/insertHookedBlocksAndSetIgnoredHookedBlocksMetadata.php b/tests/phpunit/tests/blocks/insertHookedBlocksAndSetIgnoredHookedBlocksMetadata.php new file mode 100644 index 0000000000000..ee3d8eb469d2f --- /dev/null +++ b/tests/phpunit/tests/blocks/insertHookedBlocksAndSetIgnoredHookedBlocksMetadata.php @@ -0,0 +1,247 @@ + array( + 'after' => array( self::HOOKED_BLOCK_TYPE ), + 'before' => array( self::OTHER_HOOKED_BLOCK_TYPE ), + ), + ); + + /** + * @ticket 59574 + */ + private static function create_block_template_object() { + $template = new WP_Block_Template(); + $template->type = 'wp_template'; + $template->theme = 'test-theme'; + $template->slug = 'single'; + $template->id = $template->theme . '//' . $template->slug; + $template->title = 'Single'; + $template->content = ''; + $template->description = 'Description of my template'; + + return $template; + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_returns_correct_markup_and_sets_metadata() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + ); + + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + $this->assertSame( + '', + $actual, + "Markup for hooked block wasn't generated correctly." + ); + $this->assertSame( + array( 'tests/hooked-block' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], + "Block wasn't added to ignoredHookedBlocks metadata." + ); + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_if_block_is_ignored() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array( + 'metadata' => array( + 'ignoredHookedBlocks' => array( self::HOOKED_BLOCK_TYPE ), + ), + ), + ); + + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + $this->assertSame( + '', + $actual, + "No markup should've been generated for ignored hooked block." + ); + $this->assertSame( + array( 'tests/hooked-block' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], + "ignoredHookedBlocks metadata shouldn't have been modified." + ); + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_if_other_block_is_ignored() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array( + 'metadata' => array( + 'ignoredHookedBlocks' => array( 'tests/other-ignored-block' ), + ), + ), + ); + + $hooked_blocks = array( + 'tests/anchor-block' => array( + 'after' => array( 'tests/hooked-block' ), + ), + ); + + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, array() ); + $this->assertSame( + '', + $actual, + "Markup for newly hooked block should've been generated." + ); + $this->assertSame( + array( 'tests/other-ignored-block', 'tests/hooked-block' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] + ); + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_filter_can_suppress_hooked_block() { + $anchor_block = array( + 'blockName' => self::ANCHOR_BLOCK_TYPE, + 'attrs' => array( + 'layout' => array( + 'type' => 'flex', + ), + ), + 'innerContent' => array(), + ); + + $filter = function ( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) { + // Is the hooked block adjacent to the anchor block? + if ( 'before' !== $relative_position && 'after' !== $relative_position ) { + return $parsed_hooked_block; + } + + if ( + isset( $parsed_anchor_block['attrs']['layout']['type'] ) && + 'flex' === $parsed_anchor_block['attrs']['layout']['type'] + ) { + return null; + } + + return $parsed_hooked_block; + }; + add_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter, 10, 4 ); + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', self::HOOKED_BLOCKS, array() ); + remove_filter( 'hooked_block_' . self::HOOKED_BLOCK_TYPE, $filter ); + + $this->assertSame( '', $actual, "No markup should've been generated for hooked block suppressed by filter." ); + $this->assertSame( + array(), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], + "No block should've been added to ignoredHookedBlocks metadata." + ); + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_added_by_context_aware_filter() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array(), + ); + + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type, $context ) { + if ( + ! $context instanceof WP_Block_Template || + ! property_exists( $context, 'slug' ) || + 'single' !== $context->slug + ) { + return $hooked_block_types; + } + + if ( 'tests/anchor-block' === $anchor_block_type && 'after' === $relative_position ) { + $hooked_block_types[] = 'tests/hooked-block-added-by-filter'; + } + + return $hooked_block_types; + }; + + $template = self::create_block_template_object(); + + add_filter( 'hooked_block_types', $filter, 10, 4 ); + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', array(), $template ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( + '', + $actual, + "Markup for hooked block added by filter wasn't generated correctly." + ); + $this->assertSame( + array( 'tests/hooked-block-added-by-filter' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], + "Block added by filter wasn't added to ignoredHookedBlocks metadata." + ); + } + + /** + * @ticket 59574 + */ + public function test_insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata_for_block_suppressed_by_filter() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array(), + ); + + $hooked_blocks = array( + 'tests/anchor-block' => array( + 'after' => array( 'tests/hooked-block', 'tests/hooked-block-suppressed-by-filter' ), + ), + ); + + $filter = function ( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) { + if ( + 'tests/hooked-block-suppressed-by-filter' === $hooked_block_type && + 'after' === $relative_position && + 'tests/anchor-block' === $parsed_anchor_block['blockName'] + ) { + return null; + } + + return $parsed_hooked_block; + }; + + add_filter( 'hooked_block', $filter, 10, 4 ); + $actual = insert_hooked_blocks_and_set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, null ); + remove_filter( 'hooked_block', $filter ); + + $this->assertSame( + '', + $actual, + "Markup for hooked block wasn't generated correctly." + ); + $this->assertSame( + array( 'tests/hooked-block' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'], + "ignoredHookedBlocks metadata wasn't set correctly." + ); + } +} diff --git a/tests/phpunit/tests/blocks/register.php b/tests/phpunit/tests/blocks/register.php index 18463671d4a40..d2a836ba85e68 100644 --- a/tests/phpunit/tests/blocks/register.php +++ b/tests/phpunit/tests/blocks/register.php @@ -1,21 +1,25 @@ original_wp_scripts = $wp_scripts; + $this->original_wp_styles = $wp_styles; + $wp_scripts = null; + $wp_styles = null; + wp_scripts(); + wp_styles(); + } + /** * Tear down after each test. * * @since 5.0.0 */ public function tear_down() { - $registry = WP_Block_Type_Registry::get_instance(); + // Removes test block types registered by test cases. + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + foreach ( $block_types as $block_type ) { + $block_name = $block_type->name; + if ( str_starts_with( $block_name, 'tests/' ) ) { + unregister_block_type( $block_name ); + } + } - foreach ( array( 'core/test-static', 'core/test-dynamic', 'tests/notice' ) as $block_name ) { - if ( $registry->is_registered( $block_name ) ) { - $registry->unregister( $block_name ); + foreach ( wp_scripts()->registered as $script_handle => $script ) { + if ( str_starts_with( $script_handle, 'tests-' ) ) { + wp_deregister_script( $script_handle ); } } + global $wp_scripts, $wp_styles; + $wp_scripts = $this->original_wp_scripts; + $wp_styles = $this->original_wp_styles; + parent::tear_down(); } @@ -82,7 +112,7 @@ public function filter_set_locale_to_polish() { * @ticket 45109 */ public function test_register_affects_main_registry() { - $name = 'core/test-static'; + $name = 'tests/static'; $settings = array( 'icon' => 'text', ); @@ -97,7 +127,7 @@ public function test_register_affects_main_registry() { * @ticket 45109 */ public function test_unregister_affects_main_registry() { - $name = 'core/test-static'; + $name = 'tests/static'; $settings = array( 'icon' => 'text', ); @@ -122,37 +152,65 @@ public function test_does_not_remove_block_asset_path_prefix() { * @ticket 50263 */ public function test_removes_block_asset_path_prefix() { + $result = remove_block_asset_path_prefix( 'file:block.js' ); + + $this->assertSame( 'block.js', $result ); + } + + /** + * @ticket 54797 + */ + public function test_removes_block_asset_path_prefix_and_current_directory() { $result = remove_block_asset_path_prefix( 'file:./block.js' ); - $this->assertSame( './block.js', $result ); + $this->assertSame( 'block.js', $result ); } /** * @ticket 50263 + * @ticket 60233 */ public function test_generate_block_asset_handle() { - $block_name = 'unit-tests/my-block'; + $block_name = 'tests/my-block'; $this->assertSame( - 'unit-tests-my-block-editor-script', + 'tests-my-block-editor-script', generate_block_asset_handle( $block_name, 'editorScript' ) ); $this->assertSame( - 'unit-tests-my-block-script', - generate_block_asset_handle( $block_name, 'script' ) + 'tests-my-block-script', + generate_block_asset_handle( $block_name, 'script', 0 ) ); $this->assertSame( - 'unit-tests-my-block-view-script', - generate_block_asset_handle( $block_name, 'viewScript' ) + 'tests-my-block-view-script-100', + generate_block_asset_handle( $block_name, 'viewScript', 99 ) ); $this->assertSame( - 'unit-tests-my-block-editor-style', - generate_block_asset_handle( $block_name, 'editorStyle' ) + 'tests-my-block-view-script-module', + generate_block_asset_handle( $block_name, 'viewScriptModule' ) ); $this->assertSame( - 'unit-tests-my-block-style', + 'tests-my-block-view-script-module-2', + generate_block_asset_handle( $block_name, 'viewScriptModule', 1 ) + ); + $this->assertSame( + 'tests-my-block-view-script-module-100', + generate_block_asset_handle( $block_name, 'viewScriptModule', 99 ) + ); + $this->assertSame( + 'tests-my-block-editor-style-2', + generate_block_asset_handle( $block_name, 'editorStyle', 1 ) + ); + $this->assertSame( + 'tests-my-block-style', generate_block_asset_handle( $block_name, 'style' ) ); + // @ticket 59673 + $this->assertSame( + 'tests-my-block-view-style', + generate_block_asset_handle( $block_name, 'viewStyle' ), + 'asset handle for viewStyle is not generated correctly' + ); } /** @@ -167,15 +225,15 @@ public function test_generate_block_asset_handle_core_block() { ); $this->assertSame( 'wp-block-paragraph', - generate_block_asset_handle( $block_name, 'script' ) + generate_block_asset_handle( $block_name, 'script', 0 ) ); $this->assertSame( - 'wp-block-paragraph-view', - generate_block_asset_handle( $block_name, 'viewScript' ) + 'wp-block-paragraph-view-100', + generate_block_asset_handle( $block_name, 'viewScript', 99 ) ); $this->assertSame( - 'wp-block-paragraph-editor', - generate_block_asset_handle( $block_name, 'editorStyle' ) + 'wp-block-paragraph-editor-2', + generate_block_asset_handle( $block_name, 'editorStyle', 1 ) ); $this->assertSame( 'wp-block-paragraph', @@ -183,6 +241,52 @@ public function test_generate_block_asset_handle_core_block() { ); } + /** + * @ticket 60233 + */ + public function test_generate_block_asset_handle_core_block_module() { + $block_name = 'core/paragraph'; + + $this->assertSame( + 'wp-block-paragraph-editor-script-module', + generate_block_asset_handle( $block_name, 'editorScriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-editor-script-module-2', + generate_block_asset_handle( $block_name, 'editorScriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-editor-script-module-100', + generate_block_asset_handle( $block_name, 'editorScriptModule', 99 ) + ); + + $this->assertSame( + 'wp-block-paragraph-view-script-module', + generate_block_asset_handle( $block_name, 'viewScriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-view-script-module-2', + generate_block_asset_handle( $block_name, 'viewScriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-view-script-module-100', + generate_block_asset_handle( $block_name, 'viewScriptModule', 99 ) + ); + + $this->assertSame( + 'wp-block-paragraph-script-module', + generate_block_asset_handle( $block_name, 'scriptModule' ) + ); + $this->assertSame( + 'wp-block-paragraph-script-module-2', + generate_block_asset_handle( $block_name, 'scriptModule', 1 ) + ); + $this->assertSame( + 'wp-block-paragraph-script-module-100', + generate_block_asset_handle( $block_name, 'scriptModule', 99 ) + ); + } + /** * @ticket 50263 */ @@ -195,26 +299,235 @@ public function test_field_not_found_register_block_script_handle() { /** * @ticket 50263 */ - public function test_empty_value_register_block_script_handle() { + public function test_empty_string_value_do_not_register_block_script_handle() { $metadata = array( 'script' => '' ); $result = register_block_script_handle( $metadata, 'script' ); $this->assertFalse( $result ); } + public function test_empty_array_value_do_not_register_block_script_handle() { + $metadata = array( 'script' => array() ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertFalse( $result ); + } + + public function test_wrong_array_index_do_not_register_block_script_handle() { + $metadata = array( 'script' => array( 'test-script-handle' ) ); + $result = register_block_script_handle( $metadata, 'script', 1 ); + + $this->assertFalse( $result ); + } + /** - * @expectedIncorrectUsage register_block_script_handle - * @ticket 50263 + * @ticket 60233 + */ + public function test_field_not_found_register_block_script_module_id() { + $result = register_block_script_module_id( array(), 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 */ - public function test_missing_asset_file_register_block_script_handle() { + public function test_empty_string_value_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => '' ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_empty_array_value_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => array() ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_wrong_array_index_do_not_register_block_script_module_id() { + $metadata = array( 'viewScriptModule' => array( 'test-module_id' ) ); + $result = register_block_script_module_id( $metadata, 'script', 1 ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 60233 + */ + public function test_missing_asset_file_register_block_script_module_id() { $metadata = array( - 'file' => __FILE__, - 'name' => 'unit-tests/test-block', - 'script' => 'file:./blocks/notice/missing-asset.js', + 'file' => __FILE__, + 'name' => 'tests/test-block', + 'viewScriptModule' => 'file:./blocks/notice/missing-asset.js', ); - $result = register_block_script_handle( $metadata, 'script' ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); - $this->assertFalse( $result ); + $this->assertSame( 'tests-test-block-view-script-module', $result ); + } + + /** + * @ticket 60233 + */ + public function test_handle_passed_register_block_script_module_id() { + $metadata = array( + 'viewScriptModule' => 'test-script-module-id', + ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertSame( 'test-script-module-id', $result ); + } + + /** + * @ticket 60233 + */ + public function test_handles_passed_register_block_script_module_ids() { + $metadata = array( + 'viewScriptModule' => array( 'test-id', 'test-id-other' ), + ); + + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + $this->assertSame( 'test-id', $result ); + + $result = register_block_script_module_id( $metadata, 'viewScriptModule', 1 ); + $this->assertSame( 'test-id-other', $result ); + } + + /** + * @ticket 60233 + */ + public function test_success_register_block_script_module_id() { + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/test-block', + 'viewScriptModule' => 'file:./block.js', + ); + $result = register_block_script_module_id( $metadata, 'viewScriptModule' ); + + $this->assertSame( 'tests-test-block-view-script-module', $result ); + + // Test the behavior directly within the unit test. + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['viewScriptModule'] ) ), + trailingslashit( wp_normalize_path( get_template_directory() ) ) + ) === 0 + ); + + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['viewScriptModule'] ) ), + trailingslashit( wp_normalize_path( get_stylesheet_directory() ) ) + ) === 0 + ); + } + + /** + * Tests that blocks with supports.interactivity have the + * `data-wp-router-options` directive. + * + * @ticket 64122 + * + * @covers ::register_block_script_module_id + */ + public function test_register_block_script_module_id_with_interactivity_true() { + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'viewScriptModule' => 'file:./block.js', + ); + + $interactivity_true = array_merge( + $metadata, + array( + 'name' => 'tests/interactivity-true', + 'supports' => array( 'interactivity' => true ), + ) + ); + $interactive_and_client_navigation = array_merge( + $metadata, + array( + 'name' => 'tests/interactive-and-client-navigation', + 'supports' => array( + 'interactivity' => array( + 'interactive' => true, + 'clientNavigation' => true, + ), + ), + ) + ); + $interactive_and_not_client_navigation = array_merge( + $metadata, + array( + 'name' => 'tests/interactive-and-not-client-navigation', + 'supports' => array( + 'interactivity' => array( + 'interactive' => true, + 'clientNavigation' => false, + ), + ), + ) + ); + $not_interactive_and_client_navigation = array_merge( + $metadata, + array( + 'name' => 'tests/not-interactive-and-client-navigation', + 'supports' => array( + 'interactivity' => array( + 'interactive' => false, + 'clientNavigation' => true, + ), + ), + ) + ); + $no_interactivity = array_merge( + $metadata, + array( + 'name' => 'tests/no-interactivity', + 'supports' => array(), + ) + ); + + $interactivity_true_module_id = register_block_script_module_id( $interactivity_true, 'viewScriptModule' ); + $interactive_and_client_navigation_module_id = register_block_script_module_id( $interactive_and_client_navigation, 'viewScriptModule' ); + $interactive_and_not_client_navigation_module_id = register_block_script_module_id( $interactive_and_not_client_navigation, 'viewScriptModule' ); + $not_interactive_and_client_navigation_module_id = register_block_script_module_id( $not_interactive_and_client_navigation, 'viewScriptModule' ); + $no_interactivity_module_id = register_block_script_module_id( $no_interactivity, 'viewScriptModule' ); + wp_enqueue_script_module( $interactivity_true_module_id ); + wp_enqueue_script_module( $interactive_and_client_navigation_module_id ); + wp_enqueue_script_module( $interactive_and_not_client_navigation_module_id ); + wp_enqueue_script_module( $not_interactive_and_client_navigation_module_id ); + wp_enqueue_script_module( $no_interactivity_module_id ); + + $output = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + + $p = new WP_HTML_Tag_Processor( $output ); + + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' ); + $this->assertSame( 'tests-interactivity-true-view-script-module-js-module', $p->get_attribute( 'id' ) ); + $this->assertSame( '{"loadOnClientNavigation":true}', $p->get_attribute( 'data-wp-router-options' ) ); + + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' ); + $this->assertSame( 'tests-interactive-and-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) ); + $this->assertSame( '{"loadOnClientNavigation":true}', $p->get_attribute( 'data-wp-router-options' ) ); + + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' ); + $this->assertSame( 'tests-interactive-and-not-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) ); + $this->assertNull( $p->get_attribute( 'data-wp-router-options' ) ); + + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' ); + $this->assertSame( 'tests-not-interactive-and-client-navigation-view-script-module-js-module', $p->get_attribute( 'id' ) ); + $this->assertNull( $p->get_attribute( 'data-wp-router-options' ) ); + + $this->assertTrue( $p->next_tag( array( 'tag_name' => 'SCRIPT' ) ), 'Expected there to be another SCRIPT.' ); + $this->assertSame( 'tests-no-interactivity-view-script-module-js-module', $p->get_attribute( 'id' ) ); + $this->assertNull( $p->get_attribute( 'data-wp-router-options' ) ); } /** @@ -222,11 +535,38 @@ public function test_missing_asset_file_register_block_script_handle() { */ public function test_handle_passed_register_block_script_handle() { $metadata = array( - 'editorScript' => 'test-script-handle', + 'script' => 'test-script-handle', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertSame( 'test-script-handle', $result ); + } + + public function test_handles_passed_register_block_script_handles() { + $metadata = array( + 'script' => array( 'test-script-handle', 'test-script-handle-other' ), ); - $result = register_block_script_handle( $metadata, 'editorScript' ); + $result = register_block_script_handle( $metadata, 'script' ); $this->assertSame( 'test-script-handle', $result ); + + $result = register_block_script_handle( $metadata, 'script', 1 ); + $this->assertSame( 'test-script-handle-other', $result ); + } + + /** + * @ticket 50263 + * @ticket 60460 + */ + public function test_missing_asset_file_register_block_script_handle_with_default_settings() { + $metadata = array( + 'file' => __FILE__, + 'name' => 'tests/test-block', + 'script' => 'file:./blocks/notice/missing-asset.js', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertSame( 'tests-test-block-script', $result ); } /** @@ -235,12 +575,89 @@ public function test_handle_passed_register_block_script_handle() { public function test_success_register_block_script_handle() { $metadata = array( 'file' => DIR_TESTDATA . '/blocks/notice/block.json', - 'name' => 'unit-tests/test-block', + 'name' => 'tests/test-block', 'script' => 'file:./block.js', ); $result = register_block_script_handle( $metadata, 'script' ); - $this->assertSame( 'unit-tests-test-block-script', $result ); + $this->assertSame( 'tests-test-block-script', $result ); + + // Test the behavior directly within the unit test. + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['script'] ) ), + trailingslashit( wp_normalize_path( get_template_directory() ) ) + ) === 0 + ); + + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['script'] ) ), + trailingslashit( wp_normalize_path( get_stylesheet_directory() ) ) + ) === 0 + ); + } + + /** + * @ticket 60485 + */ + public function test_success_register_block_script_handle_with_custom_handle_name() { + $custom_script_handle = 'tests-my-shared-script'; + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/sample-block', + 'script' => 'file:./shared-script.js', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertSame( $custom_script_handle, $result ); + $this->assertStringEndsWith( + 'shared-script.js', + wp_scripts()->registered[ $custom_script_handle ]->src + ); + } + + /** + * @ticket 60485 + */ + public function test_reuse_registered_block_script_handle_with_custom_handle_name() { + $custom_script_handle = 'tests-my-shared-script'; + $custom_script_src = 'https://example.com/foo.js'; + wp_register_script( $custom_script_handle, $custom_script_src ); + + $this->assertTrue( + wp_script_is( $custom_script_handle, 'registered' ) + ); + + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/sample-block', + 'script' => 'file:./shared-script.js', + ); + $result = register_block_script_handle( $metadata, 'script' ); + + $this->assertSame( $custom_script_handle, $result ); + $this->assertSame( + $custom_script_src, + wp_scripts()->registered[ $custom_script_handle ]->src + ); + } + + /** + * @ticket 55513 + */ + public function test_success_register_block_script_handle_in_theme() { + switch_theme( 'block-theme' ); + + $metadata = array( + 'file' => wp_normalize_path( get_theme_file_path( 'blocks/example-block/block.json' ) ), + 'name' => 'block-theme/example-block', + 'viewScript' => 'file:./view.js', + ); + $result = register_block_script_handle( $metadata, 'viewScript' ); + + $expected_script_handle = 'block-theme-example-block-view-script'; + $this->assertSame( $expected_script_handle, $result ); } /** @@ -255,18 +672,103 @@ public function test_field_not_found_register_block_style_handle() { /** * @ticket 50263 */ - public function test_empty_value_found_register_block_style_handle() { + public function test_empty_string_value_do_not_register_block_style_handle() { $metadata = array( 'style' => '' ); $result = register_block_style_handle( $metadata, 'style' ); $this->assertFalse( $result ); } + public function test_empty_array_value_do_not_register_block_style_handle() { + $metadata = array( 'style' => array() ); + $result = register_block_style_handle( $metadata, 'style' ); + + $this->assertFalse( $result ); + } + + public function test_wrong_array_index_do_not_register_block_style_handle() { + $metadata = array( 'style' => array( 'test-style-handle' ) ); + $result = register_block_style_handle( $metadata, 'style', 1 ); + + $this->assertFalse( $result ); + } + + /** + * @ticket 58605 + * + * @dataProvider data_register_block_style_handle_uses_correct_core_stylesheet + * + * @param string $block_json_path Path to the `block.json` file, relative to ABSPATH. + * @param string $style_field Either 'style' or 'editorStyle'. + * @param string|bool $expected_path Expected path of registered stylesheet, relative to ABSPATH. + */ + public function test_register_block_style_handle_uses_correct_core_stylesheet( $block_json_path, $style_field, $expected_path ) { + $metadata_file = ABSPATH . $block_json_path; + $metadata = wp_json_file_decode( $metadata_file, array( 'associative' => true ) ); + + $block_name = str_replace( 'core/', '', $metadata['name'] ); + + // Normalize metadata similar to `register_block_type_from_metadata()`. + $metadata['file'] = wp_normalize_path( realpath( $metadata_file ) ); + if ( ! isset( $metadata['style'] ) ) { + $metadata['style'] = "wp-block-$block_name"; + } + if ( ! isset( $metadata['editorStyle'] ) ) { + $metadata['editorStyle'] = "wp-block-{$block_name}-editor"; + } + + // Ensure block assets are separately registered. + add_filter( 'should_load_separate_core_block_assets', '__return_true' ); + + /* + * Account for minified asset path and ensure the file exists. + * This may not be the case in the testing environment since it requires the build process to place them. + */ + if ( is_string( $expected_path ) ) { + $expected_path = str_replace( '.css', wp_scripts_get_suffix() . '.css', $expected_path ); + self::touch( ABSPATH . $expected_path ); + } + + $result = register_block_style_handle( $metadata, $style_field ); + $this->assertSame( $metadata[ $style_field ], $result, 'Core block registration failed' ); + if ( $expected_path ) { + $this->assertStringEndsWith( $expected_path, wp_styles()->registered[ $result ]->src, 'Core block stylesheet path incorrect' ); + } else { + $this->assertFalse( wp_styles()->registered[ $result ]->src, 'Core block stylesheet src should be false' ); + } + } + + public function data_register_block_style_handle_uses_correct_core_stylesheet() { + return array( + 'block with style' => array( + WPINC . '/blocks/archives/block.json', + 'style', + WPINC . '/blocks/archives/style.css', + ), + 'block with editor style' => array( + WPINC . '/blocks/archives/block.json', + 'editorStyle', + WPINC . '/blocks/archives/editor.css', + ), + 'block without style' => array( + WPINC . '/blocks/widget-group/block.json', + 'style', + false, + ), + 'block without editor style' => array( + WPINC . '/blocks/widget-group/block.json', + 'editorStyle', + false, + ), + ); + } + /** * @ticket 50263 */ public function test_handle_passed_register_block_style_handle() { $metadata = array( + 'name' => 'test-block', 'style' => 'test-style-handle', ); $result = register_block_style_handle( $metadata, 'style' ); @@ -274,26 +776,171 @@ public function test_handle_passed_register_block_style_handle() { $this->assertSame( 'test-style-handle', $result ); } + public function test_handles_passed_register_block_style_handles() { + $metadata = array( + 'name' => 'test-block', + 'style' => array( 'test-style-handle', 'test-style-handle-2' ), + ); + + $result = register_block_style_handle( $metadata, 'style' ); + $this->assertSame( 'test-style-handle', $result ); + + $result = register_block_style_handle( $metadata, 'style', 1 ); + $this->assertSame( 'test-style-handle-2', $result, 1 ); + } + /** * @ticket 50263 * @ticket 50328 */ public function test_success_register_block_style_handle() { $metadata = array( - 'file' => DIR_TESTDATA . '/blocks/notice/block.json', - 'name' => 'unit-tests/test-block', - 'style' => 'file:./block.css', + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/test-block', + 'style' => 'file:./block.css', + 'viewStyle' => 'file:./block-view.css', ); $result = register_block_style_handle( $metadata, 'style' ); - $this->assertSame( 'unit-tests-test-block-style', $result ); - $this->assertSame( 'replace', wp_styles()->get_data( 'unit-tests-test-block-style', 'rtl' ) ); + $this->assertSame( 'tests-test-block-style', $result ); + $this->assertFalse( wp_styles()->get_data( 'tests-test-block-style', 'rtl' ) ); // @ticket 50328 $this->assertSame( wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block.css' ) ), - wp_normalize_path( wp_styles()->get_data( 'unit-tests-test-block-style', 'path' ) ) + wp_normalize_path( wp_styles()->get_data( 'tests-test-block-style', 'path' ) ) + ); + + // Test viewStyle property + $result = register_block_style_handle( $metadata, 'viewStyle' ); + $this->assertSame( 'tests-test-block-view-style', $result ); + + // @ticket 59673 + $this->assertSame( + wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block-view.css' ) ), + wp_normalize_path( wp_styles()->get_data( 'tests-test-block-view-style', 'path' ) ), + 'viewStyle asset path is not correct' + ); + + // Test the behavior directly within the unit test. + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['style'] ) ), + trailingslashit( wp_normalize_path( get_template_directory() ) ) + ) === 0 + ); + + $this->assertFalse( + strpos( + wp_normalize_path( realpath( dirname( $metadata['file'] ) . '/' . $metadata['style'] ) ), + trailingslashit( wp_normalize_path( get_stylesheet_directory() ) ) + ) === 0 + ); + } + + /** + * Tests that register_block_style_handle() loads RTL stylesheets when an RTL locale is set. + * + * @ticket 56325 + * @ticket 56797 + * + * @covers ::register_block_style_handle + */ + public function test_register_block_style_handle_should_load_rtl_stylesheets_for_rtl_text_direction() { + global $wp_locale; + + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/test-block-rtl', + 'style' => 'file:./block.css', + ); + + $orig_text_dir = $wp_locale->text_direction; + $wp_locale->text_direction = 'rtl'; + + $handle = register_block_style_handle( $metadata, 'style' ); + $extra_rtl = wp_styles()->get_data( 'tests-test-block-rtl-style', 'rtl' ); + $extra_suffix = wp_styles()->get_data( 'tests-test-block-rtl-style', 'suffix' ); + $extra_path = wp_normalize_path( wp_styles()->get_data( 'tests-test-block-rtl-style', 'path' ) ); + + $wp_locale->text_direction = $orig_text_dir; + + $this->assertSame( + 'tests-test-block-rtl-style', + $handle, + 'The handle did not match the expected handle.' + ); + + $this->assertSame( + 'replace', + $extra_rtl, + 'The extra "rtl" data was not "replace".' + ); + + $this->assertSame( + '', + $extra_suffix, + 'The extra "suffix" data was not an empty string.' + ); + + $this->assertSame( + wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block-rtl.css' ) ), + $extra_path, + 'The "path" did not match the expected path.' + ); + } + + /** + * @ticket 56664 + */ + public function test_register_nonexistent_stylesheet() { + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/test-block-nonexistent-stylesheet', + 'style' => 'file:./nonexistent.css', ); + register_block_style_handle( $metadata, 'style' ); + + global $wp_styles; + $this->assertFalse( $wp_styles->registered['tests-test-block-nonexistent-stylesheet-style']->src ); + } + + /** + * @ticket 55513 + */ + public function test_success_register_block_style_handle_in_theme() { + switch_theme( 'block-theme' ); + + $metadata = array( + 'file' => wp_normalize_path( get_theme_file_path( 'blocks/example-block/block.json' ) ), + 'name' => 'block-theme/example-block', + 'editorStyle' => 'file:./editor-style.css', + ); + $result = register_block_style_handle( $metadata, 'editorStyle' ); + + $expected_style_handle = 'block-theme-example-block-editor-style'; + $this->assertSame( $expected_style_handle, $result ); + $this->assertFalse( wp_styles()->get_data( $expected_style_handle, 'rtl' ) ); + } + + /** + * @ticket 58528 + * + * @covers ::register_block_style_handle + */ + public function test_success_register_block_style_handle_exists() { + $expected_style_handle = 'block-theme-example-block-editor-style'; + wp_register_style( $expected_style_handle, false ); + switch_theme( 'block-theme' ); + + $metadata = array( + 'file' => wp_normalize_path( get_theme_file_path( 'blocks/example-block/block.json' ) ), + 'name' => 'block-theme/example-block', + 'editorStyle' => 'file:./editor-style.css', + ); + $result = register_block_style_handle( $metadata, 'editorStyle' ); + + $this->assertSame( $expected_style_handle, $result ); } /** @@ -320,34 +967,201 @@ public function test_metadata_not_found_in_the_current_directory() { $this->assertFalse( $result ); } + /** + * Tests registering a block using arguments instead of a block.json file. + * + * @ticket 56865 + * + * @covers ::register_block_type_from_metadata + */ + public function test_register_block_type_from_metadata_with_arguments() { + $result = register_block_type_from_metadata( + '', + array( + 'api_version' => 2, + 'name' => 'tests/notice-from-array', + 'title' => 'Notice from array', + 'category' => 'common', + 'icon' => 'star', + 'description' => 'Shows warning, error or success notices… (registered from an array)', + 'keywords' => array( + 'alert', + 'message', + ), + 'textdomain' => 'notice-from-array', + ) + ); + + $this->assertInstanceOf( 'WP_Block_Type', $result, 'The block was not registered' ); + $this->assertSame( 2, $result->api_version, 'The API version is incorrect' ); + $this->assertSame( 'tests/notice-from-array', $result->name, 'The block name is incorrect' ); + $this->assertSame( 'Notice from array', $result->title, 'The block title is incorrect' ); + $this->assertSame( 'common', $result->category, 'The block category is incorrect' ); + $this->assertSame( 'star', $result->icon, 'The block icon is incorrect' ); + $this->assertSame( + 'Shows warning, error or success notices… (registered from an array)', + $result->description, + 'The block description is incorrect' + ); + $this->assertSameSets( array( 'alert', 'message' ), $result->keywords, 'The block keywords are incorrect' ); + } + + /** + * Tests that defined $args can properly override the block.json file. + * + * @ticket 56865 + * + * @covers ::register_block_type_from_metadata + */ + public function test_block_registers_with_args_override() { + $result = register_block_type_from_metadata( + DIR_TESTDATA . '/blocks/notice', + array( + 'name' => 'tests/notice-with-overrides', + 'title' => 'Overridden title', + 'style' => array( 'tests-notice-style-overridden' ), + ) + ); + + $this->assertInstanceOf( 'WP_Block_Type', $result, 'The block was not registered' ); + $this->assertSame( 2, $result->api_version, 'The API version is incorrect' ); + $this->assertSame( 'tests/notice-with-overrides', $result->name, 'The block name was not overridden' ); + $this->assertSame( 'Overridden title', $result->title, 'The block title was not overridden' ); + $this->assertSameSets( + array( 'tests-notice-editor-script' ), + $result->editor_script_handles, + 'The block editor script is incorrect' + ); + $this->assertSameSets( + array( 'tests-notice-style-overridden' ), + $result->style_handles, + 'The block style was not overridden' + ); + $this->assertIsCallable( $result->render_callback ); + } + + /** + * Tests that when the `name` is missing, `register_block_type_from_metadata()` + * will return `false`. + * + * @ticket 56865 + * + * @covers ::register_block_type_from_metadata + * + * @dataProvider data_register_block_registers_with_args_override_returns_false_when_name_is_missing + * + * @param string $file The metadata file. + * @param array $args Array of block type arguments. + */ + public function test_block_registers_with_args_override_returns_false_when_name_is_missing( $file, $args ) { + $this->assertFalse( register_block_type_from_metadata( $file, $args ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_register_block_registers_with_args_override_returns_false_when_name_is_missing() { + return array( + 'no block.json file and no name argument' => array( + 'file' => '', // No block.json file. + 'args' => array( + 'title' => 'Overridden title', + 'style' => array( 'tests-notice-style-overridden' ), + ), + ), + 'existing file and args not an array' => array( + // A file that exists but is empty. This will bypass the file_exists() check. + 'file' => DIR_TESTDATA . '/blocks/notice/block.js', + 'args' => false, + ), + 'existing file and args[name] missing' => array( + // A file that exists but is empty. This will bypass the file_exists() check. + 'file' => DIR_TESTDATA . '/blocks/notice/block.js', + 'args' => array( + 'title' => 'Overridden title', + 'style' => array( 'tests-notice-style-overridden' ), + ), + ), + ); + } + + /** + * Tests registering a block with variations from a PHP file. + * + * @ticket 61280 + * + * @covers ::register_block_type_from_metadata + */ + public function test_register_block_type_from_metadata_with_variations_php_file() { + $filter_metadata_registration = static function ( $metadata ) { + $metadata['variations'] = 'file:./variations.php'; + return $metadata; + }; + + add_filter( 'block_type_metadata', $filter_metadata_registration, 10, 2 ); + $result = register_block_type_from_metadata( + DIR_TESTDATA . '/blocks/notice' + ); + remove_filter( 'block_type_metadata', $filter_metadata_registration ); + + $this->assertInstanceOf( 'WP_Block_Type', $result, 'The block was not registered' ); + + $this->assertIsCallable( $result->variation_callback, 'The variation callback hasn\'t been set' ); + $expected_variations = require DIR_TESTDATA . '/blocks/notice/variations.php'; + $this->assertSame( + $expected_variations, + call_user_func( $result->variation_callback ), + 'The variation callback hasn\'t been set correctly' + ); + $this->assertSame( $expected_variations, $result->variations, 'The block variations are incorrect' ); + } + /** * Tests that the function returns the registered block when the `block.json` * is found in the fixtures directory. * * @ticket 50263 * @ticket 50328 + * @ticket 57585 + * @ticket 59797 + * @ticket 60233 */ public function test_block_registers_with_metadata_fixture() { $result = register_block_type_from_metadata( DIR_TESTDATA . '/blocks/notice' ); + // Register the styles not included in the metadata above. + $metadata = array( + 'file' => DIR_TESTDATA . '/blocks/notice/block.json', + 'name' => 'tests/notice', + 'style' => 'file:./block.css', + 'viewStyle' => 'file:./block-view.css', + ); + $this->assertSame( 'tests-notice-style', register_block_style_handle( $metadata, 'style' ), 'Style handle is expected to be tests-notice-style' ); + $this->assertSame( 'tests-notice-view-style', register_block_style_handle( $metadata, 'viewStyle' ), 'View style handle is expected to be tests-notice-view-style' ); + $this->assertTrue( wp_style_is( 'tests-notice-style', 'registered' ), 'Expected "tests-notice-style" style to be registered.' ); + $this->assertTrue( wp_style_is( 'tests-notice-view-style', 'registered' ), 'Expected "tests-notice-view-style" style to be registered.' ); + $this->assertInstanceOf( 'WP_Block_Type', $result ); $this->assertSame( 2, $result->api_version ); $this->assertSame( 'tests/notice', $result->name ); $this->assertSame( 'Notice', $result->title ); $this->assertSame( 'common', $result->category ); - $this->assertSameSets( array( 'core/group' ), $result->parent ); + $this->assertSameSets( array( 'tests/group' ), $result->parent ); + $this->assertSameSets( array( 'tests/section' ), $result->ancestor ); $this->assertSame( 'star', $result->icon ); $this->assertSame( 'Shows warning, error or success notices…', $result->description ); $this->assertSameSets( array( 'alert', 'message' ), $result->keywords ); $this->assertSame( array( - 'message' => array( - 'type' => 'string', - 'source' => 'html', - 'selector' => '.message', + 'message' => array( + 'type' => 'string', ), + 'lock' => array( 'type' => 'object' ), + 'metadata' => array( 'type' => 'object' ), ), $result->attributes ); @@ -358,6 +1172,23 @@ public function test_block_registers_with_metadata_fixture() { $result->provides_context ); $this->assertSameSets( array( 'groupId' ), $result->uses_context ); + // @ticket 57585 + $this->assertSame( + array( 'root' => '.wp-block-notice' ), + $result->selectors, + 'Block type should contain selectors from metadata.' + ); + // @ticket 59346 + $this->assertSameSets( + array( + 'tests/before' => 'before', + 'tests/after' => 'after', + 'tests/first-child' => 'first_child', + 'tests/last-child' => 'last_child', + ), + $result->block_hooks, + 'Block type should contain block hooks from metadata.' + ); $this->assertSame( array( 'align' => true, @@ -398,17 +1229,52 @@ public function test_block_registers_with_metadata_fixture() { ), $result->example ); - $this->assertSame( 'tests-notice-editor-script', $result->editor_script ); - $this->assertSame( 'tests-notice-script', $result->script ); - $this->assertSame( 'tests-notice-view-script', $result->view_script ); - $this->assertSame( 'tests-notice-editor-style', $result->editor_style ); - $this->assertSame( 'tests-notice-style', $result->style ); + $this->assertSameSets( + array( 'tests-notice-editor-script' ), + $result->editor_script_handles + ); + $this->assertSameSets( + array( 'tests-notice-script' ), + $result->script_handles + ); + $this->assertSameSets( + array( 'tests-notice-view-script', 'tests-notice-view-script-2' ), + $result->view_script_handles + ); + $this->assertSameSets( + array( 'tests-notice-view-script-module', 'tests-notice-view-script-module-2' ), + $result->view_script_module_ids + ); + $this->assertSameSets( + array( 'tests-notice-editor-style' ), + $result->editor_style_handles + ); + $this->assertSameSets( + array( 'tests-notice-style', 'tests-notice-style-2' ), + $result->style_handles + ); + // @ticket 59673 + $this->assertSameSets( + array( 'tests-notice-view-style' ), + $result->view_style_handles, + 'parsed view_style_handles is not correct' + ); // @ticket 50328 $this->assertSame( wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block.css' ) ), - wp_normalize_path( wp_styles()->get_data( 'unit-tests-test-block-style', 'path' ) ) + wp_normalize_path( wp_styles()->get_data( 'tests-notice-style', 'path' ) ) ); + + // @ticket 59673 + $this->assertSame( + wp_normalize_path( realpath( DIR_TESTDATA . '/blocks/notice/block-view.css' ) ), + wp_normalize_path( wp_styles()->get_data( 'tests-notice-view-style', 'path' ) ), + 'viewStyle asset path is not correct' + ); + + // @ticket 53148 + $this->assertIsCallable( $result->render_callback ); } /** @@ -423,6 +1289,134 @@ public function test_block_register_block_type_proxy_for_metadata() { $this->assertSame( 'tests/notice', $result->name ); } + /** + * Tests that an array value for 'editor_script' is correctly set and retrieved. + * + * As 'editor_script' is now a deprecated property, this should also set + * the value for the 'editor_script_handles' property. + * + * @ticket 56707 + * + * @covers ::register_block_type + * @covers WP_Block_Type::__set + * @covers WP_Block_Type::__get + * + * @dataProvider data_register_block_type_accepts_editor_script_array + * + * @param array $editor_script The editor script array to register. + * @param array $expected The expected registered editor script. + */ + public function test_register_block_type_accepts_editor_script_array( $editor_script, $expected ) { + $settings = array( 'editor_script' => $editor_script ); + register_block_type( 'tests/static', $settings ); + + $registry = WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( 'tests/static' ); + $this->assertObjectHasProperty( 'editor_script_handles', $block_type ); + $actual_script = $block_type->editor_script; + $actual_script_handles = $block_type->editor_script_handles; + + $this->assertSame( + $expected, + $actual_script, + 'editor_script was not set to the correct value.' + ); + + $this->assertSame( + (array) $expected, + $actual_script_handles, + 'editor_script_handles was not set to the correct value.' + ); + } + + /** + * Data provider for test_register_block_type_accepts_editor_script_array(). + * + * @return array + */ + public function data_register_block_type_accepts_editor_script_array() { + return array( + 'an empty array' => array( + 'editor_script' => array(), + 'expected' => null, + ), + 'a single item array' => array( + 'editor_script' => array( 'hello' ), + 'expected' => 'hello', + ), + 'a multi-item array' => array( + 'editor_script' => array( 'hello', 'world' ), + 'expected' => array( 'hello', 'world' ), + ), + ); + } + + /** + * Tests that an array value for 'editor_script' containing invalid values + * correctly triggers _doing_it_wrong(), filters the value, and sets the + * property to the result. + * + * As 'editor_script' is now a deprecated property, this should also set + * the value for the 'editor_script_handles' property. + * + * @ticket 56707 + * + * @covers ::register_block_type + * @covers WP_Block_Type::__set + * @covers WP_Block_Type::__get + * + * @dataProvider data_register_block_type_throws_doing_it_wrong + * + * @expectedIncorrectUsage WP_Block_Type::__set + * + * @param array $editor_script The editor script array to register. + * @param array $expected The expected registered editor script. + */ + public function test_register_block_type_throws_doing_it_wrong( $editor_script, $expected ) { + $settings = array( 'editor_script' => $editor_script ); + register_block_type( 'tests/static', $settings ); + + $registry = WP_Block_Type_Registry::get_instance(); + $block_type = $registry->get_registered( 'tests/static' ); + $this->assertObjectHasProperty( 'editor_script_handles', $block_type ); + $actual_script = $block_type->editor_script; + $actual_script_handles = $block_type->editor_script_handles; + + $this->assertSame( + $expected, + $actual_script, + 'editor_script was not set to the correct value.' + ); + + $this->assertSame( + (array) $expected, + $actual_script_handles, + 'editor_script_handles was not set to the correct value.' + ); + } + + /** + * Data provider for test_register_block_type_throws_doing_it_wrong(). + * + * @return array + */ + public function data_register_block_type_throws_doing_it_wrong() { + return array( + 'a non-string array' => array( + 'editor_script' => array( null, false, true, -1, 0, 1, -1.0, 0.0, 1.0, INF, NAN, new stdClass() ), + 'expected' => null, + ), + 'a partial string array' => array( + 'editor_script' => array( null, false, 'script.js', true, 0, 'actions.js', 1, INF ), + 'expected' => array( 'script.js', 'actions.js' ), + ), + 'a partial string array that results in one item with non-zero index' => array( + 'editor_script' => array( null, false, 'script.js' ), + 'expected' => 'script.js', + ), + ); + } + /** * @ticket 52301 */ @@ -473,13 +1467,13 @@ public function test_block_registers_with_metadata_i18n_support() { * @ticket 45109 */ public function test_get_dynamic_block_names() { - register_block_type( 'core/test-static', array() ); - register_block_type( 'core/test-dynamic', array( 'render_callback' => array( $this, 'render_stub' ) ) ); + register_block_type( 'tests/static', array() ); + register_block_type( 'tests/dynamic', array( 'render_callback' => array( $this, 'render_stub' ) ) ); $dynamic_block_names = get_dynamic_block_names(); - $this->assertContains( 'core/test-dynamic', $dynamic_block_names ); - $this->assertNotContains( 'core/test-static', $dynamic_block_names ); + $this->assertContains( 'tests/dynamic', $dynamic_block_names ); + $this->assertNotContains( 'tests/static', $dynamic_block_names ); } /** @@ -506,11 +1500,26 @@ public function test_has_blocks() { $this->assertFalse( has_blocks( $content ) ); } + /** + * Tests that `has_blocks()` returns `false` with an invalid post. + * + * @ticket 55705 + * + * @covers ::has_blocks + */ + public function test_has_blocks_with_invalid_post() { + $a_post = (object) array( + 'ID' => 55705, + 'filter' => 'display', + ); + $this->assertFalse( has_blocks( $a_post ) ); + } + /** * @ticket 49615 */ public function test_filter_block_registration() { - $filter_registration = static function( $args, $name ) { + $filter_registration = static function ( $args, $name ) { $args['attributes'] = array( $name => array( 'type' => 'boolean' ) ); return $args; }; @@ -528,7 +1537,7 @@ public function test_filter_block_registration() { * @ticket 52138 */ public function test_filter_block_registration_metadata() { - $filter_metadata_registration = static function( $metadata ) { + $filter_metadata_registration = static function ( $metadata ) { $metadata['apiVersion'] = 3; return $metadata; }; @@ -546,7 +1555,7 @@ public function test_filter_block_registration_metadata() { * @ticket 52138 */ public function test_filter_block_registration_metadata_settings() { - $filter_metadata_registration = static function( $settings, $metadata ) { + $filter_metadata_registration = static function ( $settings, $metadata ) { $settings['api_version'] = $metadata['apiVersion'] + 1; return $settings; }; @@ -559,4 +1568,90 @@ public function test_filter_block_registration_metadata_settings() { $this->assertSame( 3, $result->api_version ); } + + /** + * Test case to validate `_doing_it_wrong()` when block style name attribute + * contains one or more spaces. + * + * @dataProvider data_register_block_style_name_contains_spaces + * + * @ticket 54296 + * + * @covers ::register_block_style + * + * @expectedIncorrectUsage WP_Block_Styles_Registry::register + * @param array $block_styles Array of block styles to test. + */ + public function test_register_block_style_name_contains_spaces( array $block_styles ) { + register_block_style( 'core/query', $block_styles ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_register_block_style_name_contains_spaces() { + return array( + 'multiple spaces' => array( + array( + 'name' => 'style-class-1 style-class-2', + 'label' => 'Custom Style Label', + ), + ), + 'single space' => array( + array( + 'name' => 'style-class-1 style-class-2', + 'label' => 'Custom Style Label', + ), + ), + ); + } + + /** + * Test case to validate no `_doing_it_wrong()` happens when there is + * no empty space. + * + * @ticket 54296 + * + * @covers ::register_block_style + */ + public function test_register_block_style_name_without_spaces() { + $block_styles = array( + 'name' => 'style-class-1', + 'label' => 'Custom Style Label', + ); + + $actual = register_block_style( 'core/query', $block_styles ); + $this->assertTrue( $actual ); + } + + /** + * @ticket 59346 + * + * @covers ::register_block_type + * + * @expectedIncorrectUsage register_block_type_from_metadata + */ + public function test_register_block_hooks_targeting_itself() { + $block_type = register_block_type( + DIR_TESTDATA . '/blocks/hooked-block-error' + ); + + $this->assertSame( + array( 'tests/other-block' => 'after' ), + $block_type->block_hooks + ); + } + + /** + * @ticket 63027 + * + * @covers ::register_block_type_from_metadata + */ + public function test_register_block_type_from_metadata_with_windows_path() { + $windows_like_path = str_replace( '/', '\\', DIR_TESTDATA ) . '\\blocks\\notice'; + + $this->assertNotFalse( register_block_type_from_metadata( $windows_like_path ) ); + } } diff --git a/tests/phpunit/tests/blocks/registerBlockTypeFromMetadataWithRegistry.php b/tests/phpunit/tests/blocks/registerBlockTypeFromMetadataWithRegistry.php new file mode 100644 index 0000000000000..b77c16660b0d9 --- /dev/null +++ b/tests/phpunit/tests/blocks/registerBlockTypeFromMetadataWithRegistry.php @@ -0,0 +1,104 @@ +temp_manifest_file = wp_tempnam( 'block-metadata-manifest' ); + } + + public function tear_down() { + $this->unregister_test_blocks(); + unlink( $this->temp_manifest_file ); + parent::tear_down(); + } + + public function test_register_block_type_from_metadata_with_registry() { + $plugin_path = WP_PLUGIN_DIR . '/test-plugin'; + $block_json_path = $plugin_path . '/blocks/test-block/block.json'; + + // Create a manifest file with metadata for our test block + $manifest_data = array( + 'test-block' => array( + 'name' => 'test-suite/test-block', + 'title' => 'Custom Test Block', + 'category' => 'widgets', + 'icon' => 'smiley', + 'description' => 'A test block registered via WP_Block_Metadata_Registry', + 'supports' => array( 'html' => false ), + 'textdomain' => 'test-plugin', + ), + ); + file_put_contents( $this->temp_manifest_file, 'temp_manifest_file ); + + // Attempt to register the block + $registered_block = register_block_type_from_metadata( $block_json_path ); + + // Assert that the block was registered successfully + $this->assertInstanceOf( 'WP_Block_Type', $registered_block ); + $this->assertSame( 'test-suite/test-block', $registered_block->name ); + $this->assertSame( 'Custom Test Block', $registered_block->title ); + $this->assertSame( 'widgets', $registered_block->category ); + $this->assertSame( 'smiley', $registered_block->icon ); + $this->assertSame( 'A test block registered via WP_Block_Metadata_Registry', $registered_block->description ); + $this->assertSame( array( 'html' => false ), $registered_block->supports ); + } + + public function test_register_block_type_from_metadata_with_registry_and_override() { + $plugin_path = WP_PLUGIN_DIR . '/test-plugin-2'; + $block_json_path = $plugin_path . '/blocks/test-block/block.json'; + + // Create a manifest file with metadata for our test block + $manifest_data = array( + 'test-block' => array( + 'name' => 'test-suite/test-block', + 'title' => 'Custom Test Block', + 'category' => 'widgets', + 'icon' => 'smiley', + 'description' => 'A test block registered via WP_Block_Metadata_Registry', + 'supports' => array( 'html' => false ), + ), + ); + file_put_contents( $this->temp_manifest_file, 'temp_manifest_file ); + + // Attempt to register the block with some overrides + $registered_block = register_block_type_from_metadata( + $block_json_path, + array( + 'title' => 'Overridden Title', + 'supports' => array( 'html' => true ), + ) + ); + + // Assert that the block was registered successfully with overrides + $this->assertInstanceOf( 'WP_Block_Type', $registered_block ); + $this->assertSame( 'test-suite/test-block', $registered_block->name ); + $this->assertSame( 'Overridden Title', $registered_block->title ); + $this->assertSame( 'widgets', $registered_block->category ); + $this->assertSame( 'smiley', $registered_block->icon ); + $this->assertSame( 'A test block registered via WP_Block_Metadata_Registry', $registered_block->description ); + $this->assertSame( array( 'html' => true ), $registered_block->supports ); + } + + private function unregister_test_blocks() { + $registry = WP_Block_Type_Registry::get_instance(); + $block_name = 'test-suite/test-block'; + + if ( $registry->is_registered( $block_name ) ) { + $registry->unregister( $block_name ); + } + } +} diff --git a/tests/phpunit/tests/blocks/registerCoreBlockStyleHandles.php b/tests/phpunit/tests/blocks/registerCoreBlockStyleHandles.php new file mode 100644 index 0000000000000..7d5249b3236c0 --- /dev/null +++ b/tests/phpunit/tests/blocks/registerCoreBlockStyleHandles.php @@ -0,0 +1,199 @@ + 'style', + 'editorStyle' => 'editor', + ); + + public function set_up() { + parent::set_up(); + + global $wp_styles; + $this->original_wp_styles = $wp_styles; + $wp_styles = null; + wp_styles(); + + $this->includes_url = includes_url(); + + remove_action( 'wp_default_styles', 'wp_default_styles' ); + } + + public function tear_down() { + global $wp_styles; + $wp_styles = $this->original_wp_styles; + + add_action( 'wp_default_styles', 'wp_default_styles' ); + + parent::tear_down(); + } + + /** + * @ticket 58528 + * + * @dataProvider data_block_data + * + * @covers ::register_core_block_style_handles + * @covers ::wp_should_load_separate_core_block_assets + * + * @param string $name The block name. + * @param array $schema The block's schema. + */ + public function test_wp_should_load_separate_core_block_assets_false( $name, $schema ) { + add_filter( 'should_load_separate_core_block_assets', '__return_false' ); + $this->assertFalse( wp_should_load_separate_core_block_assets(), 'Core blocks are not expected to load separate assets' ); + register_core_block_style_handles(); + + foreach ( self::STYLE_FIELDS as $style_field => $filename ) { + $style_handle = $schema[ $style_field ]; + if ( is_array( $style_handle ) ) { + continue; + } + + $this->assertArrayNotHasKey( $style_handle, $GLOBALS['wp_styles']->registered, 'The key should not exist, as this style should not be registered' ); + } + } + + + /** + * @ticket 58528 + * + * @dataProvider data_block_data + * + * @covers ::register_core_block_style_handles + * @covers ::wp_should_load_separate_core_block_assets + * + * @param string $name The block name. + * @param array $schema The block's schema. + */ + public function test_wp_should_load_separate_core_block_assets_true( $name, $schema ) { + add_filter( 'should_load_separate_core_block_assets', '__return_true' ); + $this->assertTrue( wp_should_load_separate_core_block_assets(), 'Core assets are expected to load separately' ); + register_core_block_style_handles(); + + $wp_styles = $GLOBALS['wp_styles']; + + foreach ( self::STYLE_FIELDS as $style_field => $filename ) { + $style_handle = $schema[ $style_field ]; + if ( is_array( $style_handle ) ) { + continue; + } + + $this->assertArrayHasKey( $style_handle, $wp_styles->registered, 'The key should exist, as this style should be registered' ); + if ( false === $wp_styles->registered[ $style_handle ]->src ) { + $this->assertEmpty( $wp_styles->registered[ $style_handle ]->extra, 'If source is false, style path should not be set' ); + } else { + $this->assertStringContainsString( $this->includes_url, $wp_styles->registered[ $style_handle ]->src, 'Source of style should contain the includes url' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra, 'The path of the style should exist' ); + $this->assertArrayHasKey( 'path', $wp_styles->registered[ $style_handle ]->extra, 'The path key of the style should exist in extra array' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra['path'], 'The path key of the style should not be empty' ); + } + } + } + + /** + * @ticket 58560 + * + * @dataProvider data_block_data + * + * @param string $name The block name. + */ + public function test_wp_should_load_separate_core_block_assets_current_theme_supports( $name ) { + add_filter( 'should_load_separate_core_block_assets', '__return_true' ); + add_theme_support( 'wp-block-styles' ); + register_core_block_style_handles(); + + $wp_styles = $GLOBALS['wp_styles']; + + $style_handle = "wp-block-{$name}-theme"; + + $this->assertArrayHasKey( $style_handle, $wp_styles->registered, 'The key should exist, as this style should be registered' ); + if ( false === $wp_styles->registered[ $style_handle ]->src ) { + $this->assertEmpty( $wp_styles->registered[ $style_handle ]->extra, 'If source is false, style path should not be set' ); + } else { + $this->assertStringContainsString( $this->includes_url, $wp_styles->registered[ $style_handle ]->src, 'Source of style should contain the includes url' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra, 'The path of the style should exist' ); + $this->assertArrayHasKey( 'path', $wp_styles->registered[ $style_handle ]->extra, 'The path key of the style should exist in extra array' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra['path'], 'The path key of the style should not be empty' ); + } + } + + /** + * @ticket 59715 + * + * @dataProvider data_block_data + * + * @param string $name The block name. + */ + public function test_register_core_block_style_handles_should_load_rtl_stylesheets_for_rtl_text_direction( $name ) { + global $wp_locale; + + $orig_text_dir = $wp_locale->text_direction; + $wp_locale->text_direction = 'rtl'; + + add_filter( 'should_load_separate_core_block_assets', '__return_true' ); + register_core_block_style_handles(); + + $wp_styles = $GLOBALS['wp_styles']; + + $style_handle = "wp-block-{$name}-theme"; + + $wp_locale->text_direction = $orig_text_dir; + + $this->assertArrayHasKey( $style_handle, $wp_styles->registered, 'The key should exist, as this style should be registered' ); + if ( false === $wp_styles->registered[ $style_handle ]->src ) { + $this->assertEmpty( $wp_styles->registered[ $style_handle ]->extra, 'If source is false, style path should not be set' ); + } else { + $this->assertStringContainsString( $this->includes_url, $wp_styles->registered[ $style_handle ]->src, 'Source of style should contain the includes url' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra, 'The path of the style should exist' ); + $this->assertArrayHasKey( 'path', $wp_styles->registered[ $style_handle ]->extra, 'The path key of the style should exist in extra array' ); + $this->assertNotEmpty( $wp_styles->registered[ $style_handle ]->extra['path'], 'The path key of the style should not be empty' ); + $this->assertArrayHasKey( 'rtl', $wp_styles->registered[ $style_handle ]->extra, 'The rtl key of the style should exist in extra array' ); + } + } + + public function data_block_data() { + $core_blocks_meta = require ABSPATH . WPINC . '/blocks/blocks-json.php'; + + // Remove this blocks for now, as they are registered elsewhere. + unset( $core_blocks_meta['archives'] ); + unset( $core_blocks_meta['widget-group'] ); + + $data = array(); + foreach ( $core_blocks_meta as $name => $schema ) { + if ( ! isset( $schema['style'] ) ) { + $schema['style'] = "wp-block-$name"; + } + if ( ! isset( $schema['editorStyle'] ) ) { + $schema['editorStyle'] = "wp-block-{$name}-editor"; + } + + $data[ $name ] = array( $name, $schema ); + } + + return $data; + } +} diff --git a/tests/phpunit/tests/blocks/render.php b/tests/phpunit/tests/blocks/render.php index 373217bd74403..7b20dac147601 100644 --- a/tests/phpunit/tests/blocks/render.php +++ b/tests/phpunit/tests/blocks/render.php @@ -1,16 +1,10 @@ is_registered( 'core/dynamic' ) ) { $registry->unregister( 'core/dynamic' ); } + if ( $registry->is_registered( 'tests/notice' ) ) { + $registry->unregister( 'tests/notice' ); + } parent::tear_down(); } @@ -78,6 +75,9 @@ public function test_the_content() { // Block rendering add some extra blank lines, but we're not worried about them. $block_filtered_content = preg_replace( "/\n{2,}/", "\n", $block_filtered_content ); + // Paragraph blocks now add a class, strip it for comparison with classic content. + $block_filtered_content = str_replace( ' class="wp-block-paragraph"', '', $block_filtered_content ); + remove_shortcode( 'someshortcode' ); $this->assertSame( trim( $classic_filtered_content ), trim( $block_filtered_content ) ); @@ -219,16 +219,41 @@ public function test_do_block_output( $html_filename, $server_html_filename ) { } } - $html = do_blocks( self::strip_r( file_get_contents( $html_path ) ) ); - $expected_html = self::strip_r( file_get_contents( $server_html_path ) ); + $html = do_blocks( self::strip_r( file_get_contents( $html_path ) ) ); + // If blocks opt into Gutenberg's layout implementation + // the container will receive an additional, unique classname based on "wp-container-[blockname]-layout" + // so we need to normalize the random id. + $normalized_html = preg_replace( '/wp-container-[a-z-]+\d+/', 'wp-container-1', $html ); + + // The gallery block uses a unique class name of `wp_unique_id( 'wp-block-gallery-' )` + // so we need to normalize the random id. + $normalized_html = preg_replace( '/wp-block-gallery-\d+/', 'wp-block-gallery-1', $normalized_html ); + $expected_html = self::strip_r( file_get_contents( $server_html_path ) ); + + // Convert HTML to be white space insensitive. + $normalized_html = preg_replace( '/(\s+$)/m', '', $normalized_html ); + $expected_html = preg_replace( '/(\s+$)/m', '', $expected_html ); $this->assertSame( $expected_html, - $html, + $normalized_html, "File '$html_path' does not match expected value" ); } + /** + * @ticket 53148 + */ + public function test_render_field_in_block_json() { + $result = register_block_type( + DIR_TESTDATA . '/blocks/notice' + ); + + $actual_content = do_blocks( '' ); + $this->assertSame( '

      Hello from the test

      ', trim( $actual_content ) ); + } + + /** * @ticket 45109 */ @@ -264,6 +289,56 @@ public function test_dynamic_block_rendering() { ); } + /** + * @ticket 62114 + */ + public function test_dynamic_block_with_default_attributes() { + $settings = array( + 'attributes' => array( + 'content' => array( + 'type' => 'string', + 'default' => 'Default content.', + ), + 'align' => array( + 'type' => 'string', + 'default' => 'full', + ), + 'backgroundColor' => array( + 'type' => 'string', + 'default' => 'blueberry-1', + ), + 'textColor' => array( + 'type' => 'string', + 'default' => 'white', + ), + ), + 'supports' => array( + 'align' => array( 'wide', 'full' ), + 'color' => array( + 'background' => true, + 'text' => true, + ), + ), + 'render_callback' => function ( $attributes ) { + return '

      ' . $attributes['content'] . '

      '; + }, + ); + register_block_type( 'core/dynamic', $settings ); + + $post_content = + 'before' . + '' . + 'after'; + + $updated_post_content = do_blocks( $post_content ); + $this->assertSame( + $updated_post_content, + 'before' . + '

      Default content.

      ' . + 'after' + ); + } + /** * @ticket 45109 */ @@ -499,5 +574,4 @@ public function render_test_block_wp_query() { return $content; } - } diff --git a/tests/phpunit/tests/blocks/renderBlock.php b/tests/phpunit/tests/blocks/renderBlock.php new file mode 100644 index 0000000000000..be251692c5c62 --- /dev/null +++ b/tests/phpunit/tests/blocks/renderBlock.php @@ -0,0 +1,323 @@ + 'example', + 'post_excerpt' => '', + ); + + $post = self::factory()->post->create_and_get( $args ); + setup_postdata( $post ); + } + + /** + * Tear down each test method. + */ + public function tear_down() { + // Removes test block types registered by test cases. + $block_types = WP_Block_Type_Registry::get_instance()->get_all_registered(); + foreach ( $block_types as $block_type ) { + $block_name = $block_type->name; + if ( str_starts_with( $block_name, 'tests/' ) ) { + unregister_block_type( $block_name ); + } + } + + parent::tear_down(); + } + + /** + * Tests that a block which provides context makes that context available to + * its inner blocks. + * + * @ticket 49927 + * + * @covers ::register_block_type + * @covers ::render_block + */ + public function test_provides_block_context() { + $provided_context = array(); + + register_block_type( + 'tests/context-provider', + array( + 'attributes' => array( + 'contextWithAssigned' => array( + 'type' => 'number', + ), + 'contextWithDefault' => array( + 'type' => 'number', + 'default' => 0, + ), + 'contextWithoutDefault' => array( + 'type' => 'number', + ), + 'contextNotRequested' => array( + 'type' => 'number', + ), + ), + 'provides_context' => array( + 'tests/contextWithAssigned' => 'contextWithAssigned', + 'tests/contextWithDefault' => 'contextWithDefault', + 'tests/contextWithoutDefault' => 'contextWithoutDefault', + 'tests/contextNotRequested' => 'contextNotRequested', + ), + ) + ); + + register_block_type( + 'tests/context-consumer', + array( + 'uses_context' => array( + 'tests/contextWithDefault', + 'tests/contextWithAssigned', + 'tests/contextWithoutDefault', + ), + 'render_callback' => static function ( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $parsed_blocks = parse_blocks( + '' . + '' . + '' + ); + + render_block( $parsed_blocks[0] ); + + $this->assertSame( + array( + 'tests/contextWithDefault' => 0, + 'tests/contextWithAssigned' => 10, + ), + $provided_context[0] + ); + } + + /** + * Tests that a block can receive default-provided context through + * render_block. + * + * @ticket 49927 + * + * @covers ::register_block_type + * @covers ::render_block + */ + public function test_provides_default_context() { + global $post; + + $provided_context = array(); + + register_block_type( + 'tests/context-consumer', + array( + 'uses_context' => array( 'postId', 'postType' ), + 'render_callback' => static function ( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $parsed_blocks = parse_blocks( '' ); + + render_block( $parsed_blocks[0] ); + + $this->assertSame( + array( + 'postId' => $post->ID, + 'postType' => $post->post_type, + ), + $provided_context[0] + ); + } + + /** + * Tests that default block context can be filtered. + * + * @ticket 49927 + * + * @covers ::register_block_type + * @covers ::render_block + */ + public function test_default_context_is_filterable() { + $provided_context = array(); + + register_block_type( + 'tests/context-consumer', + array( + 'uses_context' => array( 'example' ), + 'render_callback' => static function ( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context[] = $block->context; + + return ''; + }, + ) + ); + + $filter_block_context = static function ( $context ) { + $context['example'] = 'ok'; + return $context; + }; + + $parsed_blocks = parse_blocks( '' ); + + add_filter( 'render_block_context', $filter_block_context ); + + render_block( $parsed_blocks[0] ); + + remove_filter( 'render_block_context', $filter_block_context ); + + $this->assertSame( array( 'example' => 'ok' ), $provided_context[0] ); + } + + /** + * Tests the behavior of the 'render_block_context' filter based on the location of the filtered block. + * + * @ticket 62046 + */ + public function test_render_block_context_inner_blocks() { + $provided_context = array(); + + register_block_type( + 'tests/context-provider', + array( + 'provides_context' => array( 'example' ), + ) + ); + + register_block_type( + 'tests/context-consumer', + array( + 'uses_context' => array( 'example' ), + 'render_callback' => static function ( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context = $block->context; + + return ''; + }, + ) + ); + + // Filter the context provided by the test block. + add_filter( + 'render_block_context', + function ( $context, $parsed_block ) { + if ( isset( $parsed_block['blockName'] ) && 'tests/context-provider' === $parsed_block['blockName'] ) { + $context['example'] = 'ok'; + } + + return $context; + }, + 10, + 2 + ); + + // Test inner block context when the provider block is a top-level block. + do_blocks( + << + + +HTML + ); + $this->assertArrayHasKey( 'example', $provided_context, 'Test block is top-level block: Context should include "example"' ); + $this->assertSame( 'ok', $provided_context['example'], 'Test block is top-level block: "example" in context should be "ok"' ); + + // Test inner block context when the provider block is an inner block. + do_blocks( + << + + + + +HTML + ); + $this->assertArrayHasKey( 'example', $provided_context, 'Test block is inner block: Block context should include "example"' ); + $this->assertSame( 'ok', $provided_context['example'], 'Test block is inner block: "example" in context should be "ok"' ); + } + + /** + * Tests that the 'render_block_context' filter arbitrary context. + * + * @ticket 62046 + */ + public function test_render_block_context_allowed_context() { + $provided_context = array(); + + register_block_type( + 'tests/context-consumer', + array( + 'uses_context' => array( 'example' ), + 'render_callback' => static function ( $attributes, $content, $block ) use ( &$provided_context ) { + $provided_context = $block->context; + + return ''; + }, + ) + ); + + // Filter the context provided to the test block. + add_filter( + 'render_block_context', + function ( $context, $parsed_block ) { + if ( isset( $parsed_block['blockName'] ) && 'tests/context-consumer' === $parsed_block['blockName'] ) { + $context['arbitrary'] = 'ok'; + } + + return $context; + }, + 10, + 2 + ); + + do_blocks( + << +HTML + ); + $this->assertArrayNotHasKey( 'arbitrary', $provided_context, 'Test block is top-level block: Block context should not include "arbitrary"' ); + + do_blocks( + << + + +HTML + ); + + /* + * These assertions assert something that ideally should not be the case: Inner blocks should respect the + * `uses_context` value just like top-level blocks do. However, due to logic in `WP_Block::render()`, the + * `context` property value itself is filterable when it should rather only apply to the `available_context` + * property. + * However, changing this behavior now would be a backward compatibility break, hence the assertion here. + * Potentially it can be reconsidered in the future, so that these two assertions could be replaced with an + * `assertArrayNotHasKey( 'arbitrary', $provided_context )`. + */ + $this->assertArrayHasKey( 'arbitrary', $provided_context, 'Test block is inner block: Block context should include "arbitrary"' ); + $this->assertSame( 'ok', $provided_context['arbitrary'], 'Test block is inner block: "arbitrary" in context should be "ok"' ); + } +} diff --git a/tests/phpunit/tests/blocks/renderCommentTemplate.php b/tests/phpunit/tests/blocks/renderCommentTemplate.php new file mode 100644 index 0000000000000..4390c56a6dded --- /dev/null +++ b/tests/phpunit/tests/blocks/renderCommentTemplate.php @@ -0,0 +1,661 @@ + $original_value ) { + update_option( $option, $original_value ); + } + + parent::tear_down(); + } + + public function set_up() { + parent::set_up(); + + update_option( 'page_comments', true ); + update_option( 'comments_per_page', self::$per_page ); + + self::$custom_post = self::factory()->post->create_and_get( + array( + 'post_type' => 'dogs', + 'post_status' => 'publish', + 'post_name' => 'metaldog', + 'post_title' => 'Metal Dog', + 'post_content' => 'Metal Dog content', + 'post_excerpt' => 'Metal Dog', + ) + ); + + self::$comment_ids = self::factory()->comment->create_post_comments( + self::$custom_post->ID, + 1, + array( + 'comment_author' => 'Test', + 'comment_author_email' => 'test@example.org', + 'comment_author_url' => 'http://example.com/author-url/', + 'comment_content' => 'Hello world', + ) + ); + } + + /** + * @ticket 55505 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_with_context() { + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $this->assertSameSetsWithIndex( + array( + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'status' => 'approve', + 'no_found_rows' => false, + 'post_id' => self::$custom_post->ID, + 'hierarchical' => 'threaded', + 'number' => 5, + 'paged' => 1, + ), + build_comment_query_vars_from_block( $block ) + ); + } + + /** + * @ticket 55567 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_with_context_no_pagination() { + update_option( 'page_comments', false ); + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $this->assertSameSetsWithIndex( + array( + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'status' => 'approve', + 'no_found_rows' => false, + 'post_id' => self::$custom_post->ID, + 'hierarchical' => 'threaded', + ), + build_comment_query_vars_from_block( $block ) + ); + } + + /** + * @ticket 55505 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_no_context() { + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( $parsed_blocks[0] ); + + $this->assertSameSetsWithIndex( + array( + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'status' => 'approve', + 'no_found_rows' => false, + 'hierarchical' => 'threaded', + 'number' => 5, + 'paged' => 1, + ), + build_comment_query_vars_from_block( $block ) + ); + } + + /** + * Test that if pagination is set to display the last page by default (i.e. newest comments), + * the query is set to look for page 1 (rather than page 0, which would cause an error). + * + * Regression: https://github.com/WordPress/gutenberg/issues/40758. + * + * @ticket 55658 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_pagination_with_no_comments() { + $comments_per_page = get_option( 'comments_per_page' ); + $default_comments_page = get_option( 'default_comments_page' ); + + update_option( 'comments_per_page', 50 ); + update_option( 'previous_default_page', 'newest' ); + + $post_without_comments = self::factory()->post->create_and_get( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_name' => 'fluffycat', + 'post_title' => 'Fluffy Cat', + 'post_content' => 'Fluffy Cat content', + 'post_excerpt' => 'Fluffy Cat', + ) + ); + + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => $post_without_comments->ID, + ) + ); + + $this->assertSameSetsWithIndex( + array( + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'status' => 'approve', + 'no_found_rows' => false, + 'post_id' => $post_without_comments->ID, + 'hierarchical' => 'threaded', + 'number' => 50, + ), + build_comment_query_vars_from_block( $block ) + ); + } + + + /** + * Test that both "Older Comments" and "Newer Comments" are displayed in the correct order + * inside the Comment Query Loop when we enable pagination on Discussion Settings. + * + * @ticket 55505 + * @ticket 60806 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_sets_max_num_pages() { + + // This could be any number, we set a fixed one instead of a random for better performance. + $comment_query_max_num_pages = 5; + // We subtract 1 because we created 1 comment at the beginning. + $post_comments_numbers = ( self::$per_page * $comment_query_max_num_pages ) - 1; + self::factory()->comment->create_post_comments( + self::$custom_post->ID, + $post_comments_numbers, + array( + 'comment_author' => 'Test', + 'comment_author_email' => 'test@example.org', + 'comment_author_url' => 'http://example.com/author-url/', + 'comment_content' => 'Hello world', + ) + ); + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + 'comments/inherit' => true, + ) + ); + $actual = build_comment_query_vars_from_block( $block ); + $this->assertSame( $comment_query_max_num_pages, $actual['paged'] ); + } + + /** + * Test rendering a single comment + * + * @ticket 55567 + */ + public function test_rendering_comment_template() { + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $this->assertSame( + str_replace( array( "\n", "\t" ), '', '
      1. Hello world

      ' ), + str_replace( array( "\n", "\t" ), '', $block->render() ) + ); + } + + /** + * Test rendering nested comments: + * + * └─ comment 1 + *   └─ comment 2 + *    └─ comment 4 + *   └─ comment 3 + * + * @ticket 55567 + */ + public function test_rendering_comment_template_nested() { + $first_level_ids = self::factory()->comment->create_post_comments( + self::$custom_post->ID, + 2, + array( + 'comment_parent' => self::$comment_ids[0], + 'comment_author' => 'Test', + 'comment_author_email' => 'test@example.org', + 'comment_author_url' => 'http://example.com/author-url/', + 'comment_content' => 'Hello world', + ) + ); + + $second_level_ids = self::factory()->comment->create_post_comments( + self::$custom_post->ID, + 1, + array( + 'comment_parent' => $first_level_ids[0], + 'comment_author' => 'Test', + 'comment_author_email' => 'test@example.org', + 'comment_author_url' => 'http://example.com/author-url/', + 'comment_content' => 'Hello world', + ) + ); + + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $top_level_ids = self::$comment_ids; + $expected = str_replace( + array( "\r\n", "\n", "\t" ), + '', + << +
    • + +
      +

      Hello world

      +
      +
        +
      1. + +
        +

        Hello world

        +
        +
          +
        1. + +
          +

          Hello world

          +
          +
        2. +
        +
      2. +
      3. + +
        +

        Hello world

        +
        +
      4. +
      +
    • + +END + ); + + $this->assertSame( + $expected, + str_replace( array( "\r\n", "\n", "\t" ), '', $block->render() ) + ); + } + + /** + * Test that line and paragraph breaks are converted to HTML tags in a comment. + * + * @ticket 55643 + */ + public function test_render_block_core_comment_content_converts_to_html() { + $comment_id = self::$comment_ids[0]; + $new_content = "Paragraph One\n\nP2L1\nP2L2\n\nhttps://example.com/"; + self::factory()->comment->update_object( + $comment_id, + array( 'comment_content' => $new_content ) + ); + + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + 'comments/inherit' => true, + ) + ); + + $expected_content = "

      Paragraph One

      \n

      P2L1
      \nP2L2

      \n

      https://example.com/

      \n"; + + $this->assertSame( + '
      1. ' . $expected_content . '
      ', + $block->render() + ); + } + + /** + * Test that unapproved comments are included if it is a preview. + * + * @ticket 55634 + * + * @covers ::build_comment_query_vars_from_block + */ + public function test_build_comment_query_vars_from_block_with_comment_preview() { + $parsed_blocks = parse_blocks( + '' + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $commenter_filter = static function () { + return array( + 'comment_author_email' => 'unapproved@example.org', + ); + }; + + add_filter( 'wp_get_current_commenter', $commenter_filter ); + + $this->assertSameSetsWithIndex( + array( + 'orderby' => 'comment_date_gmt', + 'order' => 'ASC', + 'status' => 'approve', + 'no_found_rows' => false, + 'include_unapproved' => array( 'unapproved@example.org' ), + 'post_id' => self::$custom_post->ID, + 'hierarchical' => 'threaded', + 'number' => 5, + 'paged' => 1, + ), + build_comment_query_vars_from_block( $block ) + ); + } + + /** + * Test rendering an unapproved comment preview. + * + * @ticket 55643 + */ + public function test_rendering_comment_template_unmoderated_preview() { + $parsed_blocks = parse_blocks( + '' + ); + + $unapproved_comment = self::factory()->comment->create_post_comments( + self::$custom_post->ID, + 1, + array( + 'comment_author' => 'Visitor', + 'comment_author_email' => 'unapproved@example.org', + 'comment_author_url' => 'http://example.com/unapproved/', + 'comment_content' => 'Hi there! My comment needs moderation.', + 'comment_approved' => 0, + ) + ); + + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + + $commenter_filter = static function () { + return array( + 'comment_author_email' => 'unapproved@example.org', + ); + }; + + add_filter( 'wp_get_current_commenter', $commenter_filter ); + + $this->assertSame( + '
      1. Hello world

      2. Visitor

        Your comment is awaiting moderation.

        Hi there! My comment needs moderation.
      ', + str_replace( array( "\n", "\t" ), '', $block->render() ), + 'Should include unapproved comments when filter applied' + ); + + remove_filter( 'wp_get_current_commenter', $commenter_filter ); + + // Test it again and ensure the unmoderated comment doesn't leak out. + $this->assertSame( + '
      1. Hello world

      ', + str_replace( array( "\n", "\t" ), '', $block->render() ), + 'Should not include any unapproved comments after removing filter' + ); + } + + /** + * Tests that the Comment Template block makes comment ID context available to programmatically inserted child blocks. + * + * @ticket 58839 + * + * @covers ::render_block_core_comment_template + * @covers ::block_core_comment_template_render_comments + */ + public function test_rendering_comment_template_sets_comment_id_context() { + $render_block_context_callback = new MockAction(); + add_filter( 'render_block_context', array( $render_block_context_callback, 'filter' ), 2, 3 ); + + $parsed_comment_author_name_block = parse_blocks( '' )[0]; + $comment_author_name_block = new WP_Block( + $parsed_comment_author_name_block, + array( + 'commentId' => self::$comment_ids[0], + ) + ); + $comment_author_name_block_markup = $comment_author_name_block->render(); + + add_filter( + 'render_block', + static function ( $block_content, $block ) use ( $parsed_comment_author_name_block ) { + /* + * Insert a Comment Author Name block (which requires `commentId` + * block context to work) after the Comment Content block. + */ + if ( 'core/comment-content' !== $block['blockName'] ) { + return $block_content; + } + + $inserted_content = render_block( $parsed_comment_author_name_block ); + return $inserted_content . $block_content; + }, + 10, + 3 + ); + + $parsed_blocks = parse_blocks( + '' + ); + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + $markup = $block->render(); + + $this->assertStringContainsString( $comment_author_name_block_markup, $markup ); + + $args = $render_block_context_callback->get_args(); + $context = $args[0][0]; + $this->assertArrayHasKey( + 'commentId', + $context, + "commentId block context wasn't set for render_block_context filter at priority 2." + ); + $this->assertSame( + strval( self::$comment_ids[0] ), + $context['commentId'], + "commentId block context wasn't set correctly." + ); + } + + /** + * Tests that an inner block added via the render_block_data filter is retained at render_block stage. + * + * @ticket 58839 + * + * @covers ::render_block_core_comment_template + * @covers ::block_core_comment_template_render_comments + */ + public function test_inner_block_inserted_by_render_block_data_is_retained() { + $render_block_callback = new MockAction(); + add_filter( 'render_block', array( $render_block_callback, 'filter' ), 10, 3 ); + + $render_block_data_callback = static function ( $parsed_block ) { + // Add a Social Links block to a Comment Template block's inner blocks. + if ( 'core/comment-template' === $parsed_block['blockName'] ) { + $inserted_block_markup = << + +' +END; + + $inserted_blocks = parse_blocks( $inserted_block_markup ); + + $parsed_block['innerBlocks'][] = $inserted_blocks[0]; + } + return $parsed_block; + }; + + add_filter( 'render_block_data', $render_block_data_callback, 10, 1 ); + $parsed_blocks = parse_blocks( + '' + ); + $block = new WP_Block( + $parsed_blocks[0], + array( + 'postId' => self::$custom_post->ID, + ) + ); + $block->render(); + remove_filter( 'render_block_data', $render_block_data_callback ); + + $this->assertSame( + 5, + $render_block_callback->get_call_count(), + "render_block filter wasn't called the correct number of 5 times." + ); + + $args = $render_block_callback->get_args(); + $this->assertSame( + 'core/comment-content', + $args[0][2]->name, + "render_block filter didn't receive Comment Content block instance upon first call." + ); + $this->assertSame( + 'core/comment-template', + $args[1][2]->name, + "render_block filter didn't receive Comment Template block instance upon second call." + ); + $this->assertCount( + 2, + $args[1][2]->inner_blocks, + "Inner block inserted by render_block_data filter wasn't retained." + ); + $this->assertInstanceOf( + 'WP_Block', + $args[1][2]->inner_blocks[1], + "Inner block inserted by render_block_data isn't a WP_Block class instance." + ); + $this->assertSame( + 'core/social-links', + $args[1][2]->inner_blocks[1]->name, + "Inner block inserted by render_block_data isn't named as expected." + ); + } +} diff --git a/tests/phpunit/tests/blocks/renderReusable.php b/tests/phpunit/tests/blocks/renderReusable.php index 086ae678948f6..f38ae41a41173 100644 --- a/tests/phpunit/tests/blocks/renderReusable.php +++ b/tests/phpunit/tests/blocks/renderReusable.php @@ -1,16 +1,10 @@ get_registered( 'core/block' ); - $output = $block_type->render( array( 'ref' => self::$block_id ) ); - $this->assertSame( '

      Hello world!

      ', $output ); + $parsed_block = array( + 'blockName' => 'core/block', + 'attrs' => array( 'ref' => self::$block_id ), + ); + $block = new WP_Block( $parsed_block ); + $output = $block->render(); + $this->assertSame( '

      Hello world!

      ', $output ); } /** - * Make sure that a reusable block can be rendered twice in a row. + * Make sure that a synced pattern can be rendered twice in a row. * * @ticket 52364 */ public function test_render_subsequent() { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); - $output = $block_type->render( array( 'ref' => self::$block_id ) ); - $output .= $block_type->render( array( 'ref' => self::$block_id ) ); - $this->assertSame( '

      Hello world!

      Hello world!

      ', $output ); + $parsed_block = array( + 'blockName' => 'core/block', + 'attrs' => array( 'ref' => self::$block_id ), + ); + $block = new WP_Block( $parsed_block ); + $output = $block->render(); + $output .= $block->render(); + $this->assertSame( '

      Hello world!

      Hello world!

      ', $output ); } public function test_ref_empty() { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); - $output = $block_type->render( array() ); + $parsed_block = array( + 'blockName' => 'core/block', + 'attrs' => array(), + ); + $block = new WP_Block( $parsed_block ); + $output = $block->render(); $this->assertSame( '', $output ); } public function test_ref_wrong_post_type() { - $block_type = WP_Block_Type_Registry::get_instance()->get_registered( 'core/block' ); - $output = $block_type->render( array( 'ref' => self::$post_id ) ); + $parsed_block = array( + 'blockName' => 'core/block', + 'attrs' => array( 'ref' => self::$post_id ), + ); + $block = new WP_Block( $parsed_block ); + $output = $block->render(); $this->assertSame( '', $output ); } } diff --git a/tests/phpunit/tests/blocks/resolvePatternBlocks.php b/tests/phpunit/tests/blocks/resolvePatternBlocks.php new file mode 100644 index 0000000000000..72471eaa05289 --- /dev/null +++ b/tests/phpunit/tests/blocks/resolvePatternBlocks.php @@ -0,0 +1,176 @@ + 'Test', + 'content' => 'HelloWorld', + 'description' => 'Test pattern.', + ) + ); + register_block_pattern( + 'core/recursive', + array( + 'title' => 'Recursive', + 'content' => 'Recursive', + 'description' => 'Recursive pattern.', + ) + ); + register_block_pattern( + 'core/single-root', + array( + 'title' => 'Single Root Pattern', + 'content' => 'Single root content', + 'description' => 'A single root pattern.', + 'categories' => array( 'text' ), + ) + ); + register_block_pattern( + 'core/single-root-with-forbidden-chars-in-attrs', + array( + 'title' => 'Single Root Pattern', + 'content' => 'Single root content', + 'description' => 'A single root pattern.', + 'categories' => array( + 'text', + 'bad\'); DROP TABLE wp_posts;--', + '', + "evil\x00null\nbyte", + 'category with html tags', + ), + ) + ); + register_block_pattern( + 'core/with-attrs', + array( + 'title' => 'Pattern With Attrs', + 'content' => 'Content', + 'description' => 'A pattern with existing attributes.', + ) + ); + register_block_pattern( + 'core/nested-single', + array( + 'title' => 'Nested Pattern', + 'content' => 'Nested content', + 'description' => 'A nested single root pattern.', + 'categories' => array( 'featured' ), + ) + ); + register_block_pattern( + 'core/existing-metadata', + array( + 'title' => 'Existing Metadata Pattern', + 'content' => 'Existing metadata content', + ) + ); + register_block_pattern( + 'core/with-custom-metadata', + array( + 'title' => 'Pattern With Custom Metadata', + 'content' => 'Content with custom metadata', + 'description' => 'A pattern with custom metadata keys.', + 'categories' => array( 'test' ), + ) + ); + } + + public function tear_down() { + unregister_block_pattern( 'core/test' ); + unregister_block_pattern( 'core/recursive' ); + unregister_block_pattern( 'core/single-root' ); + unregister_block_pattern( 'core/single-root-with-forbidden-chars-in-attrs' ); + unregister_block_pattern( 'core/with-attrs' ); + unregister_block_pattern( 'core/nested-single' ); + unregister_block_pattern( 'core/existing-metadata' ); + unregister_block_pattern( 'core/with-custom-metadata' ); + parent::tear_down(); + } + + /** + * @dataProvider data_should_resolve_pattern_blocks_as_expected + * + * @ticket 61228 + * + * @param string $blocks A string representing blocks that need resolving. + * @param string $expected Expected result. + */ + public function test_should_resolve_pattern_blocks_as_expected( $blocks, $expected ) { + $actual = resolve_pattern_blocks( parse_blocks( $blocks ) ); + $this->assertSame( $expected, serialize_blocks( $actual ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_resolve_pattern_blocks_as_expected() { + return array( + // Works without attributes, leaves the block as is. + 'pattern with no slug attribute' => array( + '', + '', + ), + // Resolves the pattern. + 'test pattern' => array( + '', + 'HelloWorld', + ), + // Skips recursive patterns. + 'recursive pattern' => array( + '', + 'Recursive', + ), + // Resolves the pattern within a block. + 'pattern within a block' => array( + 'BeforeAfter', + 'BeforeHelloWorldAfter', + ), + // Resolves the single-root pattern and adds metadata. + 'single-root pattern' => array( + '', + 'Single root content', + ), + // Existing attributes are preserved when adding metadata. + 'existing attributes preserved' => array( + '', + 'Content', + ), + // Resolves the nested single-root pattern and adds metadata. + 'nested single-root pattern' => array( + '', + 'Nested contentSingle root content', + ), + // Sanitizes fields. + 'sanitized pattern attrs' => array( + '', + 'Single root content', + ), + // Metadata is merged with existing metadata and existing metadata is preserved. + 'existing metadata preserved' => array( + '', + 'Existing metadata content', + ), + // Custom metadata keys are preserved when resolving patterns. + 'custom metadata preserved' => array( + '', + 'Content with custom metadata', + ), + ); + } +} diff --git a/tests/phpunit/tests/blocks/serialize.php b/tests/phpunit/tests/blocks/serialize.php index aa28f212097ac..5d119e655babc 100644 --- a/tests/phpunit/tests/blocks/serialize.php +++ b/tests/phpunit/tests/blocks/serialize.php @@ -1,55 +1,121 @@ 'test', + 'attrs' => array( + 'lt' => '<', + 'gt' => '>', + 'amp' => '&', + 'bs' => '\\', + 'quot' => '"', + 'bs-bs-quot' => '\\\\"', + ), + 'innerBlocks' => array(), + 'innerHTML' => '', + 'innerContent' => array(), + ); + + $expected = ''; + $this->assertSame( $expected, serialize_block( $block ) ); + } /** * @dataProvider data_serialize_identity_from_parsed + * + * @param string $original Original block markup. + * + * @ticket 63917 */ public function test_serialize_identity_from_parsed( $original ) { $blocks = parse_blocks( $original ); - $actual = serialize_blocks( $blocks ); - $expected = $original; + $actual = serialize_blocks( $blocks ); - $this->assertSame( $expected, $actual ); + $this->assertSame( $original, $actual ); } - public function data_serialize_identity_from_parsed() { + public static function data_serialize_identity_from_parsed(): array { return array( - // Void block. - array( '' ), + 'Void block' => + array( '' ), + + 'Freeform content ($block_name = null)' => + array( 'Example.' ), + + 'Block with content' => + array( 'Example.' ), + + 'Block with attributes' => + array( '' ), - // Freeform content ($block_name = null). - array( 'Example.' ), + 'Block with inner blocks' => + array( "Example.\n\nExample.\n\n" ), - // Block with content. - array( 'Example.' ), + 'Block with attribute values that may conflict with HTML comment' => + array( '' ), - // Block with attributes. - array( '' ), + 'Block with attribute values that should not be escaped' => + array( '' ), - // Block with inner blocks. - array( "Example.\n\nExample.\n\n" ), + 'Backslashes in attributes, Gutenberg #16508' => + array( '' ), - // Block with attribute values that may conflict with HTML comment. - array( '' ), + 'Tricky backslashes' => + array( '' ), + ); + } + + /** + * The serialization was adjusted to use unicode escapes sequences for escaped `\` and `"` + * characters inside JSON strings. + * + * Ensure that the previous escape form can be parsed compatibly and serialized back to + * the new form. + * + * @see https://github.com/WordPress/wordpress-develop/pull/9558 + * @see https://github.com/WordPress/gutenberg/pull/71291 + * + * @ticket 63917 + * + * @dataProvider data_serialize_compatible_forms + * + * @param string $before Previous serialization form. + * @param string $after New serialization form. + */ + public function test_older_serialization_is_compatible( string $before, string $after ) { + $this->assertNotSame( $before, $after, 'The same serialization should not be provided for before and after.' ); + $blocks = parse_blocks( $before ); + $actual = serialize_blocks( $blocks ); + $this->assertSame( $after, $actual ); + } + + public static function data_serialize_compatible_forms(): array { + return array( + 'Special characters' => array( + '', + '', + ), - // Block with attribute values that should not be escaped. - array( '' ), + 'Backslashes' => array( + '', + '', + ), ); } @@ -60,4 +126,236 @@ public function test_serialized_block_name() { $this->assertSame( 'plugin/example', strip_core_block_namespace( 'plugin/example' ) ); } + /** + * @ticket 59327 + * @ticket 59412 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_modifies_current_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'add_attribute_to_inner_block' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59669 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_modifies_current_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'add_attribute_to_inner_block' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function add_attribute_to_inner_block( &$block ) { + if ( 'core/inner' === $block['blockName'] ) { + $block['attrs']['myattr'] = 'myvalue'; + } + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_inner_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_inner_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_to_inner_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_inner_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_inner_block_callback( $block ) { + if ( 'core/inner' !== $block['blockName'] ) { + return ''; + } + + return get_comment_delimited_block_content( 'tests/inserted-block', array(), '' ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_to_child_blocks() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_to_child_blocks() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_child_blocks_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_child_blocks_callback( $block, $parent_block ) { + if ( ! isset( $parent_block ) ) { + return ''; + } + + return get_comment_delimited_block_content( + 'tests/inserted-block', + array( + 'parent' => $parent_block['blockName'], + ), + '' + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_pre_callback_prepends_if_prev_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_post_callback_appends_if_prev_block() { + $markup = "Example.\n\nExample.\n\n"; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( $blocks, null, array( __CLASS__, 'insert_next_to_if_prev_or_next_block_callback' ) ); + + $this->assertSame( + "Example.\n\nExample.\n\n", + $actual + ); + } + + public static function insert_next_to_if_prev_or_next_block_callback( $block, $parent_block, $prev_or_next ) { + if ( ! isset( $prev_or_next ) ) { + return ''; + } + + return get_comment_delimited_block_content( + 'tests/inserted-block', + array( + 'prev_or_next' => $prev_or_next['blockName'], + ), + '' + ); + } + + /** + * @ticket 59327 + * @ticket 59412 + * + * @covers ::traverse_and_serialize_blocks + * + * @dataProvider data_serialize_identity_from_parsed + * + * @param string $original Original block markup. + */ + public function test_traverse_and_serialize_identity_from_parsed( $original ) { + $blocks = parse_blocks( $original ); + + $actual = traverse_and_serialize_blocks( $blocks ); + + $this->assertSame( $original, $actual ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_do_not_insert_in_void_block() { + $markup = ''; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( + $blocks, + array( __CLASS__, 'insert_next_to_child_blocks_callback' ), + array( __CLASS__, 'insert_next_to_child_blocks_callback' ) + ); + + $this->assertSame( $markup, $actual ); + } + + /** + * @ticket 59313 + * + * @covers ::traverse_and_serialize_blocks + */ + public function test_traverse_and_serialize_blocks_do_not_insert_in_empty_parent_block() { + $markup = '
      '; + $blocks = parse_blocks( $markup ); + + $actual = traverse_and_serialize_blocks( + $blocks, + array( __CLASS__, 'insert_next_to_child_blocks_callback' ), + array( __CLASS__, 'insert_next_to_child_blocks_callback' ) + ); + + $this->assertSame( $markup, $actual ); + } } diff --git a/tests/phpunit/tests/blocks/setIgnoredHookedBlocksMetadata.php b/tests/phpunit/tests/blocks/setIgnoredHookedBlocksMetadata.php new file mode 100644 index 0000000000000..7145e56cdeef8 --- /dev/null +++ b/tests/phpunit/tests/blocks/setIgnoredHookedBlocksMetadata.php @@ -0,0 +1,183 @@ +type = 'wp_template'; + $template->theme = 'test-theme'; + $template->slug = 'single'; + $template->id = $template->theme . '//' . $template->slug; + $template->title = 'Single'; + $template->content = ''; + $template->description = 'Description of my template'; + + return $template; + } + + /** + * @ticket 60506 + * + * @covers ::set_ignored_hooked_blocks_metadata + */ + public function test_set_ignored_hooked_blocks_metadata() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + ); + + $hooked_blocks = array( + 'tests/anchor-block' => array( + 'after' => array( 'tests/hooked-block' ), + ), + ); + + set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, null ); + $this->assertSame( array( 'tests/hooked-block' ), $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ); + } + + /** + * @ticket 60506 + * + * @covers ::set_ignored_hooked_blocks_metadata + */ + public function test_set_ignored_hooked_blocks_metadata_retains_existing_items() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array( + 'metadata' => array( + 'ignoredHookedBlocks' => array( 'tests/other-ignored-block' ), + ), + ), + ); + + $hooked_blocks = array( + 'tests/anchor-block' => array( + 'after' => array( 'tests/hooked-block' ), + ), + ); + + set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, null ); + $this->assertSame( + array( 'tests/other-ignored-block', 'tests/hooked-block' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] + ); + } + + /** + * @ticket 60506 + * + * @covers ::set_ignored_hooked_blocks_metadata + */ + public function test_set_ignored_hooked_blocks_metadata_for_block_added_by_filter() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array(), + ); + + $hooked_blocks = array(); + + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type ) { + if ( 'tests/anchor-block' === $anchor_block_type && 'after' === $relative_position ) { + $hooked_block_types[] = 'tests/hooked-block-added-by-filter'; + } + + return $hooked_block_types; + }; + + add_filter( 'hooked_block_types', $filter, 10, 3 ); + set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, null ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( + array( 'tests/hooked-block-added-by-filter' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] + ); + } + + /** + * @ticket 60506 + * + * @covers ::set_ignored_hooked_blocks_metadata + */ + public function test_set_ignored_hooked_blocks_metadata_for_block_added_by_context_aware_filter() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array(), + ); + + $filter = function ( $hooked_block_types, $relative_position, $anchor_block_type, $context ) { + if ( + ! $context instanceof WP_Block_Template || + ! property_exists( $context, 'slug' ) || + 'single' !== $context->slug + ) { + return $hooked_block_types; + } + + if ( 'tests/anchor-block' === $anchor_block_type && 'after' === $relative_position ) { + $hooked_block_types[] = 'tests/hooked-block-added-by-filter'; + } + + return $hooked_block_types; + }; + + $template = self::create_block_template_object(); + + add_filter( 'hooked_block_types', $filter, 10, 4 ); + set_ignored_hooked_blocks_metadata( $anchor_block, 'after', array(), $template ); + remove_filter( 'hooked_block_types', $filter, 10 ); + + $this->assertSame( + array( 'tests/hooked-block-added-by-filter' ), + $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] + ); + } + + /** + * @ticket 60580 + * + * @covers ::set_ignored_hooked_blocks_metadata + */ + public function test_set_ignored_hooked_blocks_metadata_for_block_suppressed_by_filter() { + $anchor_block = array( + 'blockName' => 'tests/anchor-block', + 'attrs' => array(), + ); + + $hooked_blocks = array( + 'tests/anchor-block' => array( + 'after' => array( 'tests/hooked-block', 'tests/hooked-block-suppressed-by-filter' ), + ), + ); + + $filter = function ( $parsed_hooked_block, $hooked_block_type, $relative_position, $parsed_anchor_block ) { + if ( + 'tests/hooked-block-suppressed-by-filter' === $hooked_block_type && + 'after' === $relative_position && + 'tests/anchor-block' === $parsed_anchor_block['blockName'] + ) { + return null; + } + + return $parsed_hooked_block; + }; + + add_filter( 'hooked_block', $filter, 10, 4 ); + set_ignored_hooked_blocks_metadata( $anchor_block, 'after', $hooked_blocks, null ); + remove_filter( 'hooked_block', $filter ); + + $this->assertSame( array( 'tests/hooked-block' ), $anchor_block['attrs']['metadata']['ignoredHookedBlocks'] ); + } +} diff --git a/tests/phpunit/tests/blocks/supportedStyles.php b/tests/phpunit/tests/blocks/supportedStyles.php index bdbf91aa2eb13..a6fd8b598a28f 100644 --- a/tests/phpunit/tests/blocks/supportedStyles.php +++ b/tests/phpunit/tests/blocks/supportedStyles.php @@ -1,16 +1,10 @@ 'foo-bar-class', - 'style' => 'test: style;', + 'style' => 'margin-top: 2px;', ) ); return '
      ' . self::BLOCK_CONTENT . '
      '; } - /** - * Runs assertions that the rendered output has expected class/style attrs. - * - * @param array $block Block to render. - * @param string $expected_classes Expected output class attr string. - * @param string $expected_styles Expected output styles attr string. - */ - private function assert_styles_and_classes_match( $block, $expected_classes, $expected_styles ) { - $styled_block = $this->render_example_block( $block ); - $class_list = $this->get_attribute_from_block( 'class', $styled_block ); - $style_list = $this->get_attribute_from_block( 'style', $styled_block ); - - $this->assertSame( $expected_classes, $class_list, 'Class list does not match expected classes' ); - $this->assertSame( $expected_styles, $style_list, 'Style list does not match expected styles' ); - } - /** * Runs assertions that the rendered output has expected content and class/style attrs. * @@ -157,6 +135,24 @@ private function assert_content_and_styles_and_classes_match( $block, $expected_ ); } + /** + * Runs assertions that the rendered output has expected content and aria-label attr. + * + * @param array $block Block to render. + * @param string $expected_aria_label Expected output aria-label attr string. + */ + private function assert_content_and_aria_label_match( $block, $expected_aria_label ) { + $styled_block = $this->render_example_block( $block ); + $content = $this->get_content_from_block( $styled_block ); + + $this->assertSame( self::BLOCK_CONTENT, $content, 'Block content does not match expected content' ); + $this->assertSame( + $expected_aria_label, + $this->get_attribute_from_block( 'aria-label', $styled_block ), + 'Aria-label does not match expected aria-label' + ); + } + /** * Tests color support for named color support for named colors. */ @@ -175,7 +171,7 @@ public function test_named_color_support() { 'attrs' => array( 'textColor' => 'red', 'backgroundColor' => 'black', - // The following should not be applied (subcatagories of color support). + // The following should not be applied (subcategories of color support). 'gradient' => 'some-gradient', ), 'innerBlock' => array(), @@ -184,7 +180,7 @@ public function test_named_color_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-text-color has-red-color has-background has-black-background-color'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -209,7 +205,7 @@ public function test_custom_color_support() { 'color' => array( 'text' => '#000', 'background' => '#fff', - // The following should not be applied (subcatagories of color support). + // The following should not be applied (subcategories of color support). 'gradient' => 'some-gradient', 'style' => array( 'color' => array( 'link' => '#fff' ) ), ), @@ -220,7 +216,7 @@ public function test_custom_color_support() { 'innerHTML' => array(), ); - $expected_styles = 'test: style; color: #000; background-color: #fff;'; + $expected_styles = 'color:#000;background-color:#fff;margin-top: 2px'; $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -252,7 +248,7 @@ public function test_named_gradient_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-background has-red-gradient-background'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -283,7 +279,7 @@ public function test_custom_gradient_support() { ); $expected_classes = 'foo-bar-class wp-block-example has-background'; - $expected_styles = 'test: style; background: some-gradient-style;'; + $expected_styles = 'background:some-gradient-style;margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -319,7 +315,7 @@ public function test_color_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -349,7 +345,7 @@ public function test_named_font_size() { ); $expected_classes = 'foo-bar-class wp-block-example has-large-font-size'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -379,7 +375,7 @@ public function test_custom_font_size() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; font-size: 10px;'; + $expected_styles = 'font-size:10px;margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -406,7 +402,7 @@ public function test_font_size_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -436,7 +432,7 @@ public function test_line_height() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; line-height: 10;'; + $expected_styles = 'line-height:10;margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -462,7 +458,7 @@ public function test_line_height_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -490,7 +486,7 @@ public function test_block_alignment() { ); $expected_classes = 'foo-bar-class wp-block-example alignwide'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -516,7 +512,7 @@ public function test_block_alignment_unsupported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -563,7 +559,7 @@ public function test_all_supported() { ); $expected_classes = 'foo-bar-class wp-block-example has-text-color has-background alignwide'; - $expected_styles = 'test: style; color: #000; background-color: #fff; font-size: 10px; line-height: 20;'; + $expected_styles = 'color:#000;background-color:#fff;font-size:10px;line-height:20;margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -606,7 +602,7 @@ public function test_one_supported() { ); $expected_classes = 'foo-bar-class wp-block-example'; - $expected_styles = 'test: style; font-size: 10px;'; + $expected_styles = 'font-size:10px;margin-top: 2px'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } @@ -631,7 +627,7 @@ public function test_custom_classnames_support() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class wp-block-example my-custom-classname'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -659,7 +655,7 @@ public function test_custom_classnames_support_opt_out() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class wp-block-example'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); @@ -685,12 +681,37 @@ public function test_generated_classnames_support_opt_out() { 'innerHTML' => array(), ); - $expected_styles = 'test: style;'; + $expected_styles = 'margin-top: 2px'; $expected_classes = 'foo-bar-class'; $this->assert_content_and_styles_and_classes_match( $block, $expected_classes, $expected_styles ); } + /** + * Tests aria-label server-side block support. + */ + public function test_aria_label_support() { + $block_type_settings = array( + 'attributes' => array(), + 'supports' => array( + 'ariaLabel' => true, + ), + ); + $this->register_block_type( 'core/example', $block_type_settings ); + + $block = array( + 'blockName' => 'core/example', + 'attrs' => array( + 'ariaLabel' => 'Label', + ), + 'innerBlock' => array(), + 'innerContent' => array(), + 'innerHTML' => array(), + ); + + $this->assert_content_and_aria_label_match( $block, 'Label' ); + } + /** * Ensures libxml_internal_errors is being used instead of @ warning suppression */ @@ -701,13 +722,14 @@ public function test_render_block_suppresses_warnings_without_at_suppression() { ); $this->register_block_type( 'core/example', $block_type_settings ); - $block = array( + $block = array( 'blockName' => 'core/example', 'attrs' => array(), 'innerBlock' => array(), 'innerContent' => array(), 'innerHTML' => array(), ); + $wp_block = new WP_Block( $block ); // Custom error handler's see Warnings even if they are suppressed by the @ symbol. $errors = array(); @@ -720,10 +742,156 @@ static function ( $errno = 0, $errstr = '' ) use ( &$errors ) { // HTML5 elements like
      \s*', $calendar_html, 'Calendar is expected to use initials for day names' ); + $this->assertStringContainsString( '
      \s*]*>\s*([0-9.]*)#s', $response_body, $phpmatches ); - - $this->assertContains( $matches[1], $phpmatches[1], "readme.html's Recommended PHP version is too old. Remember to update the WordPress.org Requirements page, too." ); - - preg_match( '#Recommendations.*MySQL version ([0-9.]*)#s', $readme, $matches ); - - $response = wp_remote_get( "https://dev.mysql.com/doc/relnotes/mysql/{$matches[1]}/en/" ); - - $this->skipTestOnTimeout( $response ); - - $response_code = wp_remote_retrieve_response_code( $response ); - $response_body = wp_remote_retrieve_body( $response ); - - if ( 200 !== $response_code ) { - $error_message = sprintf( - 'Could not contact dev.MySQL.com to check versions. Response code: %s. Response body: %s', - $response_code, - $response_body - ); - - if ( 503 === $response_code ) { - $this->markTestSkipped( $error_message ); - } - - $this->fail( $error_message ); - } - - preg_match( '#(\d{4}-\d{2}-\d{2}), General Availability#', $response_body, $mysqlmatches ); - - // Per https://www.mysql.com/support/, Oracle actively supports MySQL releases for 5 years from GA release. - $mysql_eol = strtotime( $mysqlmatches[1] . ' +5 years' ); - - $this->assertLessThan( $mysql_eol, time(), "readme.html's Recommended MySQL version is too old. Remember to update the WordPress.org Requirements page, too." ); - } -} diff --git a/tests/phpunit/tests/feed/atom.php b/tests/phpunit/tests/feed/atom.php index 0b26c7cb57bbe..99d4e837afa4a 100644 --- a/tests/phpunit/tests/feed/atom.php +++ b/tests/phpunit/tests/feed/atom.php @@ -13,6 +13,9 @@ class Tests_Feed_Atom extends WP_UnitTestCase { public static $posts; public static $category; + private $post_count; + private $excerpt_only; + /** * Setup a new user and attribute some posts. */ @@ -51,6 +54,8 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { wp_set_object_terms( $post, self::$category->slug, 'category' ); } + // Assign a tagline option. + update_option( 'blogdescription', 'Just another WordPress site' ); } /** @@ -63,6 +68,13 @@ public function set_up() { $this->excerpt_only = get_option( 'rss_use_excerpt' ); } + /** + * Tear down. + */ + public static function wpTearDownAfterClass() { + delete_option( 'blogdescription' ); + } + /** * This is a bit of a hack used to buffer feed content. */ @@ -99,7 +111,6 @@ public function test_feed_element() { // Verify attributes. $this->assertSame( 'http://www.w3.org/2005/Atom', $atom[0]['attributes']['xmlns'] ); $this->assertSame( 'http://purl.org/syndication/thread/1.0', $atom[0]['attributes']['xmlns:thr'] ); - $this->assertSame( site_url( '/wp-atom.php' ), $atom[0]['attributes']['xml:base'] ); // Verify the element is present and contains a child element. $title = xml_find( $xml, 'feed', 'title' ); @@ -268,6 +279,8 @@ public function test_atom_enclosure_with_extended_url_length_type_parsing() { $entries = xml_find( $xml, 'feed', 'entry' ); $entries = array_slice( $entries, 0, 1 ); + $this->assertNotEmpty( $entries ); + foreach ( $entries as $key => $entry ) { $links = xml_find( $entries[ $key ]['child'], 'link' ); $i = 0; @@ -276,7 +289,7 @@ public function test_atom_enclosure_with_extended_url_length_type_parsing() { $this->assertSame( $enclosures[ $i ]['expected']['href'], $link['attributes']['href'] ); $this->assertEquals( $enclosures[ $i ]['expected']['length'], $link['attributes']['length'] ); $this->assertSame( $enclosures[ $i ]['expected']['type'], $link['attributes']['type'] ); - $i++; + ++$i; } } } diff --git a/tests/phpunit/tests/feed/fetchFeed.php b/tests/phpunit/tests/feed/fetchFeed.php new file mode 100644 index 0000000000000..c00d88756b481 --- /dev/null +++ b/tests/phpunit/tests/feed/fetchFeed.php @@ -0,0 +1,237 @@ +<?php +/** + * Tests for the fetch_feed() function. + * + * @package WordPress + * @subpackage UnitTests + * @since 6.7.0 + * + * @group feed + * + * @covers ::fetch_feed + */ +class Tests_Feed_FetchFeed extends WP_UnitTestCase { + + public function set_up() { + parent::set_up(); + + add_filter( 'pre_http_request', array( $this, 'mocked_rss_response' ) ); + } + + /** + * @ticket 62354 + */ + public function test_empty_charset_does_not_trigger_fatal_error() { + add_filter( 'pre_option_blog_charset', '__return_empty_string', 20 ); + + $feed = fetch_feed( 'https://wordpress.org/news/feed/' ); + + foreach ( $feed->get_items( 0, 1 ) as $item ) { + $content = $item->get_content(); + } + + $this->assertStringContainsString( '<a href="https://learn.wordpress.org/">Learn WordPress</a> is a learning resource providing workshops, quizzes, courses, lesson plans, and discussion groups so that anyone, from beginners to advanced users, can learn to do more with WordPress.', $content ); + } + + /** + * Ensure WP_Error object returned for 404 response. + * + * @ticket 64136 + */ + public function test_fetch_feed_returns_error_for_404_response() { + // Priority 15 to ensure this runs after the mocked_rss_response filter. + add_filter( 'pre_http_request', array( $this, 'mocked_rss_404_error_response' ), 15 ); + + $feed = fetch_feed( 'https://example.org/news/feed/' ); + + $this->assertWPError( $feed, 'A WP_Error object is expected for failing requests.' ); + $this->assertSame( 'simplepie-error', $feed->get_error_code() ); + } + + /** + * Ensure fetch_feed() returns WP_Error if any feed errors. + * + * @ticket 64136 + */ + public function test_fetch_feed_multiple_returns_error_if_any_feed_errors() { + // Priority 15 to ensure this runs after the mocked_rss_response filter. + add_filter( 'pre_http_request', array( $this, 'mocked_rss_404_error_response' ), 15 ); + add_filter( + 'pre_http_request', + /** + * Remove the 404 error response after the first call. + */ + function ( $response ) { + remove_filter( 'pre_http_request', array( $this, 'mocked_rss_404_error_response' ), 15 ); + + return $response; + }, + 20 // Priority 20 to ensure it runs after the 404 error response. + ); + + $feed = fetch_feed( array( 'https://example.org/news/feed/', 'https://wordpress.org/news/feed/' ) ); + + $this->assertWPError( $feed, 'A WP_Error object is expected for any failing requests.' ); + $this->assertSame( 'simplepie-error', $feed->get_error_code() ); + $this->assertCount( 1, $feed->get_error_messages()[0], 'There should be one error message for the failed feed.' ); + } + + /** + * Ensure fetch_feed() includes messages for all feeds that error. + * + * @ticket 64136 + */ + public function test_fetch_feed_multiple_returns_error_if_all_feeds_error() { + // Priority 15 to ensure this runs after the mocked_rss_response filter. + add_filter( 'pre_http_request', array( $this, 'mocked_rss_404_error_response' ), 15 ); + $feed = fetch_feed( array( 'https://example.org/news/feed/', 'https://example.com/news/feed/' ) ); + + $this->assertWPError( $feed, 'A WP_Error object is expected for failing requests.' ); + $this->assertSame( 'simplepie-error', $feed->get_error_code() ); + $this->assertCount( 2, $feed->get_error_messages()[0], 'There should be two error messages, one for each failed feed.' ); + } + + /** + * Ensure fetch_feed() returns a SimplePie object for an empty URL (string). + * + * @ticket 64136 + */ + public function test_fetch_feed_returns_a_simplepie_object_for_unspecified_url_string() { + $feed = fetch_feed( '' ); + + $this->assertInstanceOf( 'SimplePie\\SimplePie', $feed ); + } + + /** + * Ensure fetch_feed() returns a SimplePie object for an empty URL (array). + * + * @ticket 64136 + */ + public function test_fetch_feed_returns_a_simplepie_object_for_unspecified_url_array() { + $feed = fetch_feed( array() ); + + $this->assertInstanceOf( 'SimplePie\\SimplePie', $feed ); + } + + /** + * Ensure fetch_feed() accepts multiple feeds. + * + * The main purpose of this test is to ensure that the SimplePie deprecation warning + * is not thrown when requesting multiple feeds. + * + * Secondly it confirms that the markup of the first two items match as they will + * both be from the same feed URL as the array contains the WordPress News feed twice. + * + * @ticket 64136 + */ + public function test_fetch_feed_supports_multiple_feeds() { + $feed = fetch_feed( array( 'https://wordpress.org/news/feed/', 'https://wordpress.org/news/feed/atom/' ) ); + $content = array(); + + foreach ( $feed->get_items( 0, 2 ) as $item ) { + $content[] = $item->get_content(); + } + + $this->assertEqualHTML( $content[0], $content[1], null, 'The contents of the first two items should be identical.' ); + $this->assertCount( 20, $feed->get_items(), 'The feed should contain 20 items.' ); + } + + /** + * Ensure that fetch_feed() is cached on second and subsequent calls. + * + * Note: The HTTP request is mocked on the `pre_http_request` filter so + * this test doesn't make any HTTP requests so it doesn't need to be + * placed in the external-http group. + * + * @ticket 63717 + * + * @group feed + * + * @covers ::fetch_feed + */ + public function test_fetch_feed_cached() { + $filter = new MockAction(); + add_filter( 'pre_http_request', array( $filter, 'filter' ) ); + + fetch_feed( 'https://wordpress.org/news/feed/' ); + $this->assertSame( 1, $filter->get_call_count(), 'The feed should be fetched on the first call.' ); + + fetch_feed( 'https://wordpress.org/news/feed/' ); + $this->assertSame( 1, $filter->get_call_count(), 'The feed should be cached on the second call. For SP 1.8.x upgrades, backport simplepie/simplepie#830 to resolve.' ); + } + + /** + * Ensure that fetch_feed uses the global cache on Multisite. + * + * @ticket 63719 + * + * @group feed + * @group ms-required + * + * @covers ::fetch_feed + * @covers WP_Feed_Cache_Transient + */ + public function test_fetch_feed_uses_global_cache() { + $second_blog_id = self::factory()->blog->create(); + + $filter = new MockAction(); + add_filter( 'pre_http_request', array( $filter, 'filter' ) ); + + fetch_feed( 'https://wordpress.org/news/feed/' ); + + switch_to_blog( $second_blog_id ); + + fetch_feed( 'https://wordpress.org/news/feed/' ); + $this->assertSame( 1, $filter->get_call_count(), 'The feed cache should be global.' ); + } + + /** + * Mock response for `fetch_feed()`. + * + * This simulates a response from WordPress.org's server for the news feed + * to avoid making actual HTTP requests during the tests. + * + * The method runs on the `pre_http_request` filter, a low level filter + * to allow developers to determine whether a request would have been made + * under normal circumstances. + * + * @return array Mocked response data. + */ + public function mocked_rss_response() { + $single_value_headers = array( + 'Content-Type' => 'application/rss+xml; charset=UTF-8', + 'link' => '<https://wordpress.org/news/wp-json/>; rel="https://api.w.org/"', + ); + + return array( + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), + 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'cookies' => array(), + 'filename' => null, + ); + } + + /** + * Mock 404 error response for `fetch_feed()`. + * + * This simulates a 404 response to test error handling in `fetch_feed()`. + * + * @return array Mocked 404 error response data. + */ + public function mocked_rss_404_error_response() { + return array( + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary(), + 'body' => '', + 'response' => array( + 'code' => 404, + 'message' => 'Not Found', + ), + 'cookies' => array(), + 'filename' => null, + ); + } +} diff --git a/tests/phpunit/tests/feed/rss2.php b/tests/phpunit/tests/feed/rss2.php index 742c78f3d7494..c4f8807d7b7d3 100644 --- a/tests/phpunit/tests/feed/rss2.php +++ b/tests/phpunit/tests/feed/rss2.php @@ -14,6 +14,9 @@ class Tests_Feed_RSS2 extends WP_UnitTestCase { public static $category; public static $post_date; + private $post_count; + private $excerpt_only; + /** * Setup a new user and attribute some posts. */ @@ -58,6 +61,9 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { foreach ( self::$posts as $post ) { wp_set_object_terms( $post, self::$category->slug, 'category' ); } + + // Assign a tagline option. + update_option( 'blogdescription', 'Just another WordPress site' ); } /** @@ -75,6 +81,13 @@ public function set_up() { create_initial_taxonomies(); } + /** + * Tear down. + */ + public static function wpTearDownAfterClass() { + delete_option( 'blogdescription' ); + } + /** * This is a bit of a hack used to buffer feed content. */ @@ -233,7 +246,7 @@ public function test_item_elements() { } $cats = array_filter( $cats ); // Should be the same number of categories. - $this->assertSame( count( $cats ), count( $categories ) ); + $this->assertCount( count( $cats ), $categories ); // ..with the same names. foreach ( $cats as $id => $cat ) { @@ -276,6 +289,8 @@ public function test_items_comments_closed() { // Get all the rss -> channel -> item elements. $items = xml_find( $xml, 'rss', 'channel', 'item' ); + $this->assertNotEmpty( $items ); + // Check each of the items against the known post data. foreach ( $items as $key => $item ) { // Get post for comparison. @@ -473,7 +488,7 @@ public function test_valid_search_feed_endpoint() { * * @ticket 4575 * - * @dataProvider data_test_get_feed_build_date + * @dataProvider data_get_feed_build_date */ public function test_get_feed_build_date( $url, $element ) { $this->go_to( $url ); @@ -487,11 +502,131 @@ public function test_get_feed_build_date( $url, $element ) { } - public function data_test_get_feed_build_date() { + public function data_get_feed_build_date() { return array( array( '/?feed=rss2', 'rss' ), array( '/?feed=commentsrss2', 'rss' ), ); + } + + /** + * Test that the Last-Modified is a post's date when a more recent comment exists, + * but the "withcomments=1" query var is not passed. + * + * @ticket 47968 + * + * @covers WP::send_headers + */ + public function test_feed_last_modified_should_be_a_post_date_when_withcomments_is_not_passed() { + $last_week = gmdate( 'Y-m-d H:i:s', strtotime( '-1 week' ) ); + $yesterday = gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ); + + // Create a post dated last week. + $post_id = self::factory()->post->create( array( 'post_date' => $last_week ) ); + + // Create a comment dated yesterday. + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_date' => $yesterday, + ) + ); + + // The Last-Modified header should have the post's date when "withcomments" is not passed. + add_filter( + 'wp_headers', + function ( $headers ) use ( $last_week ) { + $this->assertSame( + strtotime( $headers['Last-Modified'] ), + strtotime( $last_week ), + 'Last-Modified was not the date of the post' + ); + return $headers; + } + ); + + $this->go_to( '/?feed=rss2' ); + } + + /** + * Test that the Last-Modified is a comment's date when a more recent comment exists, + * and the "withcomments=1" query var is passed. + * + * @ticket 47968 + * + * @covers WP::send_headers + */ + public function test_feed_last_modified_should_be_the_date_of_a_comment_that_is_the_latest_update_when_withcomments_is_passed() { + $last_week = gmdate( 'Y-m-d H:i:s', strtotime( '-1 week' ) ); + $yesterday = gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ); + + // Create a post dated last week. + $post_id = self::factory()->post->create( array( 'post_date' => $last_week ) ); + + // Create a comment dated yesterday. + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_date' => $yesterday, + ) + ); + + // The Last-Modified header should have the comment's date when "withcomments=1" is passed. + add_filter( + 'wp_headers', + function ( $headers ) use ( $yesterday ) { + $this->assertSame( + strtotime( $headers['Last-Modified'] ), + strtotime( $yesterday ), + 'Last-Modified was not the date of the comment' + ); + return $headers; + } + ); + + $this->go_to( '/?feed=rss2&withcomments=1' ); + } + + /** + * Test that the Last-Modified is the latest post's date when an earlier post and comment exist, + * and the "withcomments=1" query var is passed. + * + * @ticket 47968 + * + * @covers WP::send_headers + */ + public function test_feed_last_modified_should_be_the_date_of_a_post_that_is_the_latest_update_when_withcomments_is_passed() { + $last_week = gmdate( 'Y-m-d H:i:s', strtotime( '-1 week' ) ); + $yesterday = gmdate( 'Y-m-d H:i:s', strtotime( '-1 day' ) ); + $today = gmdate( 'Y-m-d H:i:s' ); + + // Create a post dated last week. + $post_id = self::factory()->post->create( array( 'post_date' => $last_week ) ); + + // Create a comment dated yesterday. + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_date' => $yesterday, + ) + ); + + // Create a post dated today. + self::factory()->post->create( array( 'post_date' => $today ) ); + + // The Last-Modified header should have the date from today's post when it is the latest update. + add_filter( + 'wp_headers', + function ( $headers ) use ( $today ) { + $this->assertSame( + strtotime( $headers['Last-Modified'] ), + strtotime( $today ), + 'Last-Modified was not the date of the most recent post' + ); + return $headers; + } + ); + $this->go_to( '/?feed=rss2&withcomments=1' ); } } diff --git a/tests/phpunit/tests/feed/rssEnclosure.php b/tests/phpunit/tests/feed/rssEnclosure.php new file mode 100644 index 0000000000000..89c6ab7971754 --- /dev/null +++ b/tests/phpunit/tests/feed/rssEnclosure.php @@ -0,0 +1,134 @@ +<?php + +/** + * Tests for the rss_enclosure() function. + * + * @group feed + * + * @covers ::rss_enclosure + */ +class Tests_Feed_RssEnclosure extends WP_UnitTestCase { + + /** + * @ticket 58798 + */ + public function test_rss_enclosure_filter() { + $post_id = self::factory()->post->create(); + $GLOBALS['post'] = $post_id; + + $valid_enclosure_string = "http://example.com/sound2.mp3\n12345\naudio/mpeg\n"; + + update_post_meta( $post_id, 'enclosure', $valid_enclosure_string ); + + add_filter( + 'rss_enclosure', + static function () { + return 'filtered_html_link_tag'; + } + ); + + $this->assertSame( 'filtered_html_link_tag', get_echo( 'rss_enclosure' ), 'The `rss_enclosure` filter could not be applied.' ); + } + + /** + * @ticket 58798 + */ + public function test_rss_enclosure_when_global_post_is_empty() { + $this->assertEmpty( get_echo( 'rss_enclosure' ), 'The output should be empty when the global post is not set.' ); + } + + /** + * @ticket 58798 + */ + public function test_rss_enclosure_when_enclosure_meta_field_is_empty() { + $post_id = self::factory()->post->create(); + $GLOBALS['post'] = $post_id; + + $this->assertEmpty( get_echo( 'rss_enclosure' ), 'The output should be empty when the global post does not have the `enclosure` meta field.' ); + } + + /** + * @ticket 58798 + * + * @dataProvider data_rss_enclosure_with_multiline_enclosure_string + */ + public function test_rss_enclosure_with_multiline_enclosure_string( $enclosure_data, $enclosure_string ) { + $post_id = self::factory()->post->create(); + $GLOBALS['post'] = $post_id; + + update_post_meta( $post_id, 'enclosure', $enclosure_string ); + + $expected = '<enclosure url="' . $enclosure_data['url'] . '" length="' . $enclosure_data['length'] . '" type="' . $enclosure_data['type'] . '" />' . "\n"; + + $this->assertSame( $expected, get_echo( 'rss_enclosure' ), 'The output should be a valid enclosure tag.' ); + } + + /** + * Data provider for valid enclosure string. + * + * @return array[] + */ + public function data_rss_enclosure_with_multiline_enclosure_string() { + return array( + 'two-break-lines' => array( + array( + 'url' => 'http://example.com/sound2.mp3', + 'length' => 12345, + 'type' => 'audio/mpeg', + ), + "http://example.com/sound2.mp3\n12345\naudio/mpeg", + ), + 'three-break-lines' => array( + array( + 'url' => 'http://example.com/sound2.mp3', + 'length' => 12345, + 'type' => 'audio/mpeg', + ), + "http://example.com/sound2.mp3\n12345\naudio/mpeg\n", + ), + 'extra-break-line-at-end' => array( + array( + 'url' => 'http://example.com/sound2.mp3', + 'length' => 12345, + 'type' => 'audio/mpeg', + ), + "http://example.com/sound2.mp3\n12345\naudio/mpeg\n\n", + ), + 'extra-type-elements' => array( + array( + 'url' => 'http://example.com/sound2.mp3', + 'length' => 12345, + 'type' => 'audio/mpeg', + ), + "http://example.com/sound2.mp3\n12345\naudio/mpeg mpga mp2 mp3\n", + ), + ); + } + + /** + * @ticket 58798 + * + * @dataProvider data_rss_enclosure_with_non_valid_enclosure_string + */ + public function test_rss_enclosure_with_non_valid_enclosure_string( $enclosure_string ) { + $post_id = self::factory()->post->create(); + $GLOBALS['post'] = $post_id; + + update_post_meta( $post_id, 'enclosure', $enclosure_string ); + + $this->assertEmpty( get_echo( 'rss_enclosure' ), 'The output should be empty when the `enclosure` meta field is not saved in a multiline string.' ); + } + + /** + * Data provider for non-valid enclosure string. + * + * @return array[] + */ + public function data_rss_enclosure_with_non_valid_enclosure_string() { + return array( + 'empty' => array( '' ), + 'no-break-lines' => array( 'http://example.com/sound2.mp3 12345 audio/mpeg' ), + 'one-break-line' => array( "http://example.com/sound2.mp3\n12345 audio/mpeg" ), + ); + } +} diff --git a/tests/phpunit/tests/feed/wpSimplePieFile.php b/tests/phpunit/tests/feed/wpSimplePieFile.php index f9748e3a7bcac..466e0df4be838 100644 --- a/tests/phpunit/tests/feed/wpSimplePieFile.php +++ b/tests/phpunit/tests/feed/wpSimplePieFile.php @@ -1,26 +1,20 @@ <?php /** - * Unit tests for methods in `WP_SimplePie_File`. + * Tests the `WP_SimplePie_File` class. * * @package WordPress * @subpackage UnitTests * @since 5.6.1 - */ - -/** - * Tests the `WP_SimplePie_File` class. * * @group feed * @group wp-simplepie-file - * - * @since 5.6.1 */ class Tests_Feed_wpSimplePieFile extends WP_UnitTestCase { public static function set_up_before_class() { parent::set_up_before_class(); - require_once ABSPATH . '/wp-includes/class-simplepie.php'; - require_once ABSPATH . '/wp-includes/class-wp-simplepie-file.php'; + require_once ABSPATH . 'wp-includes/class-simplepie.php'; + require_once ABSPATH . 'wp-includes/class-wp-simplepie-file.php'; } /** @@ -85,7 +79,7 @@ public function mocked_response_single_header_values() { ); return array( - 'headers' => new Requests_Utility_CaseInsensitiveDictionary( $single_value_headers ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), 'response' => array( 'code' => 200, @@ -114,7 +108,7 @@ public function mocked_response_multiple_header_values() { ), ); - $response['headers'] = new Requests_Utility_CaseInsensitiveDictionary( $multiple_value_headers ); + $response['headers'] = new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $multiple_value_headers ); return $response; } diff --git a/tests/phpunit/tests/file.php b/tests/phpunit/tests/file.php index eb6168d9af5cd..9a29e4e67669a 100644 --- a/tests/phpunit/tests/file.php +++ b/tests/phpunit/tests/file.php @@ -5,12 +5,14 @@ */ class Tests_File extends WP_UnitTestCase { + const BADCHARS = '"\'[]*&?$'; + + private $dir; + public function set_up() { parent::set_up(); $this->dir = untrailingslashit( get_temp_dir() ); - - $this->badchars = '"\'[]*&?$'; } /** @@ -38,6 +40,8 @@ public function test_get_file_data() { 'AuthorURI' => 'http://binarybonsai.com/', ); + $this->assertNotEmpty( $actual ); + foreach ( $actual as $header => $value ) { $this->assertSame( $expected[ $header ], $value, $header ); } @@ -62,6 +66,8 @@ public function test_get_file_data_with_cr_line_endings() { 'Author' => 'A Very Old Mac', ); + $this->assertNotEmpty( $actual ); + foreach ( $actual as $header => $value ) { $this->assertSame( $expected[ $header ], $value, $header ); } @@ -82,6 +88,8 @@ public function test_get_file_data_with_php_open_tag_prefix() { 'TemplateName' => 'Something', ); + $this->assertNotEmpty( $actual ); + foreach ( $actual as $header => $value ) { $this->assertSame( $expected[ $header ], $value, $header ); } @@ -96,8 +104,8 @@ private function is_unique_writable_file( $path, $filename ) { return false; } - // Write some random contents. - $c = rand_str(); + // Write some contents. + $c = 'foo'; fwrite( $fp, $c ); fclose( $fp ); @@ -137,7 +145,7 @@ public function test_unique_filename_is_unique() { public function test_unique_filename_is_sanitized() { $name = __FUNCTION__; - $filename = wp_unique_filename( $this->dir, $name . $this->badchars . '.txt' ); + $filename = wp_unique_filename( $this->dir, $name . self::BADCHARS . '.txt' ); // Make sure the bad characters were all stripped out. $this->assertSame( $name . '.txt', $filename ); @@ -186,8 +194,8 @@ public function test_unique_filename_no_ext() { /** * @dataProvider data_wp_tempnam_filenames */ - public function test_wp_tempnam( $case ) { - $file = wp_tempnam( $case ); + public function test_wp_tempnam( $filename ) { + $file = wp_tempnam( $filename ); unlink( $file ); $this->assertNotEmpty( basename( basename( $file, '.tmp' ), '.zip' ) ); @@ -203,6 +211,199 @@ public function data_wp_tempnam_filenames() { ); } + /** + * Tests that `wp_tempnam()` limits the filename's length to 252 characters. + * + * @ticket 35755 + * + * @covers ::wp_tempnam + * + * @dataProvider data_wp_tempnam_should_limit_filename_length_to_252_characters + */ + public function test_wp_tempnam_should_limit_filename_length_to_252_characters( $filename ) { + $file = wp_tempnam( $filename ); + + if ( file_exists( $file ) ) { + self::unlink( $file ); + } + + $this->assertLessThanOrEqual( 252, strlen( basename( $file ) ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_tempnam_should_limit_filename_length_to_252_characters() { + return array( + 'the limit before adding characters for uniqueness' => array( 'filename' => str_pad( '', 241, 'filename' ) ), + 'one more than the limit before adding characters for uniqueness' => array( 'filename' => str_pad( '', 242, 'filename' ) ), + '251 characters' => array( 'filename' => str_pad( '', 251, 'filename' ) ), + '252 characters' => array( 'filename' => str_pad( '', 252, 'filename' ) ), + '253 characters' => array( 'filename' => str_pad( '', 253, 'filename' ) ), + ); + } + + /** + * Tests that `wp_tempnam()` limits the filename's length to 252 characters + * when there is a name conflict. + * + * @ticket 35755 + * + * @covers ::wp_tempnam + */ + public function test_wp_tempnam_should_limit_filename_length_to_252_characters_with_name_conflict() { + // Create a conflict by removing the randomness of the generated password. + add_filter( + 'random_password', + static function () { + return '123456'; + }, + 10, + 0 + ); + + // A filename at the limit. + $filename = str_pad( '', 252, 'filename' ); + + // Create the initial file. + $existing_file = wp_tempnam( $filename ); + + // Try creating a file with the same name. + $actual = wp_tempnam( basename( $existing_file ) ); + + self::unlink( $existing_file ); + self::unlink( $actual ); + + $this->assertLessThanOrEqual( 252, strlen( basename( $actual ) ) ); + } + + /** + * Tests that `wp_tempnam()` limits the filename's length to 252 characters + * when a 'random_password' filter returns passwords longer than 6 characters. + * + * @ticket 35755 + * + * @covers ::wp_tempnam + */ + public function test_wp_tempnam_should_limit_filename_length_to_252_characters_when_random_password_is_filtered() { + // Force random passwords to 12 characters. + add_filter( + 'random_password', + static function () { + return '1a2b3c4d5e6f'; + }, + 10, + 0 + ); + + // A filename at the limit. + $filename = str_pad( '', 252, 'filename' ); + $actual = wp_tempnam( $filename ); + + self::unlink( $actual ); + + $this->assertLessThanOrEqual( 252, strlen( basename( $actual ) ) ); + } + + /** + * Tests that `wp_tempnam()` limits the filename's length to 252 characters + * when a 'wp_unique_filename' filter returns a filename longer than 252 characters. + * + * @ticket 35755 + * + * @covers ::wp_tempnam + */ + public function test_wp_tempnam_should_limit_filename_length_to_252_characters_when_wp_unique_filename_is_filtered() { + // Determine the number of additional characters added by `wp_tempnam()`. + $temp_dir = get_temp_dir(); + $additional_chars_filename = wp_unique_filename( $temp_dir, 'filename' ); + $additional_chars_generated = wp_tempnam( $additional_chars_filename, $temp_dir ); + $additional_chars_difference = strlen( basename( $additional_chars_generated ) ) - strlen( $additional_chars_filename ); + + $filenames_over_limit = 0; + + // Make the filter send the filename over the limit. + add_filter( + 'wp_unique_filename', + static function ( $filename ) use ( &$filenames_over_limit ) { + if ( strlen( $filename ) === 252 ) { + $filename .= '1'; + ++$filenames_over_limit; + } + + return $filename; + }, + 10, + 1 + ); + + // A filename that will hit the limit when `wp_tempnam()` adds characters. + $filename = str_pad( '', 252 - $additional_chars_difference, 'filename' ); + $actual = wp_tempnam( $filename ); + + self::unlink( $additional_chars_generated ); + self::unlink( $actual ); + + $this->assertLessThanOrEqual( 252, strlen( basename( $actual ) ), 'The final filename was over the limit.' ); + $this->assertSame( 1, $filenames_over_limit, 'One filename should have been over the limit.' ); + } + + /** + * Tests that `wp_tempnam()` limits the filename's length to 252 characters + * when both a 'random_password' filter and a 'wp_unique_filename' filter + * cause the filename to be greater than 252 characters. + * + * @ticket 35755 + * + * @covers ::wp_tempnam + */ + public function test_wp_tempnam_should_limit_filename_length_to_252_characters_when_random_password_and_wp_unique_filename_are_filtered() { + // Force random passwords to 12 characters. + add_filter( + 'random_password', + static function () { + return '1a2b3c4d5e6f'; + }, + 10, + 0 + ); + + // Determine the number of additional characters added by `wp_tempnam()`. + $temp_dir = get_temp_dir(); + $additional_chars_filename = wp_unique_filename( $temp_dir, 'filename' ); + $additional_chars_generated = wp_tempnam( $additional_chars_filename, $temp_dir ); + $additional_chars_difference = strlen( basename( $additional_chars_generated ) ) - strlen( $additional_chars_filename ); + + $filenames_over_limit = 0; + + // Make the filter send the filename over the limit. + add_filter( + 'wp_unique_filename', + static function ( $filename ) use ( &$filenames_over_limit ) { + if ( strlen( $filename ) === 252 ) { + $filename .= '1'; + ++$filenames_over_limit; + } + + return $filename; + }, + 10, + 1 + ); + + // A filename that will hit the limit when `wp_tempnam()` adds characters. + $filename = str_pad( '', 252 - $additional_chars_difference, 'filename' ); + $actual = wp_tempnam( $filename ); + + self::unlink( $additional_chars_generated ); + self::unlink( $actual ); + + $this->assertLessThanOrEqual( 252, strlen( basename( $actual ) ), 'The final filename was over the limit.' ); + $this->assertSame( 1, $filenames_over_limit, 'One filename should have been over the limit.' ); + } + /** * @ticket 47186 */ diff --git a/tests/phpunit/tests/filesystem/base.php b/tests/phpunit/tests/filesystem/base.php index 52ab59e3cdf6e..ceefeafa3e5c7 100644 --- a/tests/phpunit/tests/filesystem/base.php +++ b/tests/phpunit/tests/filesystem/base.php @@ -24,7 +24,7 @@ public function filter_fs_method( $method ) { return 'MockFS'; } public function filter_abstraction_file( $file ) { - return dirname( dirname( __DIR__ ) ) . '/includes/mock-fs.php'; + return dirname( __DIR__, 2 ) . '/includes/mock-fs.php'; } public function test_is_MockFS_sane() { diff --git a/tests/phpunit/tests/filesystem/copyDir.php b/tests/phpunit/tests/filesystem/copyDir.php new file mode 100644 index 0000000000000..ff80273c8afb2 --- /dev/null +++ b/tests/phpunit/tests/filesystem/copyDir.php @@ -0,0 +1,83 @@ +<?php + +/** + * Tests copy_dir(). + * + * @group file + * @group filesystem + * + * @covers ::copy_dir + */ +class Tests_Filesystem_CopyDir extends WP_UnitTestCase { + + /** + * The test directory. + * + * @var string $test_dir + */ + private static $test_dir; + + /** + * Sets up the filesystem and test directory before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + + self::$test_dir = get_temp_dir() . 'copy_dir/'; + } + + /** + * Sets up the test directory before each test. + */ + public function set_up() { + global $wp_filesystem; + + parent::set_up(); + + // Create the root directory. + $wp_filesystem->mkdir( self::$test_dir ); + } + + /** + * Removes the test directory after each test. + */ + public function tear_down() { + global $wp_filesystem; + + // Delete the root directory and its contents. + $wp_filesystem->delete( self::$test_dir, true ); + + parent::tear_down(); + } + + /** + * Tests that the destination is created if it does not already exist. + * + * @ticket 41855 + */ + public function test_should_create_destination_it_if_does_not_exist() { + global $wp_filesystem; + + $from = self::$test_dir . 'folder1/folder2/'; + $to = self::$test_dir . 'folder3/folder2/'; + + // Create the file structure for the test. + $wp_filesystem->mkdir( self::$test_dir . 'folder1' ); + $wp_filesystem->mkdir( self::$test_dir . 'folder3' ); + $wp_filesystem->mkdir( $from ); + $wp_filesystem->touch( $from . 'file1.txt' ); + $wp_filesystem->mkdir( $from . 'subfolder1' ); + $wp_filesystem->touch( $from . 'subfolder1/file2.txt' ); + + $this->assertTrue( copy_dir( $from, $to ), 'copy_dir() failed.' ); + + $this->assertDirectoryExists( $to, 'The destination was not created.' ); + $this->assertFileExists( $to . 'file1.txt', 'The destination file was not created.' ); + + $this->assertDirectoryExists( $to . 'subfolder1/', 'The destination subfolder was not created.' ); + $this->assertFileExists( $to . 'subfolder1/file2.txt', 'The destination subfolder file was not created.' ); + } +} diff --git a/tests/phpunit/tests/filesystem/findFolder.php b/tests/phpunit/tests/filesystem/findFolder.php index 3db6107585787..e2be834792212 100644 --- a/tests/phpunit/tests/filesystem/findFolder.php +++ b/tests/phpunit/tests/filesystem/findFolder.php @@ -3,8 +3,8 @@ require_once __DIR__ . '/base.php'; /** + * @group file * @group filesystem - * @group wp-filesystem */ class WP_Filesystem_Find_Folder_Test extends WP_Filesystem_UnitTestCase { @@ -24,7 +24,6 @@ public function test_ftp_has_root_access() { $path = $fs->find_folder( '/this/directory/doesnt/exist/' ); $this->assertFalse( $path ); - } public function test_sibling_wordpress_in_subdir() { @@ -48,7 +47,6 @@ public function test_sibling_wordpress_in_subdir() { $path = $fs->find_folder( '/var/www/wp.example.com/wordpress/wp-content/' ); $this->assertSame( '/www/wp.example.com/wordpress/wp-content/', $path ); - } /** @@ -76,7 +74,6 @@ public function test_subdir_of_another() { $path = $fs->abspath( '/var/www/example.com/' ); $this->assertSame( '/', $path ); - } /** @@ -93,7 +90,7 @@ public function test_multiple_tokens_in_path1() { /example.com/www/index.php /example.com/www/wp-includes/ /example.com/www/wp-content/plugins/ - + # sub.example.com /example.com/sub/index.php /example.com/sub/wp-includes/ @@ -113,5 +110,4 @@ public function test_multiple_tokens_in_path1() { $path = $fs->find_folder( '/var/www/example.com/sub/wp-content/plugins/' ); $this->assertSame( '/example.com/sub/wp-content/plugins/', $path ); } - } diff --git a/tests/phpunit/tests/filesystem/moveDir.php b/tests/phpunit/tests/filesystem/moveDir.php new file mode 100644 index 0000000000000..7def36a5b218d --- /dev/null +++ b/tests/phpunit/tests/filesystem/moveDir.php @@ -0,0 +1,312 @@ +<?php + +/** + * Tests move_dir(). + * + * @group file + * @group filesystem + * + * @covers ::move_dir + */ +class Tests_Filesystem_MoveDir extends WP_UnitTestCase { + + /** + * The test directory. + * + * @var string $test_dir + */ + private static $test_dir; + + /** + * The existing 'from' directory path. + * + * @var string $existing_from + */ + private static $existing_from; + + /** + * The existing 'from' sub-directory path. + * + * @var string $existing_from_subdir + */ + private static $existing_from_subdir; + + /** + * The existing 'from' file path. + * + * @var string $existing_from_file + */ + private static $existing_from_file; + + /** + * The existing 'from' sub-directory file path. + * + * @var string $existing_from_subdir_file + */ + private static $existing_from_subdir_file; + + /** + * The existing 'to' directory file path. + * + * @var string $existing_to + */ + private static $existing_to; + + /** + * The existing 'to' file path. + * + * @var string $existing_to_file + */ + private static $existing_to_file; + + /** + * Sets up the filesystem and directory structure properties + * before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + + self::$test_dir = get_temp_dir() . 'move_dir/'; + self::$existing_from = self::$test_dir . 'existing_from/'; + self::$existing_from_subdir = self::$existing_from . 'existing_from_subdir/'; + self::$existing_from_file = self::$existing_from . 'existing_from_file.txt'; + self::$existing_from_subdir_file = self::$existing_from_subdir . 'existing_from_subdir_file.txt'; + self::$existing_to = self::$test_dir . 'existing_to/'; + self::$existing_to_file = self::$existing_to . 'existing_to_file.txt'; + } + + /** + * Sets up the directory structure before each test. + */ + public function set_up() { + global $wp_filesystem; + + parent::set_up(); + + // Create the root directory. + $wp_filesystem->mkdir( self::$test_dir ); + + // Create the "from" directory structure. + $wp_filesystem->mkdir( self::$existing_from ); + $wp_filesystem->touch( self::$existing_from_file ); + $wp_filesystem->mkdir( self::$existing_from_subdir ); + $wp_filesystem->touch( self::$existing_from_subdir_file ); + + // Create the "to" directory structure. + $wp_filesystem->mkdir( self::$existing_to ); + $wp_filesystem->touch( self::$existing_to_file ); + } + + /** + * Removes the test directory structure after each test. + */ + public function tear_down() { + global $wp_filesystem; + + // Delete the root directory and its contents. + $wp_filesystem->delete( self::$test_dir, true ); + + parent::tear_down(); + } + + /** + * Tests that move_dir() returns a WP_Error object. + * + * @ticket 57375 + * + * @dataProvider data_should_return_wp_error + * + * @param string $from The source directory path. + * @param string $to The destination directory path. + * @param bool $overwrite Whether to overwrite the destination directory. + * @param string $expected The expected WP_Error code. + */ + public function test_should_return_wp_error( $from, $to, $overwrite, $expected ) { + global $wp_filesystem; + + $from = self::$test_dir . $from; + $to = self::$test_dir . $to; + $result = move_dir( $from, $to, $overwrite ); + + $this->assertWPError( + $result, + 'move_dir() did not return a WP_Error object.' + ); + + $this->assertSame( + $expected, + $result->get_error_code(), + 'The expected error code was not returned.' + ); + + if ( 'source_destination_same_move_dir' !== $expected ) { + $this->assertTrue( + $wp_filesystem->exists( $from ), + 'The $from directory does not exist anymore.' + ); + + if ( false === $overwrite && 'existing_to' === untrailingslashit( $to ) ) { + $this->assertTrue( + $wp_filesystem->exists( $to ), + 'The $to directory does not exist anymore.' + ); + } + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_return_wp_error() { + return array( + '$overwrite is false and $to exists' => array( + 'from' => 'existing_from', + 'to' => 'existing_to', + 'overwrite' => false, + 'expected' => 'destination_already_exists_move_dir', + ), + 'same source and destination, source has trailing slash' => array( + 'from' => 'existing_from/', + 'to' => 'existing_from', + 'overwrite' => false, + 'expected' => 'source_destination_same_move_dir', + ), + 'same source and destination, destination has trailing slash' => array( + 'from' => 'existing_from', + 'to' => 'existing_from/', + 'overwrite' => false, + 'expected' => 'source_destination_same_move_dir', + ), + 'same source and destination, source lowercase, destination uppercase' => array( + 'from' => 'existing_from', + 'to' => 'EXISTING_FROM', + 'overwrite' => false, + 'expected' => 'source_destination_same_move_dir', + ), + 'same source and destination, source uppercase, destination lowercase' => array( + 'from' => 'EXISTING_FROM', + 'to' => 'existing_from', + 'overwrite' => false, + 'expected' => 'source_destination_same_move_dir', + ), + 'same source and destination, source and destination in inverted case' => array( + 'from' => 'ExIsTiNg_FrOm', + 'to' => 'eXiStInG_fRoM', + 'overwrite' => false, + 'expected' => 'source_destination_same_move_dir', + ), + ); + } + + /** + * Tests that move_dir() successfully moves a directory. + * + * @ticket 57375 + * + * @dataProvider data_should_move_directory + * + * @param string $from The source directory path. + * @param string $to The destination directory path. + * @param bool $overwrite Whether to overwrite the destination directory. + */ + public function test_should_move_directory( $from, $to, $overwrite ) { + global $wp_filesystem; + + $from = self::$test_dir . $from; + $to = self::$test_dir . $to; + $result = move_dir( $from, $to, $overwrite ); + + $this->assertTrue( + $result, + 'The directory was not moved.' + ); + + $this->assertFalse( + $wp_filesystem->exists( $from ), + 'The source directory still exists.' + ); + + $this->assertTrue( + $wp_filesystem->exists( $to ), + 'The destination directory does not exist.' + ); + + $dirlist = $wp_filesystem->dirlist( $to, true, true ); + + // Prevent PHP array sorting bugs from breaking tests. + $to_contents = array_keys( $dirlist ); + + $this->assertSameSets( + array( + 'existing_from_file.txt', + 'existing_from_subdir', + ), + $to_contents, + 'The expected files were not moved.' + ); + + $this->assertSame( + array( 'existing_from_subdir_file.txt' ), + array_keys( $dirlist['existing_from_subdir']['files'] ), + 'Sub-directory files failed to move.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_move_directory() { + return array( + '$overwrite is false and $to does not exist' => array( + 'from' => 'existing_from', + 'to' => 'non_existing_to', + 'overwrite' => false, + ), + '$overwrite is true and $to exists' => array( + 'from' => 'existing_from', + 'to' => 'existing_to', + 'overwrite' => true, + ), + ); + } + + /** + * Tests that `move_dir()` returns a WP_Error object when overwriting + * is enabled, the destination exists, but cannot be deleted. + * + * @ticket 57375 + */ + public function test_should_return_wp_error_when_overwriting_is_enabled_the_destination_exists_but_cannot_be_deleted() { + global $wp_filesystem; + $wpfilesystem_backup = $wp_filesystem; + + // Force failure conditions. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' )->setConstructorArgs( array( null ) )->getMock(); + $filesystem_mock->expects( $this->once() )->method( 'exists' )->willReturn( true ); + $filesystem_mock->expects( $this->once() )->method( 'delete' )->willReturn( false ); + $wp_filesystem = $filesystem_mock; + + $actual = move_dir( self::$existing_from, self::$existing_from_subdir, true ); + + // Restore the filesystem. + $wp_filesystem = $wpfilesystem_backup; + + $this->assertWPError( + $actual, + 'A WP_Error object was not returned.' + ); + + $this->assertSame( + 'destination_not_deleted_move_dir', + $actual->get_error_code(), + 'An unexpected error code was returned.' + ); + } +} diff --git a/tests/phpunit/tests/filesystem/unzipFilePclzip.php b/tests/phpunit/tests/filesystem/unzipFilePclzip.php new file mode 100644 index 0000000000000..10e4b1082a794 --- /dev/null +++ b/tests/phpunit/tests/filesystem/unzipFilePclzip.php @@ -0,0 +1,86 @@ +<?php + +/** + * Tests _unzip_file_pclzip(). + * + * @group file + * @group filesystem + * + * @covers ::_unzip_file_pclzip + */ +class Tests_Filesystem_UnzipFilePclzip extends WP_UnitTestCase { + + /** + * The test data directory. + * + * @var string $test_data_dir + */ + private static $test_data_dir; + + /** + * Sets up the filesystem and test data directory property + * before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + + self::$test_data_dir = DIR_TESTDATA . '/filesystem/'; + } + + /** + * Tests that _unzip_file_pclzip() applies "pre_unzip_file" filters. + * + * @ticket 37719 + */ + public function test_should_apply_pre_unzip_file_filters() { + $filter = new MockAction(); + add_filter( 'pre_unzip_file', array( $filter, 'filter' ), 10, 2 ); + + // Prepare test environment. + $unzip_destination = self::$test_data_dir . 'archive/'; + if ( file_exists( $unzip_destination ) ) { + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + } + mkdir( $unzip_destination ); + + _unzip_file_pclzip( self::$test_data_dir . 'archive.zip', $unzip_destination ); + + // Clean up test environment. + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + + $this->assertSame( 1, $filter->get_call_count(), 'The filter should be called once.' ); + $this->assertSame( self::$test_data_dir . 'archive.zip', $filter->get_args()[0][1], 'The $file parameter should be correct.' ); + } + + /** + * Tests that _unzip_file_pclzip() applies "unzip_file" filters. + * + * @ticket 37719 + */ + public function test_should_apply_unzip_file_filters() { + $filter = new MockAction(); + add_filter( 'unzip_file', array( $filter, 'filter' ), 10, 2 ); + + // Prepare test environment. + $unzip_destination = self::$test_data_dir . 'archive/'; + if ( file_exists( $unzip_destination ) ) { + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + } + mkdir( $unzip_destination ); + + _unzip_file_pclzip( self::$test_data_dir . 'archive.zip', $unzip_destination ); + + // Clean up test environment. + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + + $this->assertSame( 1, $filter->get_call_count(), 'The filter should be called once.' ); + $this->assertSame( self::$test_data_dir . 'archive.zip', $filter->get_args()[0][1], 'The $file parameter should be correct.' ); + } +} diff --git a/tests/phpunit/tests/filesystem/unzipFileZiparchive.php b/tests/phpunit/tests/filesystem/unzipFileZiparchive.php new file mode 100644 index 0000000000000..eba5d1407c394 --- /dev/null +++ b/tests/phpunit/tests/filesystem/unzipFileZiparchive.php @@ -0,0 +1,92 @@ +<?php + +/** + * Tests _unzip_file_ziparchive(). + * + * @group file + * @group filesystem + * + * @covers ::_unzip_file_ziparchive + */ +class Tests_Filesystem_UnzipFileZiparchive extends WP_UnitTestCase { + + /** + * The test data directory. + * + * @var string $test_data_dir + */ + private static $test_data_dir; + + /** + * Sets up the filesystem and test data directory property + * before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + + self::$test_data_dir = DIR_TESTDATA . '/filesystem/'; + } + + /** + * Tests that _unzip_file_ziparchive() applies "pre_unzip_file" filters. + * + * @ticket 37719 + */ + public function test_should_apply_pre_unzip_file_filters() { + if ( ! class_exists( 'ZipArchive' ) ) { + $this->markTestSkipped( 'This test requires the ZipArchive class.' ); + } + + $filter = new MockAction(); + add_filter( 'pre_unzip_file', array( $filter, 'filter' ) ); + + // Prepare test environment. + $unzip_destination = self::$test_data_dir . 'archive/'; + if ( file_exists( $unzip_destination ) ) { + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + } + mkdir( $unzip_destination ); + + _unzip_file_ziparchive( self::$test_data_dir . 'archive.zip', $unzip_destination ); + + // Clean up test environment. + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Tests that _unzip_file_ziparchive() applies "unzip_file" filters. + * + * @ticket 37719 + */ + public function test_should_apply_unzip_file_filters() { + if ( ! class_exists( 'ZipArchive' ) ) { + $this->markTestSkipped( 'This test requires the ZipArchive class.' ); + } + + $filter = new MockAction(); + add_filter( 'unzip_file', array( $filter, 'filter' ) ); + + // Prepare test environment. + $unzip_destination = self::$test_data_dir . 'archive/'; + if ( file_exists( $unzip_destination ) ) { + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + } + mkdir( $unzip_destination ); + + _unzip_file_ziparchive( self::$test_data_dir . 'archive.zip', $unzip_destination ); + + // Clean up test environment. + $this->rmdir( $unzip_destination ); + $this->delete_folders( $unzip_destination ); + + $this->assertSame( 1, $filter->get_call_count() ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/atime.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/atime.php new file mode 100644 index 0000000000000..c6f57780c01c5 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/atime.php @@ -0,0 +1,50 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::atime() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::atime + */ +class Tests_Filesystem_WpFilesystemDirect_Atime extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::atime()` + * returns an integer for a path that exists. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path. + */ + public function test_should_determine_accessed_time( $path ) { + $path = self::$file_structure['test_dir']['path'] . $path; + + $this->assertIsInt( self::$filesystem->atime( $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::atime()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_false_for_a_path_that_does_not_exist( $path ) { + $path = self::$file_structure['test_dir']['path'] . $path; + + $this->assertFalse( self::$filesystem->atime( $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/base.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/base.php new file mode 100644 index 0000000000000..96b449dd0306a --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/base.php @@ -0,0 +1,197 @@ +<?php + +/** + * Base class for WP_Filesystem_Direct tests. + * + * @package WordPress + */ +abstract class WP_Filesystem_Direct_UnitTestCase extends WP_UnitTestCase { + + /** + * The filesystem object. + * + * @var WP_Filesystem_Direct + */ + protected static $filesystem; + + /** + * The file structure for tests. + * + * @var array + */ + protected static $file_structure = array(); + + /** + * Sets up test assets before the class. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-base.php'; + require_once ABSPATH . 'wp-admin/includes/class-wp-filesystem-direct.php'; + + self::$filesystem = new WP_Filesystem_Direct( null ); + + $filesystem_data_dir = wp_normalize_path( DIR_TESTDATA . '/filesystem/' ); + if ( ! file_exists( $filesystem_data_dir ) ) { + mkdir( $filesystem_data_dir ); + } + + /* + * These must be created during the tests as they may be modified or deleted + * during testing, either intentionally or accidentally as a result of test failure. + */ + $test_data_root_dir = $filesystem_data_dir . 'filesystem_api/'; + $test_data_dir = $test_data_root_dir . 'wpFilesystemDirect/'; + + self::$file_structure = array( + // Directories first. + 'test_dir_root' => array( + 'type' => 'd', + 'path' => $test_data_root_dir, + ), + 'test_dir' => array( + 'type' => 'd', + 'path' => $test_data_dir, + ), + 'subdir' => array( + 'type' => 'd', + 'path' => $test_data_dir . 'subdir/', + ), + + // Then files. + 'visible_file' => array( + 'type' => 'f', + 'path' => $test_data_dir . 'a_file_that_exists.txt', + 'contents' => "Contents of a file.\r\nNext line of a file.\r\n", + ), + 'hidden_file' => array( + 'type' => 'f', + 'path' => $test_data_dir . '.a_hidden_file', + 'contents' => "A hidden file.\r\n", + ), + 'subfile' => array( + 'type' => 'f', + 'path' => $test_data_dir . 'subdir/subfile.txt', + 'contents' => "A file in a subdirectory.\r\n", + ), + ); + } + + /** + * Creates any missing test assets before each test. + */ + public function set_up() { + parent::set_up(); + + foreach ( self::$file_structure as $entry ) { + if ( 'd' === $entry['type'] ) { + $this->create_directory_if_needed( $entry['path'] ); + } elseif ( 'f' === $entry['type'] ) { + $this->create_file_if_needed( + $entry['path'], + $entry['contents'] ?? '' + ); + } + } + } + + /** + * Removes any existing test assets after each test. + */ + public function tear_down() { + foreach ( array_reverse( self::$file_structure ) as $entry ) { + if ( ! file_exists( $entry['path'] ) ) { + continue; + } + + if ( 'f' === $entry['type'] ) { + unlink( $entry['path'] ); + } elseif ( 'd' === $entry['type'] ) { + rmdir( $entry['path'] ); + } + } + + parent::tear_down(); + } + + /** + * Creates a directory if it doesn't already exist. + * + * @throws Exception If the path already exists as a file. + * + * @param string $path The path to the directory. + */ + public function create_directory_if_needed( $path ) { + if ( file_exists( $path ) ) { + if ( is_file( $path ) ) { + throw new Exception( "$path already exists as a file." ); + } + + return; + } + + mkdir( $path ); + } + + /** + * Creates a file if it doesn't already exist. + * + * @throws Exception If the path already exists as a directory. + * + * @param string $path The path to the file. + * @param string $contents Optional. The contents of the file. Default empty string. + */ + public function create_file_if_needed( $path, $contents = '' ) { + if ( file_exists( $path ) ) { + if ( is_dir( $path ) ) { + throw new Exception( "$path already exists as a directory." ); + } + + return; + } + + file_put_contents( $path, $contents ); + } + + /** + * Determines whether the operating system is Windows. + * + * @return bool Whether the operating system is Windows. + */ + public static function is_windows() { + return 'Windows' === PHP_OS_FAMILY; + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_paths_that_exist() { + return array( + 'a file that exists' => array( + 'path' => 'a_file_that_exists.txt', + ), + 'a directory that exists' => array( + 'path' => '', + ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_paths_that_do_not_exist() { + return array( + 'a file that does not exist' => array( + 'path' => 'a_file_that_does_not_exist.txt', + ), + 'a directory that does not exist' => array( + 'path' => 'a_directory_that_does_not_exist', + ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/chdir.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chdir.php new file mode 100644 index 0000000000000..8d70acc3b76e1 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chdir.php @@ -0,0 +1,95 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::chdir() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::chdir + */ +class Tests_Filesystem_WpFilesystemDirect_Chdir extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::chdir()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_should_fail_to_change_directory + * + * @param string $path The path. + */ + public function test_should_fail_to_change_directory( $path ) { + $original_cwd = self::$filesystem->cwd(); + $path = wp_normalize_path( realpath( self::$file_structure['test_dir']['path'] ) ) . $path; + $chdir_result = self::$filesystem->chdir( $path ); + $cwd_result = self::$filesystem->cwd(); + + // Reset the current working directory. + self::$filesystem->chdir( $original_cwd ); + + $this->assertFalse( + $chdir_result, + 'Changing working directory succeeded.' + ); + + $this->assertSame( + $original_cwd, + $cwd_result, + 'The current working directory was changed.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_fail_to_change_directory() { + return array( + 'a file that exists' => array( + 'path' => 'a_file_that_exists.txt', + ), + 'a file that does not exist' => array( + 'path' => 'a_file_that_does_not_exist.txt', + ), + 'a directory that does not exist' => array( + 'path' => 'a_directory_that_does_not_exist', + ), + ); + } + + /** + * Tests that `WP_Filesystem_Direct::chdir()` changes to + * an existing directory. + * + * @ticket 57774 + */ + public function test_should_change_directory() { + $original_cwd = self::$filesystem->cwd(); + $path = wp_normalize_path( realpath( self::$file_structure['test_dir']['path'] ) ); + $chdir_result = self::$filesystem->chdir( $path ); + $cwd_result = self::$filesystem->cwd(); + + // Reset the current working directory. + self::$filesystem->chdir( $original_cwd ); + + $this->assertTrue( + $chdir_result, + 'Changing working directory failed.' + ); + + $this->assertSamePathIgnoringDirectorySeparators( + $path, + $cwd_result, + 'The current working directory was incorrect.' + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/chgrp.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chgrp.php new file mode 100644 index 0000000000000..a56e5c5ac2b41 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chgrp.php @@ -0,0 +1,32 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::chgrp() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::chgrp + */ +class Tests_Filesystem_WpFilesystemDirect_Chgrp extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::chgrp()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_fail_to_change_file_group( $path ) { + $this->assertFalse( self::$filesystem->chgrp( self::$file_structure['test_dir']['path'] . $path, 0 ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/chmod.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chmod.php new file mode 100644 index 0000000000000..4deb47c4f09fb --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chmod.php @@ -0,0 +1,77 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::chmod() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::chmod + */ +class Tests_Filesystem_WpFilesystemDirect_Chmod extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::chmod()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_false( $path ) { + $this->assertFalse( self::$filesystem->chmod( $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::chmod()` should set + * $mode when it is not passed. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @dataProvider data_should_set_mode_when_not_passed + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param string $path The path. + * @param string $type The type of path. "FILE" for file, "DIR" for directory. + */ + public function test_should_handle_set_mode_when_not_passed( $path, $type ) { + define( 'FS_CHMOD_' . $type, ( 'FILE' === $type ? 0644 : 0755 ) ); + + $this->assertTrue( self::$filesystem->chmod( self::$file_structure['test_dir']['path'] . $path, false ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_set_mode_when_not_passed() { + return array( + 'a file' => array( + 'path' => 'a_file_that_exists.txt', + 'type' => 'FILE', + ), + 'a directory' => array( + 'path' => '', + 'type' => 'DIR', + ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/chown.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chown.php new file mode 100644 index 0000000000000..040693b03c54c --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/chown.php @@ -0,0 +1,32 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::chown() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::chown + */ +class Tests_Filesystem_WpFilesystemDirect_Chown extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::chown()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_false( $path ) { + $this->assertFalse( self::$filesystem->chown( $path, fileowner( __FILE__ ) ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/construct.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/construct.php new file mode 100644 index 0000000000000..e8c475a9ee044 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/construct.php @@ -0,0 +1,39 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::__construct() method. + * + * @package WordPress + */ + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::__construct + */ +class Tests_Filesystem_WpFilesystemDirect_Construct extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that the $method and $errors properties are set upon + * the instantiation of a WP_Filesystem_Direct object. + * + * @ticket 57774 + */ + public function test_should_set_method_and_errors() { + // For coverage reports, a new object must be created in the method. + $filesystem = new WP_Filesystem_Direct( null ); + + $this->assertSame( + 'direct', + $filesystem->method, + 'The "$method" property is not set to "direct".' + ); + + $this->assertInstanceOf( + 'WP_Error', + $filesystem->errors, + 'The "$errors" property is not set to a WP_Error object.' + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php new file mode 100644 index 0000000000000..d0355c20e5662 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/copy.php @@ -0,0 +1,72 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::copy() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::copy + */ +class Tests_Filesystem_WpFilesystemDirect_Copy extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::copy()` overwrites an existing + * destination when overwriting is enabled. + * + * @ticket 57774 + */ + public function test_should_overwrite_an_existing_file_when_overwriting_is_enabled() { + $source = self::$file_structure['visible_file']['path']; + $destination = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.dest'; + + if ( ! file_exists( $destination ) ) { + touch( $destination ); + } + + $actual = self::$filesystem->copy( $source, $destination, true ); + + unlink( $destination ); + + $this->assertTrue( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::copy()` does not overwrite + * an existing destination when overwriting is disabled. + * + * @ticket 57774 + */ + public function test_should_not_overwrite_an_existing_file_when_overwriting_is_disabled() { + $source = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.txt'; + $destination = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.dest'; + + if ( ! file_exists( $destination ) ) { + touch( $destination ); + } + + $actual = self::$filesystem->copy( $source, $destination ); + + unlink( $destination ); + + $this->assertFalse( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::copy()` does not overwrite an existing + * destination when overwriting is enabled and the source and destination + * are the same. + * + * @ticket 57774 + */ + public function test_should_not_overwrite_when_overwriting_is_enabled_and_source_and_destination_are_the_same() { + $source = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.txt'; + $this->assertFalse( self::$filesystem->copy( $source, $source, true ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/cwd.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/cwd.php new file mode 100644 index 0000000000000..c4ce9617314cb --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/cwd.php @@ -0,0 +1,26 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::cwd() method. + * + * @package WordPress + */ + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::cwd + */ +class Tests_Filesystem_WpFilesystemDirect_Cwd extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::cwd()` returns the current + * working directory. + * + * @ticket 57774 + */ + public function test_should_get_current_working_directory() { + $this->assertSame( wp_normalize_path( dirname( ABSPATH ) ), wp_normalize_path( self::$filesystem->cwd() ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/delete.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/delete.php new file mode 100644 index 0000000000000..4afe54890a5f3 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/delete.php @@ -0,0 +1,204 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::delete() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::delete + */ +class Tests_Filesystem_WpFilesystemDirect_Delete extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::delete()` returns false + * for an empty path. + * + * @ticket 57774 + */ + public function test_should_return_false_for_empty_path() { + $this->assertFalse( self::$filesystem->delete( '' ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` deletes an empty directory. + * + * @ticket 57774 + */ + public function test_should_delete_an_empty_directory() { + $dir = self::$file_structure['test_dir']['path'] . 'directory-to-delete'; + + $this->assertTrue( + mkdir( $dir ), + 'The directory was not created.' + ); + + $this->assertTrue( + self::$filesystem->delete( $dir ), + 'The directory was not deleted.' + ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` deletes a directory with contents. + * + * @ticket 57774 + */ + public function test_should_delete_a_directory_with_contents() { + $this->assertTrue( + self::$filesystem->delete( self::$file_structure['test_dir']['path'], true ), + 'Directory deletion failed.' + ); + + $this->assertDirectoryDoesNotExist( + self::$file_structure['test_dir']['path'], + 'The directory was not deleted.' + ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` deletes a file. + * + * @ticket 57774 + * + * @dataProvider data_should_delete_a_file + * + * @param string $key The key for the file in `self::$filesystem_structure`. + */ + public function test_should_delete_a_file( $file ) { + $file = self::$file_structure[ $file ]['path'] . $file; + + $this->assertTrue( self::$filesystem->delete( $file ), 'File deletion failed.' ); + $this->assertFileDoesNotExist( $file, 'The file was not deleted.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_delete_a_file() { + return array( + 'A visible file' => array( + 'key' => 'visible_file', + ), + 'A hidden file' => array( + 'key' => 'hidden_file', + ), + ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` + * returns true when deleting a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_true_when_deleting_path_that_does_not_exist( $path ) { + $path = self::$file_structure['test_dir']['path'] . $path; + + /* + * Verify that the path doesn't exist before testing. + * + * assertFileDoesNotExist() uses file_exists(), which returns the same result for both + * files and directories. + * assertDirectoryDoesNotExist() uses is_dir(), which tests strictly for a directory. + * + * For more useful debugging in the event of a failure, test for a directory first. + */ + $this->assertDirectoryDoesNotExist( $path, "$path already existed as a directory before testing." ); + $this->assertFileDoesNotExist( $path, "$path already existed as a file before testing." ); + + $this->assertTrue( self::$filesystem->delete( $path ), 'Attempting to delete a non-existent path should return true.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` + * returns false when a directory's contents cannot be deleted. + * + * @ticket 57774 + */ + public function test_should_return_false_when_contents_cannot_be_deleted() { + global $wp_filesystem; + + $wp_filesystem = new WP_Filesystem_Direct( array() ); + + $path = self::$file_structure['test_dir']['path'] . 'dir-to-delete/'; + + if ( ! is_dir( $path ) ) { + mkdir( $path ); + } + + // Set up mock filesystem. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + ->setConstructorArgs( array( null ) ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'dirlist' ) ) + ->getMock(); + + $filesystem_mock->expects( $this->once() ) + ->method( 'dirlist' ) + ->willReturn( + array( 'a_file_that_does_not_exist.txt' => array( 'type' => 'f' ) ) + ); + + $wp_filesystem_backup = $wp_filesystem; + $wp_filesystem = $filesystem_mock; + + $actual = $filesystem_mock->delete( $path, true ); + + if ( $actual ) { + rmdir( $path ); + } + + $wp_filesystem = $wp_filesystem_backup; + + $this->assertFalse( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::delete()` + * returns false when the path is not a file or directory, but exists. + * + * @ticket 57774 + */ + public function test_should_return_false_when_path_exists_but_is_not_a_file_or_directory() { + global $wp_filesystem; + + $wp_filesystem = new WP_Filesystem_Direct( array() ); + + // Set up mock filesystem. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + ->setConstructorArgs( array( null ) ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'is_file', 'dirlist' ) ) + ->getMock(); + + $filesystem_mock->expects( $this->once() ) + ->method( 'is_file' ) + ->willReturn( false ); + + $filesystem_mock->expects( $this->once() ) + ->method( 'dirlist' ) + ->willReturn( false ); + + $wp_filesystem_backup = $wp_filesystem; + $wp_filesystem = $filesystem_mock; + + $actual = $filesystem_mock->delete( self::$file_structure['subdir']['path'], true ); + + $wp_filesystem = $wp_filesystem_backup; + + $this->assertFalse( $actual ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/dirlist.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/dirlist.php new file mode 100644 index 0000000000000..04dab2a48a612 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/dirlist.php @@ -0,0 +1,130 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::dirlist() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::dirlist + */ +class Tests_Filesystem_WpFilesystemDirect_Dirlist extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::dirlist()` returns + * the expected result for a path. + * + * @ticket 57774 + * + * @dataProvider data_should_get_dirlist + * + * @param string $path The path. + * @param bool $include_hidden Whether to include hidden files. + * @param bool $recursive Whether to recursive into subdirectories. + * @param array|false $expected The expected result. + */ + public function test_should_get_dirlist( $path, $include_hidden, $recursive, $expected ) { + $actual = self::$filesystem->dirlist( self::$file_structure['test_dir']['path'] . $path, $include_hidden, $recursive ); + + if ( is_array( $expected ) ) { + $this->assertSameSets( + $expected, + array_keys( $actual ), + 'The array keys do not match.' + ); + } else { + $this->assertFalse( + $actual, + '`WP_Filesystem_Direct::dirlist()` did not return false.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_get_dirlist() { + return array( + 'a directory that exists excluding hidden files' => array( + 'path' => '', + 'include_hidden' => false, + 'recursive' => false, + 'expected' => array( + 'a_file_that_exists.txt', + 'subdir', + ), + ), + 'a directory that exists including hidden files' => array( + 'path' => '', + 'include_hidden' => true, + 'recursive' => false, + 'expected' => array( + 'a_file_that_exists.txt', + '.a_hidden_file', + 'subdir', + ), + ), + 'a directory that does not exist' => array( + 'path' => 'a_directory_that_does_not_exist/', + 'include_hidden' => true, + 'recursive' => false, + 'expected' => false, + ), + 'a file that exists' => array( + 'path' => 'a_file_that_exists.txt', + 'include_hidden' => true, + 'recursive' => false, + 'expected' => array( + 'a_file_that_exists.txt', + ), + ), + 'a file that does not exist' => array( + 'path' => 'a_file_that_does_not_exist.txt', + 'include_hidden' => true, + 'recursive' => false, + 'expected' => false, + ), + ); + } + + /** + * Tests that `WP_Filesystem_Direct::dirlist()` recurses + * into a subdirectory. + * + * @ticket 57774 + */ + public function test_should_recurse_into_subdirectory() { + $actual = self::$filesystem->dirlist( self::$file_structure['test_dir']['path'], true, true ); + + $this->assertIsArray( $actual, 'Did not return an array.' ); + $this->assertArrayHasKey( 'subdir', $actual, 'The subdirectory was not detected.' ); + $this->assertArrayHasKey( 'files', $actual['subdir'], 'The subdirectory does not have a "files" key.' ); + $this->assertNotEmpty( $actual['subdir']['files'], "The subdirectory's contents were not retrieved." ); + $this->assertArrayHasKey( 'subfile.txt', $actual['subdir']['files'], 'The subfile was not detected.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::dirlist()` should not recurse + * into a subdirectory. + * + * @ticket 57774 + */ + public function test_should_not_recurse_into_subdirectory() { + + $actual = self::$filesystem->dirlist( self::$file_structure['test_dir']['path'], true, false ); + + $this->assertIsArray( $actual, 'Did not return an array.' ); + $this->assertArrayHasKey( 'subdir', $actual, 'The subdirectory was not detected.' ); + $this->assertArrayHasKey( 'files', $actual['subdir'], 'The "files" key was not set.' ); + $this->assertIsArray( $actual['subdir']['files'], 'The "files" key was not set to an array.' ); + $this->assertEmpty( $actual['subdir']['files'], 'The "files" array was not empty.' ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/exists.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/exists.php new file mode 100644 index 0000000000000..11eb32621da6a --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/exists.php @@ -0,0 +1,44 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::exists() method. + * + * @package WordPress + */ + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::exists + */ +class Tests_Filesystem_WpFilesystemDirect_Exists extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::exists()` determines that + * a path exists. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path to check. + */ + public function test_should_determine_that_a_path_exists( $path ) { + $this->assertTrue( self::$filesystem->exists( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::exists()` determines that + * a path does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path to check. + */ + public function test_should_determine_that_a_path_does_not_exist( $path ) { + $this->assertFalse( self::$filesystem->exists( self::$file_structure['test_dir']['path'] . $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContents.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContents.php new file mode 100644 index 0000000000000..fed713239a48e --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContents.php @@ -0,0 +1,47 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::get_contents() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::get_contents + */ +class Tests_Filesystem_WpFilesystemDirect_GetContents extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::get_contents()` gets the + * contents of the provided $file. + * + * @ticket 57774 + */ + public function test_should_get_the_contents_of_a_file() { + $file = self::$file_structure['visible_file']['path']; + + $this->assertSame( + "Contents of a file.\r\nNext line of a file.\r\n", + self::$filesystem->get_contents( $file ) + ); + } + + /** + * Tests that `WP_Filesystem_Direct::get_contents()` + * returns false for a file that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_false( $path ) { + $this->assertFalse( self::$filesystem->get_contents( self::$file_structure['test_dir']['path'] . $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContentsArray.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContentsArray.php new file mode 100644 index 0000000000000..0e09822c07328 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getContentsArray.php @@ -0,0 +1,57 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::get_contents_array() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::get_contents_array + */ +class Tests_Filesystem_WpFilesystemDirect_GetContentsArray extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::get_contents_array()` gets + * the contents of the provided file. + * + * @ticket 57774 + */ + public function test_should_get_the_contents_of_a_file_as_an_array() { + $file = self::$file_structure['visible_file']['path']; + $contents = self::$filesystem->get_contents_array( $file ); + + $this->assertIsArray( + $contents, + 'The file contents are not an array.' + ); + + $this->assertSameSetsWithIndex( + array( + "Contents of a file.\r\n", + "Next line of a file.\r\n", + ), + $contents, + 'The file contents do not match the expected value.' + ); + } + + /** + * Tests that `WP_Filesystem_Direct::get_contents_array()` + * returns false for a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_false( $path ) { + $this->assertFalse( self::$filesystem->get_contents_array( self::$file_structure['test_dir']['path'] . $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/getchmod.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getchmod.php new file mode 100644 index 0000000000000..c81349e7503e7 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/getchmod.php @@ -0,0 +1,49 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::getchmod() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::getchmod + */ +class Tests_Filesystem_WpFilesystemDirect_Getchmod extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::getchmod()` returns + * the permissions for a path that exists. + * + * @dataProvider data_paths_that_exist + * + * @ticket 57774 + * + * @param string $path The path. + */ + public function test_should_get_chmod_for_a_path_that_exists( $path ) { + $actual = self::$filesystem->getchmod( self::$file_structure['test_dir']['path'] . $path ); + $this->assertNotSame( '', $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::getchmod()` returns + * "0" for a path that does not exist. + * + * @dataProvider data_paths_that_do_not_exist + * + * @ticket 57774 + * @ticket 64426 + * + * @param string $path The path. + */ + public function test_should_return_zero_for_a_path_that_does_not_exist( $path ) { + $actual = self::$filesystem->getchmod( self::$file_structure['test_dir']['path'] . $path ); + $this->assertSame( '0', $actual ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/isDir.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isDir.php new file mode 100644 index 0000000000000..1a367851f6a78 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isDir.php @@ -0,0 +1,63 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::is_dir() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::is_dir + */ +class Tests_Filesystem_WpFilesystemDirect_IsDir extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::is_directory()` determines that + * a path is a directory. + * + * @ticket 57774 + */ + public function test_should_determine_that_a_path_is_a_directory() { + $this->assertTrue( self::$filesystem->is_dir( self::$file_structure['test_dir']['path'] ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::is_directory()` determines that + * a path is not a directory. + * + * @ticket 57774 + * + * @dataProvider data_should_determine_that_a_path_is_not_a_directory + * + * @param string $path The path to check. + * @param string $type The type of resource. Accepts 'f' or 'd'. + * Used to invert $expected due to data provider setup. + */ + public function test_should_determine_that_a_path_is_not_a_directory( $path ) { + $this->assertFalse( self::$filesystem->is_dir( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_determine_that_a_path_is_not_a_directory() { + return array( + 'a file that exists' => array( + 'path' => 'a_file_that_exists.txt', + ), + 'a file that does not exist' => array( + 'path' => 'a_file_that_does_not_exist.txt', + ), + 'a directory that does not exist' => array( + 'path' => 'a_directory_that_does_not_exist', + ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/isFile.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isFile.php new file mode 100644 index 0000000000000..e0471f309b62f --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isFile.php @@ -0,0 +1,61 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::is_file() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::is_file + */ +class Tests_Filesystem_WpFilesystemDirect_IsFile extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::is_file()` determies that + * a path is a file. + * + * @ticket 57774 + */ + public function test_should_determine_that_a_path_is_a_file() { + $this->assertTrue( self::$filesystem->is_file( self::$file_structure['test_dir']['path'] . 'a_file_that_exists.txt' ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::is_file()` determies that + * a path is not a file. + * + * @ticket 57774 + * + * @dataProvider data_should_determine_if_a_path_is_not_a_file + * + * @param string $path The path to check. + */ + public function test_should_determine_that_a_path_is_not_a_file( $path ) { + $this->assertFalse( self::$filesystem->is_file( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_determine_if_a_path_is_not_a_file() { + return array( + 'a file that does not exist' => array( + 'path' => 'a_file_that_does_not_exist.txt', + ), + 'a directory that exists' => array( + 'path' => '', + ), + 'a directory that does not exist' => array( + 'path' => 'a_directory_that_does_not_exist', + ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/isReadable.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isReadable.php new file mode 100644 index 0000000000000..9ccde92c709ac --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isReadable.php @@ -0,0 +1,46 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::is_readable() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::is_readable + */ +class Tests_Filesystem_WpFilesystemDirect_IsReadable extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::is_readable()` determines that + * a path is readable. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path. + */ + public function test_should_determine_that_a_path_is_readable( $path ) { + $this->assertTrue( self::$filesystem->is_readable( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::is_readable()` determines that + * a path is not readable. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_determine_that_a_path_is_not_readable( $path ) { + $this->assertFalse( self::$filesystem->is_readable( self::$file_structure['test_dir']['path'] . $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/isWritable.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isWritable.php new file mode 100644 index 0000000000000..f3a1e0ea757de --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/isWritable.php @@ -0,0 +1,46 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::is_writable() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::is_writable + */ +class Tests_Filesystem_WpFilesystemDirect_IsWritable extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::is_writable()` determines that + * a path is writable. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path. + */ + public function test_should_determine_that_a_path_is_writable( $path ) { + $this->assertTrue( self::$filesystem->is_writable( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::is_writable()` determines that + * a path is not writable. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_determine_that_a_path_is_not_writable( $path ) { + $this->assertFalse( self::$filesystem->is_writable( self::$file_structure['test_dir']['path'] . $path ) ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php new file mode 100644 index 0000000000000..0d87bd536b51d --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mkdir.php @@ -0,0 +1,209 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::mkdir() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::mkdir + */ +class Tests_Filesystem_WpFilesystemDirect_Mkdir extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::mkdir()` creates a directory. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @dataProvider data_should_create_directory + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param mixed $path The path to create. + */ + public function test_should_create_directory( $path ) { + define( 'FS_CHMOD_DIR', 0755 ); + + $path = str_replace( 'TEST_DIR', self::$file_structure['test_dir']['path'], $path ); + $actual = self::$filesystem->mkdir( $path ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $this->assertTrue( $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_create_directory() { + return array( + 'no trailing slash' => array( + 'path' => 'TEST_DIR/directory-to-create', + ), + 'a trailing slash' => array( + 'path' => 'TEST_DIR/directory-to-create/', + ), + ); + } + + /** + * Tests that `WP_Filesystem_Direct::mkdir()` does not create a directory. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @dataProvider data_should_not_create_directory + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param mixed $path The path to create. + */ + public function test_should_not_create_directory( $path ) { + define( 'FS_CHMOD_DIR', 0755 ); + + $path = str_replace( 'TEST_DIR', self::$file_structure['test_dir']['path'], $path ); + $actual = self::$filesystem->mkdir( $path ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $this->assertFalse( $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_create_directory() { + return array( + 'empty path' => array( + 'path' => '', + ), + 'a path that exists' => array( + 'path' => 'TEST_DIR', + ), + ); + } + + /** + * Tests that `WP_Filesystem_Direct::mkdir()` sets chmod. + * + * @ticket 57774 + */ + public function test_should_set_chmod() { + $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; + + $created = self::$filesystem->mkdir( $path, 0644 ); + $chmod = substr( sprintf( '%o', fileperms( $path ) ), -4 ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $expected_permissions = $this->is_windows() ? '0777' : '0644'; + + $this->assertTrue( $created, 'The directory was not created.' ); + $this->assertSame( $expected_permissions, $chmod, 'The permissions are incorrect.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::mkdir()` sets the owner. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_should_set_owner() { + define( 'FS_CHMOD_DIR', 0755 ); + + $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; + + // Get the default owner. + self::$filesystem->mkdir( $path ); + $original_owner = fileowner( $path ); + + rmdir( $path ); + + $created = self::$filesystem->mkdir( $path, 0755, $original_owner ); + $owner = fileowner( $path ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $this->assertTrue( $created, 'The directory was not created.' ); + $this->assertSame( $original_owner, $owner, 'The owner is incorrect.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::mkdir()` sets the group. + * + * This test runs in a separate process so that it can define + * constants without impacting other tests. + * + * This test does not preserve global state to prevent the exception + * "Serialization of 'Closure' is not allowed." when running in a + * separate process. + * + * @ticket 57774 + * + * @runInSeparateProcess + * @preserveGlobalState disabled + */ + public function test_should_set_group() { + define( 'FS_CHMOD_DIR', 0755 ); + + $path = self::$file_structure['test_dir']['path'] . 'directory-to-create'; + + // Get the default group. + self::$filesystem->mkdir( $path ); + $original_group = filegroup( $path ); + + rmdir( $path ); + + $created = self::$filesystem->mkdir( $path, 0755, false, $original_group ); + $group = filegroup( $path ); + + if ( $path !== self::$file_structure['test_dir']['path'] && is_dir( $path ) ) { + rmdir( $path ); + } + + $this->assertTrue( $created, 'The directory was not created.' ); + $this->assertSame( $original_group, $group, 'The group is incorrect.' ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/move.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/move.php new file mode 100644 index 0000000000000..a6822a90f418b --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/move.php @@ -0,0 +1,150 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::move() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::move + */ +class Tests_Filesystem_WpFilesystemDirect_Move extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::copy()` overwrites an existing + * destination when overwriting is enabled. + * + * @ticket 57774 + */ + public function test_should_overwrite_an_existing_file_when_overwriting_is_enabled() { + $source = self::$file_structure['visible_file']['path']; + $destination = self::$file_structure['test_dir']['path'] . 'a_file_that_exists.dest'; + $actual = self::$filesystem->move( $source, $destination, true ); + + rename( $destination, $source ); + + $this->assertTrue( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::move()` does not overwrite + * an existing destination when overwriting is disabled. + * + * @ticket 57774 + */ + public function test_should_not_overwrite_an_existing_file_when_overwriting_is_disabled() { + $source = self::$file_structure['visible_file']['path']; + $destination = self::$file_structure['subfile']['path']; + $actual = self::$filesystem->move( $source, $destination ); + + $this->assertFalse( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::move()` moves directories. + * + * @ticket 57774 + */ + public function test_should_move_directories() { + $source = self::$file_structure['test_dir']['path']; + $destination = untrailingslashit( self::$file_structure['test_dir']['path'] ) . '-dest'; + $actual = self::$filesystem->move( $source, $destination, true ); + + $source_exists = is_dir( $source ); + $destination_exists = is_dir( $destination ); + + if ( $actual ) { + $restored = rename( $destination, $source ); + } + + $this->assertTrue( $actual, 'The directory was not moved.' ); + $this->assertFalse( $source_exists, 'The source still exists.' ); + $this->assertTrue( $destination_exists, 'The destination does not exist.' ); + $this->assertTrue( $restored, 'The test assets were not cleaned up after the test.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::move()` returns false for an + * invalid destination. + * + * @ticket 57774 + */ + public function test_should_return_false_for_invalid_destination() { + $source = self::$file_structure['test_dir']['path']; + $destination = 'http://example.org'; + + $this->assertFalse( self::$filesystem->move( $source, $destination, true ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::move()` returns false for an + * invalid destination. + * + * @ticket 57774 + */ + public function test_should_return_false_when_overwriting_is_enabled_the_destination_exists_but_cannot_be_deleted() { + global $wp_filesystem; + $wpfilesystem_backup = $wp_filesystem; + + // Force failure conditions. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'exists', 'delete' ) ) + ->setConstructorArgs( array( null ) ) + ->getMock(); + + $filesystem_mock->expects( $this->once() )->method( 'exists' )->willReturn( true ); + $filesystem_mock->expects( $this->once() )->method( 'delete' )->willReturn( false ); + $wp_filesystem = $filesystem_mock; + + $actual = $wp_filesystem->move( + self::$file_structure['test_dir']['path'], + self::$file_structure['subdir']['path'], + true + ); + + // Restore the filesystem. + $wp_filesystem = $wpfilesystem_backup; + + $this->assertFalse( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::move()` falls back to a single + * file copy when the source and destination do not exist. + * + * @ticket 57774 + */ + public function test_should_fall_back_to_single_file_copy_when_source_and_destination_do_not_exist() { + global $wp_filesystem; + + $source = self::$file_structure['test_dir']['path'] . 'a_file_that_does_not_exist.txt'; + $destination = self::$file_structure['test_dir']['path'] . 'another_file_that_does_not_exist.txt'; + + // Set up mock filesystem. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + ->setConstructorArgs( array( null ) ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'exists', 'delete', 'is_file', 'copy' ) ) + ->getMock(); + + $filesystem_mock->expects( $this->exactly( 2 ) )->method( 'exists' )->willReturn( array( true, true ) ); + $filesystem_mock->expects( $this->exactly( 2 ) )->method( 'delete' )->willReturn( array( true, false ) ); + $filesystem_mock->expects( $this->once() )->method( 'is_file' )->willReturn( true ); + $filesystem_mock->expects( $this->once() )->method( 'copy' )->willReturn( true ); + + $wp_filesystem_backup = $wp_filesystem; + $wp_filesystem = $filesystem_mock; + + $actual = $filesystem_mock->move( $source, $destination, true ); + $wp_filesystem = $wp_filesystem_backup; + + $this->assertTrue( $actual ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/mtime.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mtime.php new file mode 100644 index 0000000000000..57d6f92bbc6f6 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/mtime.php @@ -0,0 +1,68 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::mtime() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::mtime + */ +class Tests_Filesystem_WpFilesystemDirect_Mtime extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::mtime()` determines + * the mtime of a path. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path. + */ + public function test_should_determine_file_modified_time( $path ) { + $result = self::$filesystem->mtime( self::$file_structure['test_dir']['path'] . $path ); + $has_mtime = false !== $result; + + $this->assertTrue( + $has_mtime, + 'The mtime was not determined.' + ); + + $this->assertIsInt( + $result, + 'The mtime is not an integer.' + ); + } + + /** + * Tests that `WP_Filesystem_Direct::mtime()` does not determine + * the mtime of a path. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_not_determine_file_modified_time( $path ) { + $result = self::$filesystem->mtime( self::$file_structure['test_dir']['path'] . $path ); + $has_mtime = false !== $result; + + $this->assertFalse( + $has_mtime, + 'An mtime was determined.' + ); + + $this->assertIsNotInt( + $result, + 'The mtime is an integer.' + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/putContents.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/putContents.php new file mode 100644 index 0000000000000..8eabfffdb2f6a --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/putContents.php @@ -0,0 +1,42 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::put_contents() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::put_contents + */ +class Tests_Filesystem_WpFilesystemDirect_PutContents extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::put_contents()` + * returns false for a directory. + * + * @ticket 57774 + */ + public function test_should_return_false_for_a_directory() { + $this->assertFalse( self::$filesystem->put_contents( self::$file_structure['test_dir']['path'], 'New content.' ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::put_contents()` inserts + * content into the provided file. + * + * @ticket 57774 + */ + public function test_should_insert_contents_into_file() { + $file = self::$file_structure['test_dir']['path'] . 'file-to-create.txt'; + $actual = self::$filesystem->put_contents( $file, 'New content.', 0644 ); + unlink( $file ); + + $this->assertTrue( $actual, 'The contents were not inserted.' ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/rmdir.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/rmdir.php new file mode 100644 index 0000000000000..9186fcf2e03ce --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/rmdir.php @@ -0,0 +1,200 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::rmdir() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::rmdir + */ +class Tests_Filesystem_WpFilesystemDirect_Rmdir extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` returns false + * for an empty path. + * + * @ticket 57774 + */ + public function test_should_return_false_for_empty_path() { + $this->assertFalse( self::$filesystem->rmdir( '' ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` deletes an empty directory. + * + * @ticket 57774 + */ + public function test_should_delete_an_empty_directory() { + $dir = self::$file_structure['test_dir']['path'] . 'directory-to-delete/'; + + if ( ! is_dir( $dir ) ) { + mkdir( $dir ); + } + + $actual = self::$filesystem->rmdir( $dir ); + + if ( ! $actual ) { + rmdir( $dir ); + } + + $this->assertTrue( $actual, 'The directory was not deleted.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` recursively deletes + * a directory with contents. + * + * @ticket 57774 + */ + public function test_should_recursively_delete_a_directory() { + $dir = self::$file_structure['test_dir']['path'] . 'directory-to-delete/'; + $file = $dir . 'file-to-delete.txt'; + $subdir = $dir . 'subdirectory-to-delete/'; + $subfile = $subdir . 'subfile-to-delete.txt'; + + mkdir( $dir, 0755 ); + mkdir( $subdir, 0755 ); + touch( $file, 0644 ); + touch( $subfile, 0644 ); + + $actual = self::$filesystem->rmdir( self::$file_structure['test_dir']['path'], true ); + + if ( ! $actual ) { + unlink( $file ); + unlink( $subfile ); + rmdir( $subdir ); + rmdir( $dir ); + } + + $this->assertTrue( $actual, 'The directory was deleted.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` deletes a file. + * + * @ticket 57774 + */ + public function test_should_delete_a_file() { + $file = self::$file_structure['test_dir']['path'] . 'file-to-delete.txt'; + + touch( $file ); + + $actual = self::$filesystem->rmdir( $file ); + + if ( ! $actual ) { + unlink( $file ); + } + + $this->assertTrue( $actual, 'The directory was not deleted.' ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` + * returns true when deleting a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_return_true_when_deleting_path_that_does_not_exist( $path ) { + if ( + '' === $path + || str_starts_with( $path, '.' ) + || str_starts_with( $path, '/' ) + ) { + $this->markTestSkipped( 'Dangerous delete path.' ); + } + + $this->assertTrue( self::$filesystem->rmdir( self::$file_structure['test_dir']['path'] . $path ) ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` + * returns false when a directory's contents cannot be deleted. + * + * @ticket 57774 + */ + public function test_should_return_false_when_contents_cannot_be_deleted() { + + global $wp_filesystem; + + $wp_filesystem = new WP_Filesystem_Direct( array() ); + + $path = self::$file_structure['test_dir']['path'] . 'dir-to-delete/'; + + if ( ! is_dir( $path ) ) { + mkdir( $path ); + } + + // Set up mock filesystem. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + ->setConstructorArgs( array( null ) ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'dirlist' ) ) + ->getMock(); + + $filesystem_mock->expects( $this->once() ) + ->method( 'dirlist' ) + ->willReturn( + array( 'a_file_that_does_not_exist.txt' => array( 'type' => 'f' ) ) + ); + + $wp_filesystem_backup = $wp_filesystem; + $wp_filesystem = $filesystem_mock; + + $actual = $filesystem_mock->rmdir( $path, true ); + + if ( $actual ) { + rmdir( $path ); + } + + $wp_filesystem = $wp_filesystem_backup; + + $this->assertFalse( $actual ); + } + + /** + * Tests that `WP_Filesystem_Direct::rmdir()` + * returns false when the path is not a file or directory, but exists. + * + * @ticket 57774 + */ + public function test_should_return_false_when_path_exists_but_is_not_a_file_or_directory() { + global $wp_filesystem; + + $wp_filesystem = new WP_Filesystem_Direct( array() ); + + // Set up mock filesystem. + $filesystem_mock = $this->getMockBuilder( 'WP_Filesystem_Direct' ) + ->setConstructorArgs( array( null ) ) + // Note: setMethods() is deprecated in PHPUnit 9, but still supported. + ->setMethods( array( 'is_file', 'dirlist' ) ) + ->getMock(); + + $filesystem_mock->expects( $this->once() ) + ->method( 'is_file' ) + ->willReturn( false ); + + $filesystem_mock->expects( $this->once() ) + ->method( 'dirlist' ) + ->willReturn( false ); + + $wp_filesystem_backup = $wp_filesystem; + $wp_filesystem = $filesystem_mock; + + $actual = $filesystem_mock->rmdir( self::$file_structure['subdir']['path'], true ); + + $wp_filesystem = $wp_filesystem_backup; + + $this->assertFalse( $actual ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/size.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/size.php new file mode 100644 index 0000000000000..8870b8ac8cdec --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/size.php @@ -0,0 +1,63 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::size() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::size + */ +class Tests_Filesystem_WpFilesystemDirect_Size extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::size()` determines + * the file size of a path that exists. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_exist + * + * @param string $path The path. + */ + public function test_should_determine_file_size( $path ) { + $result = self::$filesystem->size( self::$file_structure['test_dir']['path'] . $path ); + $has_filesize = false !== $result; + + $this->assertTrue( + $has_filesize, + 'The file size was not determined.' + ); + + $this->assertIsInt( + $result, + 'The file size is not an integer.' + ); + } + + /** + * Tests that `WP_Filesystem_Direct::size()` does not determine + * the filesize of a path that does not exist. + * + * @ticket 57774 + * + * @dataProvider data_paths_that_do_not_exist + * + * @param string $path The path. + */ + public function test_should_not_determine_file_size( $path ) { + $result = self::$filesystem->size( self::$file_structure['test_dir']['path'] . $path ); + $has_filesize = false !== $result; + + $this->assertFalse( + $has_filesize, + 'A file size was determined.' + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpFilesystemDirect/touch.php b/tests/phpunit/tests/filesystem/wpFilesystemDirect/touch.php new file mode 100644 index 0000000000000..6fc494479c61f --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpFilesystemDirect/touch.php @@ -0,0 +1,93 @@ +<?php +/** + * Tests for the WP_Filesystem_Direct::touch() method. + * + * @package WordPress + */ + +require_once __DIR__ . '/base.php'; + +/** + * @group admin + * @group filesystem + * @group filesystem-direct + * + * @covers WP_Filesystem_Direct::touch + */ +class Tests_Filesystem_WpFilesystemDirect_Touch extends WP_Filesystem_Direct_UnitTestCase { + + /** + * Tests that `WP_Filesystem_Direct::touch()` creates a file. + * + * @ticket 57774 + * + * @dataProvider data_should_create_file + * + * @param string $file The file path. + * @param int $mtime The modified time to set. + * @param int $atime The accessed time to set. + */ + public function test_should_create_file( $file, $mtime, $atime ) { + $file = str_replace( 'TEST_DATA', self::$file_structure['test_dir']['path'], $file ); + + if ( is_string( $mtime ) ) { + $mtime = (int) str_replace( + array( 'time plus one minute', time() + MINUTE_IN_SECONDS ), + array( 'time', time() ), + $mtime + ); + } + + $expected_mtime = 0 === $mtime ? time() : $mtime; + + if ( is_string( $atime ) ) { + $atime = (int) str_replace( + array( 'time plus one minute', time() + MINUTE_IN_SECONDS ), + array( 'time', time() ), + $atime + ); + } + + $expected_atime = 0 === $atime ? time() : $atime; + + $result = self::$filesystem->touch( $file, $mtime, $atime ); + + $actual_atime = fileatime( $file ); + $actual_exists = file_exists( $file ); + $actual_mtime = filemtime( $file ); + + if ( $actual_exists ) { + unlink( $file ); + } + + $this->assertTrue( $result, 'WP_Filesystem_Direct::touch() did not return true.' ); + $this->assertTrue( $actual_exists, 'The file does not exist.' ); + $this->assertSame( $actual_atime, $expected_atime, 'The file does not have the expected atime.' ); + $this->assertSame( $actual_mtime, $expected_mtime, 'The file does not have the expected mtime.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_create_file() { + return array( + 'default mtime or atime' => array( + 'file' => 'TEST_DATA/file-to-create.txt', + 'mtime' => 0, + 'atime' => 0, + ), + 'set mtime and default atime' => array( + 'file' => 'TEST_DATA/file-to-create.txt', + 'mtime' => 'time plus one minute', + 'atime' => 'time', + ), + 'default mtime and set atime' => array( + 'file' => 'TEST_DATA/file-to-create.txt', + 'mtime' => 'time', + 'atime' => 'time plus one minute', + ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpOpcacheInvalidateDirectory.php b/tests/phpunit/tests/filesystem/wpOpcacheInvalidateDirectory.php new file mode 100644 index 0000000000000..a297c15e99940 --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpOpcacheInvalidateDirectory.php @@ -0,0 +1,102 @@ +<?php + +/** + * Tests wp_opcache_invalidate_directory(). + * + * @group file + * @group filesystem + * + * @covers ::wp_opcache_invalidate_directory + */ +class Tests_Filesystem_WpOpcacheInvalidateDirectory extends WP_UnitTestCase { + + /** + * Sets up the filesystem before any tests run. + */ + public static function set_up_before_class() { + global $wp_filesystem; + + parent::set_up_before_class(); + + if ( ! $wp_filesystem ) { + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + } + } + + /** + * Tests that wp_opcache_invalidate_directory() returns a WP_Error object + * when the $dir argument invalid. + * + * @ticket 57375 + * + * @dataProvider data_should_trigger_error_with_invalid_dir + * + * @param mixed $dir An invalid directory path. + */ + public function test_should_trigger_error_with_invalid_dir( $dir ) { + $this->expectError(); + $this->expectErrorMessage( + '<code>wp_opcache_invalidate_directory()</code> expects a non-empty string.', + 'The expected error was not triggered.' + ); + + wp_opcache_invalidate_directory( $dir ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error_with_invalid_dir() { + return array( + 'an empty string' => array( '' ), + 'a string with spaces' => array( ' ' ), + 'a string with tabs' => array( "\t" ), + 'a string with new lines' => array( "\n" ), + 'a string with carriage returns' => array( "\r" ), + 'int -1' => array( -1 ), + 'int 0' => array( 0 ), + 'int 1' => array( 1 ), + 'float -1.0' => array( -1.0 ), + 'float 0.0' => array( 0.0 ), + 'float 1.0' => array( 1.0 ), + 'false' => array( false ), + 'true' => array( true ), + 'null' => array( null ), + 'an empty array' => array( array() ), + 'a non-empty array' => array( array( 'directory_path' ) ), + 'an empty object' => array( new stdClass() ), + 'a non-empty object' => array( (object) array( 'directory_path' ) ), + 'INF' => array( INF ), + 'NAN' => array( NAN ), + ); + } + + /** + * Tests that wp_opcache_invalidate_directory() does not trigger an error + * with a valid directory. + * + * @ticket 57375 + * + * @dataProvider data_should_not_trigger_error_wp_opcache_valid_directory + * + * @param string $dir A directory path. + */ + public function test_should_not_trigger_error_wp_opcache_valid_directory( $dir ) { + $this->assertNull( wp_opcache_invalidate_directory( $dir ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_not_trigger_error_wp_opcache_valid_directory() { + return array( + 'an existing directory' => array( DIR_TESTDATA ), + 'a non-existent directory' => array( 'non_existent_directory' ), + ); + } +} diff --git a/tests/phpunit/tests/filesystem/wpZipFileIsValid.php b/tests/phpunit/tests/filesystem/wpZipFileIsValid.php new file mode 100644 index 0000000000000..94a5f4ebbd21e --- /dev/null +++ b/tests/phpunit/tests/filesystem/wpZipFileIsValid.php @@ -0,0 +1,75 @@ +<?php + +/** + * Tests wp_zip_file_is_valid(). + * + * @group file + * @group filesystem + * + * @covers ::wp_zip_file_is_valid + */ +class Tests_Filesystem_WpZipFileIsValid extends WP_UnitTestCase { + + /** + * The test data directory. + * + * @var string $test_data_dir + */ + private static $test_data_dir; + + /** + * Sets up the filesystem and test data directory property + * before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + require_once ABSPATH . 'wp-admin/includes/file.php'; + WP_Filesystem(); + + self::$test_data_dir = DIR_TESTDATA . '/filesystem/'; + } + + /** + * Tests ZIP file validity is correctly determined. + * + * @ticket 60398 + * + * @dataProvider data_zip_file_validity + * + * @param string $file The ZIP file to test. + * @param bool $expected Whether the ZIP file is expected to be valid. + */ + public function test_zip_file_validity( $file, $expected ) { + $zip_file = self::$test_data_dir . $file; + + $expected_message = $expected ? 'valid' : 'invalid'; + $this->assertSame( $expected, wp_zip_file_is_valid( $zip_file ), "Expected archive to be {$expected_message}." ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_zip_file_validity() { + return array( + 'standard zip' => array( 'archive.zip', true ), + 'large zip' => array( 'archive-large.zip', true ), + 'commented zip' => array( 'archive-comment.zip', true ), + 'cp866 zip' => array( 'archive-cp866.zip', true ), + 'directory entry zip' => array( 'archive-directory-entry.zip', true ), + 'encrypted zip' => array( 'archive-encrypted.zip', true ), + 'flags-set zip' => array( 'archive-flags-set.zip', true ), + 'uncompressed zip' => array( 'archive-uncompressed.zip', true ), + 'crx zip' => array( 'archive.crx', true ), + 'macos generated zip' => array( 'archive-macos.zip', true ), + 'gnome generated zip' => array( 'archive-gnome.zip', true ), + 'ubuntu nautilus zip' => array( 'archive-ubuntu-nautilus.zip', true ), + + 'invalid zip file' => array( 'archive-invalid.zip', false ), + 'invalid file extension' => array( 'archive-invalid-ext.md', false ), + 'non-existent file' => array( 'archive-non-existent.zip', false ), + ); + } +} diff --git a/tests/phpunit/tests/filters.php b/tests/phpunit/tests/filters.php index 6bcd43736524d..8ce599c388f29 100644 --- a/tests/phpunit/tests/filters.php +++ b/tests/phpunit/tests/filters.php @@ -8,69 +8,92 @@ class Tests_Filters extends WP_UnitTestCase { public function test_simple_filter() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; - add_filter( $tag, array( $a, 'filter' ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); + add_filter( $hook_name, array( $a, 'filter' ) ); + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); // Only one event occurred for the hook, with empty args. $this->assertSame( 1, $a->get_call_count() ); // Only our hook was called. - $this->assertSame( array( $tag ), $a->get_tags() ); + $this->assertSame( array( $hook_name ), $a->get_hook_names() ); $argsvar = $a->get_args(); $args = array_pop( $argsvar ); $this->assertSame( array( $val ), $args ); } + /** + * @covers ::remove_filter + */ public function test_remove_filter() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; - add_filter( $tag, array( $a, 'filter' ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); + add_filter( $hook_name, array( $a, 'filter' ) ); + add_filter( $hook_name, array( $a, 'filter' ), 100 ); + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); // Make sure our hook was called correctly. - $this->assertSame( 1, $a->get_call_count() ); - $this->assertSame( array( $tag ), $a->get_tags() ); + $this->assertSame( 2, $a->get_call_count() ); + $this->assertSame( array( $hook_name, $hook_name ), $a->get_hook_names() ); // Now remove the filter, do it again, and make sure it's not called this time. - remove_filter( $tag, array( $a, 'filter' ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); - $this->assertSame( 1, $a->get_call_count() ); - $this->assertSame( array( $tag ), $a->get_tags() ); - + remove_filter( $hook_name, array( $a, 'filter' ) ); + remove_filter( $hook_name, array( $a, 'filter' ), 100 ); + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); + $this->assertSame( 2, $a->get_call_count() ); + $this->assertSame( array( $hook_name, $hook_name ), $a->get_hook_names() ); } + /** + * @ticket 64186 + * @covers ::has_filter + */ public function test_has_filter() { - $tag = __FUNCTION__; - $func = __FUNCTION__ . '_func'; - - $this->assertFalse( has_filter( $tag, $func ) ); - $this->assertFalse( has_filter( $tag ) ); - add_filter( $tag, $func ); - $this->assertSame( 10, has_filter( $tag, $func ) ); - $this->assertTrue( has_filter( $tag ) ); - remove_filter( $tag, $func ); - $this->assertFalse( has_filter( $tag, $func ) ); - $this->assertFalse( has_filter( $tag ) ); + $hook_name = __FUNCTION__; + $callback = __FUNCTION__ . '_func'; + + $this->assertFalse( has_filter( $hook_name, $callback ) ); + $this->assertFalse( has_filter( $hook_name ) ); + + add_filter( $hook_name, $callback ); + $this->assertSame( 10, has_filter( $hook_name, $callback ) ); + $this->assertFalse( has_filter( $hook_name, $callback, 9 ) ); + $this->assertTrue( has_filter( $hook_name ) ); + + add_filter( $hook_name, $callback, 9 ); + add_filter( $hook_name, $callback, 11 ); + $this->assertSame( 9, has_filter( $hook_name, $callback ) ); + $this->assertTrue( has_filter( $hook_name, $callback, 9 ) ); + $this->assertTrue( has_filter( $hook_name, $callback, 10 ) ); + $this->assertTrue( has_filter( $hook_name, $callback, 11 ) ); + $this->assertTrue( has_filter( $hook_name ) ); + + remove_filter( $hook_name, $callback, 9 ); + remove_filter( $hook_name, $callback, 11 ); + $this->assertSame( 10, has_filter( $hook_name, $callback ) ); + + remove_filter( $hook_name, $callback ); + $this->assertFalse( has_filter( $hook_name, $callback ) ); + $this->assertFalse( has_filter( $hook_name ) ); } // One tag with multiple filters. public function test_multiple_filters() { - $a1 = new MockAction(); - $a2 = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a1 = new MockAction(); + $a2 = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; // Add both filters to the hook. - add_filter( $tag, array( $a1, 'filter' ) ); - add_filter( $tag, array( $a2, 'filter' ) ); + add_filter( $hook_name, array( $a1, 'filter' ) ); + add_filter( $hook_name, array( $a2, 'filter' ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); // Both filters called once each. $this->assertSame( 1, $a1->get_call_count() ); @@ -78,14 +101,14 @@ public function test_multiple_filters() { } public function test_filter_args_1() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; - $arg1 = __FUNCTION__ . '_arg1'; + $a = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; + $arg1 = __FUNCTION__ . '_arg1'; - add_filter( $tag, array( $a, 'filter' ), 10, 2 ); + add_filter( $hook_name, array( $a, 'filter' ), 10, 2 ); // Call the filter with a single argument. - $this->assertSame( $val, apply_filters( $tag, $val, $arg1 ) ); + $this->assertSame( $val, apply_filters( $hook_name, $val, $arg1 ) ); $this->assertSame( 1, $a->get_call_count() ); $argsvar = $a->get_args(); @@ -93,18 +116,18 @@ public function test_filter_args_1() { } public function test_filter_args_2() { - $a1 = new MockAction(); - $a2 = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; - $arg1 = __FUNCTION__ . '_arg1'; - $arg2 = __FUNCTION__ . '_arg2'; + $a1 = new MockAction(); + $a2 = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; + $arg1 = __FUNCTION__ . '_arg1'; + $arg2 = __FUNCTION__ . '_arg2'; // $a1 accepts two arguments, $a2 doesn't. - add_filter( $tag, array( $a1, 'filter' ), 10, 3 ); - add_filter( $tag, array( $a2, 'filter' ) ); + add_filter( $hook_name, array( $a1, 'filter' ), 10, 3 ); + add_filter( $hook_name, array( $a2, 'filter' ) ); // Call the filter with two arguments. - $this->assertSame( $val, apply_filters( $tag, $val, $arg1, $arg2 ) ); + $this->assertSame( $val, apply_filters( $hook_name, $val, $arg1, $arg2 ) ); // $a1 should be called with both args. $this->assertSame( 1, $a1->get_call_count() ); @@ -117,115 +140,278 @@ public function test_filter_args_2() { $this->assertSame( array( $val ), array_pop( $argsvar2 ) ); } - public function test_filter_priority() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + /** + * @ticket 60193 + * + * @dataProvider data_priority_callback_order_with_integers + * @dataProvider data_priority_callback_order_with_unhappy_path_nonintegers + * + * @covers ::apply_filters + * + * @param array $priorities { + * Indexed array of the priorities for the MockAction callbacks. + * + * @type mixed $0 Priority for 'action' callback. + * @type mixed $1 Priority for 'action2' callback. + * } + * @param array $expected_call_order An array of callback names in expected call order. + * @param string $expected_deprecation Optional. Deprecation message. Default ''. + */ + public function test_priority_callback_order( $priorities, $expected_call_order, $expected_deprecation = '' ) { + $mock = new MockAction(); + $hook_name = __FUNCTION__; - // Make two filters with different priorities. - add_filter( $tag, array( $a, 'filter' ), 10 ); - add_filter( $tag, array( $a, 'filter2' ), 9 ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); + if ( $expected_deprecation && PHP_VERSION_ID >= 80100 ) { + $this->expectDeprecation(); + $this->expectDeprecationMessage( $expected_deprecation ); + } - // There should be two events, one per filter. - $this->assertSame( 2, $a->get_call_count() ); + add_filter( $hook_name, array( $mock, 'filter' ), $priorities[0] ); + add_filter( $hook_name, array( $mock, 'filter2' ), $priorities[1] ); + apply_filters( $hook_name, __FUNCTION__ . '_val' ); - $expected = array( - // 'filter2' is called first because it has priority 9. - array( - 'filter' => 'filter2', - 'tag' => $tag, - 'args' => array( $val ), + $this->assertSame( 2, $mock->get_call_count(), 'The number of call counts does not match' ); + + $actual_call_order = wp_list_pluck( $mock->get_events(), 'filter' ); + $this->assertSame( $expected_call_order, $actual_call_order, 'The filter callback order does not match the expected order' ); + } + + /** + * Happy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_integers() { + return array( + 'int DESC' => array( + 'priorities' => array( 10, 9 ), + 'expected_call_order' => array( 'filter2', 'filter' ), ), - // 'filter' is called second. - array( - 'filter' => 'filter', - 'tag' => $tag, - 'args' => array( $val ), + 'int ASC' => array( + 'priorities' => array( 9, 10 ), + 'expected_call_order' => array( 'filter', 'filter2' ), ), ); + } - $this->assertSame( $expected, $a->get_events() ); + /** + * Unhappy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_unhappy_path_nonintegers() { + return array( + // Numbers as strings and floats. + 'int as string DESC' => array( + 'priorities' => array( '10', '9' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'int as string ASC' => array( + 'priorities' => array( '9', '10' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'float DESC' => array( + 'priorities' => array( 10.0, 9.5 ), + 'expected_call_order' => array( 'filter2', 'filter' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float ASC' => array( + 'priorities' => array( 9.5, 10.0 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float as string DESC' => array( + 'priorities' => array( '10.0', '9.5' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'float as string ASC' => array( + 'priorities' => array( '9.5', '10.0' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + + // Non-numeric. + 'null' => array( + 'priorities' => array( null, null ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'bool DESC' => array( + 'priorities' => array( true, false ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'bool ASC' => array( + 'priorities' => array( false, true ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'non-numerical string DESC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'non-numerical string ASC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'int, non-numerical string DESC' => array( + 'priorities' => array( 10, 'test' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'int, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'float, non-numerical string DESC' => array( + 'priorities' => array( 10.0, 'test' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'float, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10.0 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + ); + } + + /** + * @covers ::did_filter + */ + public function test_did_filter() { + $hook_name1 = 'filter1'; + $hook_name2 = 'filter2'; + $val = __FUNCTION__ . '_val'; + + // Apply filter $hook_name1 but not $hook_name2. + apply_filters( $hook_name1, $val ); + $this->assertSame( 1, did_filter( $hook_name1 ) ); + $this->assertSame( 0, did_filter( $hook_name2 ) ); + + // Apply filter $hook_name2 10 times. + $count = 10; + for ( $i = 0; $i < $count; $i++ ) { + apply_filters( $hook_name2, $val ); + } + + // $hook_name1's count hasn't changed, $hook_name2 should be correct. + $this->assertSame( 1, did_filter( $hook_name1 ) ); + $this->assertSame( $count, did_filter( $hook_name2 ) ); } public function test_all_filter() { - $a = new MockAction(); - $tag1 = __FUNCTION__ . '_1'; - $tag2 = __FUNCTION__ . '_2'; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name1 = __FUNCTION__ . '_1'; + $hook_name2 = __FUNCTION__ . '_2'; + $val = __FUNCTION__ . '_val'; // Add an 'all' filter. add_filter( 'all', array( $a, 'filterall' ) ); // Apply some filters. - $this->assertSame( $val, apply_filters( $tag1, $val ) ); - $this->assertSame( $val, apply_filters( $tag2, $val ) ); - $this->assertSame( $val, apply_filters( $tag1, $val ) ); - $this->assertSame( $val, apply_filters( $tag1, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name1, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name2, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name1, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name1, $val ) ); // Our filter should have been called once for each apply_filters call. $this->assertSame( 4, $a->get_call_count() ); // The right hooks should have been called in order. - $this->assertSame( array( $tag1, $tag2, $tag1, $tag1 ), $a->get_tags() ); + $this->assertSame( array( $hook_name1, $hook_name2, $hook_name1, $hook_name1 ), $a->get_hook_names() ); remove_filter( 'all', array( $a, 'filterall' ) ); $this->assertFalse( has_filter( 'all', array( $a, 'filterall' ) ) ); - } public function test_remove_all_filter() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name = __FUNCTION__; + $val = __FUNCTION__ . '_val'; add_filter( 'all', array( $a, 'filterall' ) ); $this->assertTrue( has_filter( 'all' ) ); $this->assertSame( 10, has_filter( 'all', array( $a, 'filterall' ) ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); // Make sure our hook was called correctly. $this->assertSame( 1, $a->get_call_count() ); - $this->assertSame( array( $tag ), $a->get_tags() ); + $this->assertSame( array( $hook_name ), $a->get_hook_names() ); // Now remove the filter, do it again, and make sure it's not called this time. remove_filter( 'all', array( $a, 'filterall' ) ); $this->assertFalse( has_filter( 'all', array( $a, 'filterall' ) ) ); $this->assertFalse( has_filter( 'all' ) ); - $this->assertSame( $val, apply_filters( $tag, $val ) ); - // Call cound should remain at 1. + $this->assertSame( $val, apply_filters( $hook_name, $val ) ); + // Call count should remain at 1. $this->assertSame( 1, $a->get_call_count() ); - $this->assertSame( array( $tag ), $a->get_tags() ); + $this->assertSame( array( $hook_name ), $a->get_hook_names() ); } /** * @ticket 20920 */ public function test_remove_all_filters_should_respect_the_priority_argument() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name = __FUNCTION__; - add_filter( $tag, array( $a, 'filter' ), 12 ); - $this->assertTrue( has_filter( $tag ) ); + add_filter( $hook_name, array( $a, 'filter' ), 12 ); + $this->assertTrue( has_filter( $hook_name ) ); // Should not be removed. - remove_all_filters( $tag, 11 ); - $this->assertTrue( has_filter( $tag ) ); + remove_all_filters( $hook_name, 11 ); + $this->assertTrue( has_filter( $hook_name ) ); + + remove_all_filters( $hook_name, 12 ); + $this->assertFalse( has_filter( $hook_name ) ); + } + + /** + * @ticket 53218 + */ + public function test_filter_with_ref_value() { + $obj = new stdClass(); + $ref = &$obj; + $a = new MockAction(); + $hook_name = __FUNCTION__; + + add_action( $hook_name, array( $a, 'filter' ) ); + + $filtered = apply_filters( $hook_name, $ref ); + + $args = $a->get_args(); + $this->assertSame( $args[0][0], $obj ); + $this->assertSame( $filtered, $obj ); + // Just in case we don't trust assertSame(). + $obj->foo = true; + $this->assertNotEmpty( $args[0][0]->foo ); + $this->assertNotEmpty( $filtered->foo ); + } + + /** + * @ticket 53218 + */ + public function test_filter_with_ref_argument() { + $obj = new stdClass(); + $ref = &$obj; + $a = new MockAction(); + $hook_name = __FUNCTION__; + $val = 'Hello'; + + add_action( $hook_name, array( $a, 'filter' ), 10, 2 ); + + apply_filters( $hook_name, $val, $ref ); - remove_all_filters( $tag, 12 ); - $this->assertFalse( has_filter( $tag ) ); + $args = $a->get_args(); + $this->assertSame( $args[0][1], $obj ); + // Just in case we don't trust assertSame(). + $obj->foo = true; + $this->assertNotEmpty( $args[0][1]->foo ); } /** * @ticket 9886 */ public function test_filter_ref_array() { - $obj = new stdClass(); - $a = new MockAction(); - $tag = __FUNCTION__; + $obj = new stdClass(); + $a = new MockAction(); + $hook_name = __FUNCTION__; - add_action( $tag, array( $a, 'filter' ) ); + add_action( $hook_name, array( $a, 'filter' ) ); - apply_filters_ref_array( $tag, array( &$obj ) ); + apply_filters_ref_array( $hook_name, array( &$obj ) ); $args = $a->get_args(); $this->assertSame( $args[0][0], $obj ); @@ -238,15 +424,15 @@ public function test_filter_ref_array() { * @ticket 12723 */ public function test_filter_ref_array_result() { - $obj = new stdClass(); - $a = new MockAction(); - $b = new MockAction(); - $tag = __FUNCTION__; + $obj = new stdClass(); + $a = new MockAction(); + $b = new MockAction(); + $hook_name = __FUNCTION__; - add_action( $tag, array( $a, 'filter_append' ), 10, 2 ); - add_action( $tag, array( $b, 'filter_append' ), 10, 2 ); + add_action( $hook_name, array( $a, 'filter_append' ), 10, 2 ); + add_action( $hook_name, array( $b, 'filter_append' ), 10, 2 ); - $result = apply_filters_ref_array( $tag, array( 'string', &$obj ) ); + $result = apply_filters_ref_array( $hook_name, array( 'string', &$obj ) ); $this->assertSame( $result, 'string_append_append' ); @@ -261,33 +447,31 @@ public function test_filter_ref_array_result() { // Just in case we don't trust assertSame(). $obj->foo = true; $this->assertNotEmpty( $args[0][1]->foo ); - } /** * @ticket 29070 */ public function test_has_filter_after_remove_all_filters() { - $a = new MockAction(); - $tag = __FUNCTION__; - $val = __FUNCTION__ . '_val'; + $a = new MockAction(); + $hook_name = __FUNCTION__; // No priority. - add_filter( $tag, array( $a, 'filter' ), 11 ); - add_filter( $tag, array( $a, 'filter' ), 12 ); - $this->assertTrue( has_filter( $tag ) ); + add_filter( $hook_name, array( $a, 'filter' ), 11 ); + add_filter( $hook_name, array( $a, 'filter' ), 12 ); + $this->assertTrue( has_filter( $hook_name ) ); - remove_all_filters( $tag ); - $this->assertFalse( has_filter( $tag ) ); + remove_all_filters( $hook_name ); + $this->assertFalse( has_filter( $hook_name ) ); // Remove priorities one at a time. - add_filter( $tag, array( $a, 'filter' ), 11 ); - add_filter( $tag, array( $a, 'filter' ), 12 ); - $this->assertTrue( has_filter( $tag ) ); + add_filter( $hook_name, array( $a, 'filter' ), 11 ); + add_filter( $hook_name, array( $a, 'filter' ), 12 ); + $this->assertTrue( has_filter( $hook_name ) ); - remove_all_filters( $tag, 11 ); - remove_all_filters( $tag, 12 ); - $this->assertFalse( has_filter( $tag ) ); + remove_all_filters( $hook_name, 11 ); + remove_all_filters( $hook_name, 12 ); + $this->assertFalse( has_filter( $hook_name ) ); } /** @@ -344,6 +528,7 @@ public function test_apply_filters_deprecated_without_filter() { } private $current_priority; + /** * @ticket 39007 */ @@ -357,6 +542,7 @@ public function test_current_priority() { public function current_priority_action() { global $wp_filter; + $this->current_priority = $wp_filter[ current_filter() ]->current_priority(); } diff --git a/tests/phpunit/tests/fonts/font-face/base.php b/tests/phpunit/tests/fonts/font-face/base.php new file mode 100644 index 0000000000000..3b016557728a6 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/base.php @@ -0,0 +1,122 @@ +<?php +/** + * Test case for the Fonts tests. + * + * @package WordPress + * @subpackage Fonts + */ + +require_once __DIR__ . '/wp-font-face-tests-dataset.php'; +/** + * Abstracts the common tasks for the Font Face tests. + */ +abstract class WP_Font_Face_UnitTestCase extends WP_UnitTestCase { + use WP_Font_Face_Tests_Datasets; + + /** + * Current error reporting level (before a test changes it). + * + * @var null|int + */ + protected $error_reporting_level = null; + + /** + * Reflection data store for non-public property access. + * + * @var ReflectionProperty[] + */ + protected $property = array(); + + /** + * Indicates the test class uses `switch_theme()` and requires + * set_up and tear_down fixtures to set and reset hooks and memory. + * + * If a test class switches themes, set this property to `true`. + * + * @var bool + */ + protected static $requires_switch_theme_fixtures = false; + + /** + * Theme root directory. + * + * @var string + */ + protected static $theme_root; + + /** + * Original theme directory. + * + * @var string + */ + protected $orig_theme_dir; + + /** + * Administrator ID. + * + * @var int + */ + protected static $administrator_id = 0; + + public static function set_up_before_class() { + parent::set_up_before_class(); + + if ( self::$requires_switch_theme_fixtures ) { + self::$theme_root = realpath( DIR_TESTDATA . '/themedir1' ); + } + } + + public static function tear_down_after_class() { + // Reset static flags. + self::$requires_switch_theme_fixtures = false; + + parent::tear_down_after_class(); + } + + public function set_up() { + parent::set_up(); + + if ( self::$requires_switch_theme_fixtures ) { + $this->orig_theme_dir = $GLOBALS['wp_theme_directories']; + + // /themes is necessary as theme.php functions assume /themes is the root if there is only one root. + $GLOBALS['wp_theme_directories'] = array( WP_CONTENT_DIR . '/themes', self::$theme_root ); + + // Set up the new root. + add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + + // Clear caches. + wp_clean_themes_cache(); + unset( $GLOBALS['wp_themes'] ); + } + } + + public function tear_down() { + $this->property = array(); + + // Reset the error reporting when modified within a test. + if ( is_int( $this->error_reporting_level ) ) { + error_reporting( $this->error_reporting_level ); + $this->error_reporting_level = null; + } + + // Restore themes. + if ( self::$requires_switch_theme_fixtures ) { + $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; + remove_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); + remove_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + wp_clean_themes_cache(); + wp_clean_theme_json_cache(); + unset( $GLOBALS['wp_themes'] ); + } + + parent::tear_down(); + } + + public function filter_set_theme_root() { + return self::$theme_root; + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php new file mode 100644 index 0000000000000..d410acb7c4124 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wp-font-face-tests-dataset.php @@ -0,0 +1,497 @@ +<?php +/** + * Datasets for unit and integration tests. + * + * @package WordPress + * @subpackage Fonts + */ + +/** + * Trait for reusing datasets within the Fonts tests. + */ +trait WP_Font_Face_Tests_Datasets { + /** + * Data provider. + * + * @return array + */ + public function data_should_print_given_fonts() { + return array( + 'single truetype format font' => array( + 'fonts' => array( + 'Inter' => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf', + ), + 'font-family' => 'Inter', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '200', + ), + ), + ), + 'expected' => <<<CSS +@font-face{font-family:Inter;font-style:normal;font-weight:200;font-display:fallback;src:url('https://example.org/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf') format('truetype');font-stretch:normal;} +CSS + , + ), + 'multiple truetype format fonts' => array( + 'fonts' => array( + 'Inter' => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf', + ), + 'font-family' => 'Inter', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '200', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/inter/Inter-VariableFont_slnt-Italic,wght.ttf', + ), + 'font-family' => 'Inter', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '900', + ), + ), + ), + 'expected' => <<<CSS +@font-face{font-family:Inter;font-style:normal;font-weight:200;font-display:fallback;src:url('https://example.org/assets/fonts/inter/Inter-VariableFont_slnt,wght.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:Inter;font-style:italic;font-weight:900;font-display:fallback;src:url('https://example.org/assets/fonts/inter/Inter-VariableFont_slnt-Italic,wght.ttf') format('truetype');font-stretch:normal;} +CSS + , + ), + 'single woff2 format font' => array( + 'fonts' => array( + 'DM Sans' => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/dm-sans/DMSans-Regular.woff2', + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + ), + ), + 'expected' => <<<CSS +@font-face{font-family:"DM Sans";font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/assets/fonts/dm-sans/DMSans-Regular.woff2') format('woff2');font-stretch:normal;} +CSS + , + ), + 'multiple woff2 format fonts' => array( + 'fonts' => array( + 'DM Sans' => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/dm-sans/DMSans-Regular.woff2', + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/dm-sans/DMSans-Regular-Italic.woff2', + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '400', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/dm-sans/DMSans-Bold.woff2', + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '700', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/dm-sans/DMSans-Bold-Italic.woff2', + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '700', + ), + ), + 'IBM Plex Mono' => + array( + array( + 'src' => + array( + 'https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Light.woff2', + ), + 'font-family' => 'IBM Plex Mono', + 'font-display' => 'block', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '300', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2', + ), + 'font-family' => 'IBM Plex Mono', + 'font-display' => 'block', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Italic.woff2', + ), + 'font-family' => 'IBM Plex Mono', + 'font-display' => 'block', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '400', + ), + array( + 'src' => + array( + 'https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2', + ), + 'font-family' => 'IBM Plex Mono', + 'font-display' => 'block', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '700', + ), + ), + ), + 'expected' => <<<CSS +@font-face{font-family:"DM Sans";font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/assets/fonts/dm-sans/DMSans-Regular.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:italic;font-weight:400;font-display:fallback;src:url('https://example.org/assets/fonts/dm-sans/DMSans-Regular-Italic.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:normal;font-weight:700;font-display:fallback;src:url('https://example.org/assets/fonts/dm-sans/DMSans-Bold.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:italic;font-weight:700;font-display:fallback;src:url('https://example.org/assets/fonts/dm-sans/DMSans-Bold-Italic.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:300;font-display:block;src:url('https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Light.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:400;font-display:block;src:url('https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Regular.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"IBM Plex Mono";font-style:italic;font-weight:400;font-display:block;src:url('https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Italic.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"IBM Plex Mono";font-style:normal;font-weight:700;font-display:block;src:url('https://example.org/assets/fonts/ibm-plex-mono/IBMPlexMono-Bold.woff2') format('woff2');font-stretch:normal;} +CSS + , + 'indexed array as input' => array( + 'fonts' => array( + array( + array( + 'font-family' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla400.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + array( + 'font-family' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla500.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + ), + array( + array( + 'font-family' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster400.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '400', + 'font-stretch' => 'normal', + ), + array( + 'font-family' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster500.ttf' ), + 'font-style' => 'normal', + 'font-weight' => '500', + 'font-stretch' => 'normal', + ), + ), + ), + 'expected' => <<<CSS +@font-face{font-family:Piazzolla;font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/fonts/piazzolla400.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:Piazzolla;font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/fonts/piazzolla500.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:Lobster;font-style:normal;font-weight:400;font-display:fallback;src:url('https://example.org/fonts/lobster400.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:Lobster;font-style:normal;font-weight:500;font-display:fallback;src:url('https://example.org/fonts/lobster500.ttf') format('truetype');font-stretch:normal;} +CSS + , + ), + ), + ); + } + + public function get_expected_fonts_for_fonts_block_theme( $key = '' ) { + static $data = null; + + if ( null === $data ) { + $uri = get_stylesheet_directory_uri() . '/assets/fonts/'; + $data = array( + 'fonts' => array( + array( + array( + 'src' => array( $uri . 'dm-sans/DMSans-Regular.woff2' ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => array( $uri . 'dm-sans/DMSans-Regular-Italic.woff2' ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '400', + ), + array( + 'src' => array( $uri . 'dm-sans/DMSans-Bold.woff2' ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '700', + ), + array( + 'src' => array( $uri . 'dm-sans/DMSans-Bold-Italic.woff2' ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '700', + ), + ), + array( + array( + 'src' => array( $uri . 'source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ), + 'font-family' => 'Source Serif Pro', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '200 900', + ), + array( + 'src' => array( $uri . 'source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2' ), + 'font-family' => 'Source Serif Pro', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '200 900', + ), + ), + ), + 'font_face_styles' => <<<CSS +@font-face{font-family:"DM Sans";font-style:normal;font-weight:400;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Regular.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:italic;font-weight:400;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Regular-Italic.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:normal;font-weight:700;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Bold.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:italic;font-weight:700;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Bold-Italic.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"Source Serif Pro";font-style:normal;font-weight:200 900;font-display:fallback;src:url('{$uri}source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"Source Serif Pro";font-style:italic;font-weight:200 900;font-display:fallback;src:url('{$uri}source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2') format('woff2');font-stretch:normal;} +CSS + , + ); + } + + if ( isset( $data[ $key ] ) ) { + return $data[ $key ]; + } + + return $data; + } + + public static function get_custom_font_families( $key = '' ) { + static $data = null; + + $custom_theme_json_fonts = array( + array( + 'fontFamily' => 'Piazzolla, serif', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla400.ttf' ), + 'fontStyle' => 'normal', + 'fontWeight' => '400', + ), + array( + 'fontFamily' => 'Piazzolla', + 'src' => array( 'https://example.org/fonts/piazzolla500.ttf' ), + 'fontStyle' => 'normal', + 'fontWeight' => '400', + ), + ), + ), + array( + 'fontFamily' => 'Lobster, sans-serif', + 'name' => 'Lobster', + 'slug' => 'lobster', + 'fontFace' => array( + array( + 'fontFamily' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster400.ttf' ), + 'fontStyle' => 'normal', + 'fontWeight' => '400', + ), + array( + 'fontFamily' => 'Lobster', + 'src' => array( 'https://example.org/fonts/lobster500.ttf' ), + 'fontStyle' => 'normal', + 'fontWeight' => '500', + ), + ), + ), + ); + + $expected_font_faces = array( + array( + array( + 'src' => array( 'https://example.org/fonts/piazzolla400.ttf' ), + 'font-family' => 'Piazzolla', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => array( 'https://example.org/fonts/piazzolla500.ttf' ), + 'font-family' => 'Piazzolla', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + ), + array( + array( + 'src' => array( 'https://example.org/fonts/lobster400.ttf' ), + 'font-family' => 'Lobster', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => array( 'https://example.org/fonts/lobster500.ttf' ), + 'font-family' => 'Lobster', + 'font-style' => 'normal', + 'font-weight' => '500', + ), + ), + ); + + if ( null === $data ) { + $data = array( + 'input' => $custom_theme_json_fonts, + 'expected' => $expected_font_faces, + ); + } + + if ( isset( $data[ $key ] ) ) { + return $data[ $key ]; + } + + return $data; + } + + public static function get_custom_style_variations( $key = '' ) { + static $data = null; + + $path = get_stylesheet_directory() . '/assets/fonts/'; + $uri = get_stylesheet_directory_uri() . '/assets/fonts/'; + $expected_font_families = array( + array( + array( + 'src' => array( + "{$path}dm-sans/DMSans-Regular.woff2", + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => array( + "{$path}dm-sans/DMSans-Bold.woff2", + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '700', + ), + ), + array( + array( + 'src' => array( + "{$path}open-sans/OpenSans-VariableFont_wdth,wght.ttf", + ), + 'font-family' => 'Open Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '400', + ), + array( + 'src' => array( + "{$path}open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf", + ), + 'font-family' => 'Open Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '400', + ), + ), + array( + array( + 'src' => array( + "{$path}dm-sans/DMSans-Medium.woff2", + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'normal', + 'font-weight' => '500', + ), + array( + 'src' => array( + "{$path}dm-sans/DMSans-Medium-Italic.woff2", + ), + 'font-family' => 'DM Sans', + 'font-stretch' => 'normal', + 'font-style' => 'italic', + 'font-weight' => '500', + ), + ), + ); + + $expected_styles = <<<CSS +@font-face{font-family:"DM Sans";font-style:normal;font-weight:400;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Regular.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:normal;font-weight:700;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Bold.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"Open Sans";font-style:normal;font-weight:400;font-display:fallback;src:url('{$uri}open-sans/OpenSans-VariableFont_wdth,wght.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:"Open Sans";font-style:italic;font-weight:400;font-display:fallback;src:url('{$uri}open-sans/OpenSans-Italic-VariableFont_wdth,wght.ttf') format('truetype');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:normal;font-weight:500;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Medium.woff2') format('woff2');font-stretch:normal;} +@font-face{font-family:"DM Sans";font-style:italic;font-weight:500;font-display:fallback;src:url('{$uri}dm-sans/DMSans-Medium-Italic.woff2') format('woff2');font-stretch:normal;} +CSS; + + if ( null === $data ) { + $data = array( + 'expected' => $expected_font_families, + 'expected_styles' => $expected_styles, + ); + } + + if ( isset( $data[ $key ] ) ) { + return $data[ $key ]; + } + + return $data; + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php new file mode 100644 index 0000000000000..c0d314d0ab4d8 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wpFontFace/generateAndPrint.php @@ -0,0 +1,40 @@ +<?php +/** + * Test case for WP_Font_Face::generate_and_print(). + * + * @package WordPress + * @subpackage Fonts + * + * @since 6.4.0 + * + * @group fonts + * @group fontface + * + * @covers WP_Font_Face::generate_and_print + */ +class Tests_Fonts_WPFontFace_GenerateAndPrint extends WP_UnitTestCase { + use WP_Font_Face_Tests_Datasets; + + public function test_should_not_generate_and_print_when_no_fonts() { + $font_face = new WP_Font_Face(); + $fonts = array(); + + $this->expectOutputString( '' ); + $font_face->generate_and_print( $fonts ); + } + + /** + * @dataProvider data_should_print_given_fonts + * + * @param array $fonts Prepared fonts. + * @param string $expected Expected CSS. + */ + public function test_should_generate_and_print_given_fonts( array $fonts, $expected ) { + $font_face = new WP_Font_Face(); + $style_element = "<style class='wp-fonts-local'>\n%s\n</style>\n"; + $expected_output = sprintf( $style_element, $expected ); + + $output = get_echo( array( $font_face, 'generate_and_print' ), array( $fonts ) ); + $this->assertEqualHTML( $expected_output, $output ); + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromStyleVariations.php b/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromStyleVariations.php new file mode 100644 index 0000000000000..0087dac087292 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromStyleVariations.php @@ -0,0 +1,71 @@ +<?php +/** + * Test case for WP_Font_Face_Resolver::get_fonts_from_style_variations(). + * + * @package WordPress + * @subpackage Fonts + * + * @since 6.7.0 + * + * @group fonts + * @group fontface + * + * @covers WP_Font_Face_Resolver::get_fonts_from_style_variations + */ +class Tests_Fonts_WPFontFaceResolver_GetFontsFromStyleVariations extends WP_Font_Face_UnitTestCase { + const FONTS_THEME = 'fonts-block-theme'; + + public static function set_up_before_class() { + self::$requires_switch_theme_fixtures = true; + + parent::set_up_before_class(); + } + + /** + * Ensure that an empty array is returned when the theme has no style variations. + * + * @ticket 62231 + */ + public function test_should_return_empty_array_when_theme_has_no_style_variations() { + switch_theme( 'block-theme' ); + + $fonts = WP_Font_Face_Resolver::get_fonts_from_style_variations(); + $this->assertIsArray( $fonts, 'Should return an array data type' ); + $this->assertEmpty( $fonts, 'Should return an empty array' ); + } + + /** + * Ensure that all variations are loaded from a theme. + * + * @ticket 62231 + */ + public function test_should_return_all_fonts_from_all_style_variations() { + switch_theme( static::FONTS_THEME ); + + $actual = WP_Font_Face_Resolver::get_fonts_from_style_variations(); + $expected = self::get_custom_style_variations( 'expected' ); + + $this->assertSame( $expected, $actual, 'All the fonts from the theme variations should be returned.' ); + } + + /** + * Ensure that file:./ is replaced in the src list. + * + * @ticket 62231 + */ + public function test_should_replace_src_file_placeholder() { + switch_theme( static::FONTS_THEME ); + + $fonts = WP_Font_Face_Resolver::get_fonts_from_style_variations(); + + // Check that the there is no theme relative url in the src list. + foreach ( $fonts as $family ) { + foreach ( $family as $font ) { + foreach ( $font['src'] as $src ) { + $src_basename = basename( $src ); + $this->assertStringNotContainsString( 'file:./', $src, "Font $src_basename should not contain the 'file:./' placeholder" ); + } + } + } + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromThemeJson.php b/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromThemeJson.php new file mode 100644 index 0000000000000..1e48c5ae7dd59 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wpFontFaceResolver/getFontsFromThemeJson.php @@ -0,0 +1,257 @@ +<?php +/** + * Test case for WP_Font_Face_Resolver::get_fonts_from_theme_json(). + * + * @package WordPress + * @subpackage Fonts + * + * @since 6.4.0 + * + * @group fonts + * @group fontface + * + * @covers WP_Font_Face_Resolver::get_fonts_from_theme_json + */ +class Tests_Fonts_WPFontFaceResolver_GetFontsFromThemeJson extends WP_Font_Face_UnitTestCase { + const FONTS_THEME = 'fonts-block-theme'; + + public static function set_up_before_class() { + self::$requires_switch_theme_fixtures = true; + + parent::set_up_before_class(); + } + + public function test_should_return_empty_array_when_no_fonts_defined_in_theme() { + switch_theme( 'block-theme' ); + + $fonts = WP_Font_Face_Resolver::get_fonts_from_theme_json(); + $this->assertIsArray( $fonts, 'Should return an array data type' ); + $this->assertEmpty( $fonts, 'Should return an empty array' ); + } + + public function test_should_return_all_fonts_from_theme() { + switch_theme( static::FONTS_THEME ); + + $actual = WP_Font_Face_Resolver::get_fonts_from_theme_json(); + $expected = $this->get_expected_fonts_for_fonts_block_theme( 'fonts' ); + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 60605 + */ + public function test_should_return_all_fonts_from_all_theme_origins() { + switch_theme( static::FONTS_THEME ); + + $add_custom_fonts = static function ( $theme_json_data ) { + $data = $theme_json_data->get_data(); + // Add font families to the custom origin of theme json. + $data['settings']['typography']['fontFamilies']['custom'] = self::get_custom_font_families( 'input' ); + return new WP_Theme_JSON_Data( $data ); + }; + + add_filter( 'wp_theme_json_data_theme', $add_custom_fonts ); + $actual = WP_Font_Face_Resolver::get_fonts_from_theme_json(); + remove_filter( 'wp_theme_json_data_theme', $add_custom_fonts ); + + $expected = array_merge( + $this->get_expected_fonts_for_fonts_block_theme( 'fonts' ), + $this->get_custom_font_families( 'expected' ) + ); + + $this->assertSame( $expected, $actual, 'Both the fonts from the theme and the custom origin should be returned.' ); + } + + /** + * @dataProvider data_should_replace_src_file_placeholder + * + * @param string $font_name Font's name. + * @param string $font_weight Font's weight. + * @param string $font_style Font's style. + * @param string $expected Expected src. + */ + public function test_should_replace_src_file_placeholder( $font_name, $font_weight, $font_style, $expected ) { + switch_theme( static::FONTS_THEME ); + + $fonts = WP_Font_Face_Resolver::get_fonts_from_theme_json(); + $fonts = array_merge( array(), ...array_map( 'array_values', $fonts ) ); + + $font = array_filter( + $fonts, + static function ( $font ) use ( $font_name, $font_weight, $font_style ) { + return $font['font-family'] === $font_name + && $font['font-weight'] === $font_weight + && $font['font-style'] === $font_style; + } + ); + + $font = reset( $font ); + + $expected = get_stylesheet_directory_uri() . $expected; + $actual = $font['src'][0]; + + $this->assertStringNotContainsString( 'file:./', $actual, 'Font src should not contain the "file:./" placeholder' ); + $this->assertSame( $expected, $actual, 'Font src should be an URL to its file' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_replace_src_file_placeholder() { + return array( + // Theme's theme.json. + 'DM Sans: 400 normal' => array( + 'font_name' => 'DM Sans', + 'font_weight' => '400', + 'font_style' => 'normal', + 'expected' => '/assets/fonts/dm-sans/DMSans-Regular.woff2', + ), + 'DM Sans: 400 italic' => array( + 'font_name' => 'DM Sans', + 'font_weight' => '400', + 'font_style' => 'italic', + 'expected' => '/assets/fonts/dm-sans/DMSans-Regular-Italic.woff2', + ), + 'DM Sans: 700 normal' => array( + 'font_name' => 'DM Sans', + 'font_weight' => '700', + 'font_style' => 'normal', + 'expected' => '/assets/fonts/dm-sans/DMSans-Bold.woff2', + ), + 'DM Sans: 700 italic' => array( + 'font_name' => 'DM Sans', + 'font_weight' => '700', + 'font_style' => 'italic', + 'expected' => '/assets/fonts/dm-sans/DMSans-Bold-Italic.woff2', + ), + 'Source Serif Pro: 200-900 normal' => array( + 'font_name' => 'Source Serif Pro', + 'font_weight' => '200 900', + 'font_style' => 'normal', + 'expected' => '/assets/fonts/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2', + ), + 'Source Serif Pro: 200-900 italic' => array( + 'font_name' => 'Source Serif Pro', + 'font_weight' => '200 900', + 'font_style' => 'italic', + 'expected' => '/assets/fonts/source-serif-pro/SourceSerif4Variable-Italic.ttf.woff2', + ), + ); + } + + /** + * @dataProvider data_should_get_font_family_name + * + * @param array $fonts Fonts to test. + * @param string $expected_name Expected font-family name. + */ + public function test_should_get_font_family_name( $fonts, $expected_name ) { + switch_theme( static::FONTS_THEME ); + + $replace_fonts = static function ( $theme_json_data ) use ( $fonts ) { + $data = $theme_json_data->get_data(); + + // Replace typography.fontFamilies. + $data['settings']['typography']['fontFamilies']['theme'] = $fonts; + + return new WP_Theme_JSON_Data( $data ); + }; + add_filter( 'wp_theme_json_data_theme', $replace_fonts ); + $fonts = WP_Font_Face_Resolver::get_fonts_from_theme_json(); + remove_filter( 'wp_theme_json_data_theme', $replace_fonts ); + + // flatten the array to make it easier to test. + $fonts = array_merge( array(), ...array_map( 'array_values', $fonts ) ); + + $fonts_found = array_filter( + $fonts, + function ( $font ) use ( $expected_name ) { + return $font['font-family'] === $expected_name; + } + ); + + $this->assertNotEmpty( $fonts_found, 'Expected font-family name not found in the array' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_get_font_family_name() { + $font_face = array( + array( + 'fontFamily' => 'DM Sans', + 'fontStretch' => 'normal', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'file:./assets/fonts/dm-sans/DMSans-Regular.woff2', + ), + ), + array( + 'fontFamily' => 'DM Sans', + 'fontStretch' => 'normal', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => array( + 'file:./assets/fonts/dm-sans/DMSans-Regular-Italic.woff2', + ), + ), + array( + 'fontFamily' => 'DM Sans', + 'fontStretch' => 'normal', + 'fontStyle' => 'italic', + 'fontWeight' => '700', + 'src' => array( + 'file:./assets/fonts/dm-sans/DMSans-Bold.woff2', + ), + ), + array( + 'fontFamily' => 'DM Sans', + 'fontStretch' => 'normal', + 'fontStyle' => 'italic', + 'fontWeight' => '700', + 'src' => array( + 'file:./assets/fonts/dm-sans/DMSans-Bold-Italic.woff2', + ), + ), + ); + + return array( + 'name declared' => array( + 'fonts' => array( + array( + 'fontFamily' => 'DM Sans', + 'name' => 'DM Sans Family', + 'slug' => 'dm-sans', + 'fontFace' => $font_face, + ), + ), + 'expected_name' => 'DM Sans', + ), + 'name not declared' => array( + 'fonts' => array( + array( + 'fontFamily' => 'DM Sans', + 'slug' => 'dm-sans', + 'fontFace' => $font_face, + ), + ), + 'expected_name' => 'DM Sans', + ), + 'fontFamily comma-separated list' => array( + 'fonts' => array( + array( + 'fontFamily' => '"DM Sans", sans-serif', + 'slug' => 'dm-sans', + 'fontFace' => $font_face, + ), + ), + 'expected_name' => 'DM Sans', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php b/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php new file mode 100644 index 0000000000000..a1fea3ac948e2 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wpPrintFontFaces.php @@ -0,0 +1,82 @@ +<?php +/** + * Test case for wp_print_font_faces(). + * + * @package WordPress + * @subpackage Fonts + * + * @since 6.4.0 + * + * @group fonts + * @group fontface + * + * @covers wp_print_font_faces + */ +class Tests_Fonts_WpPrintFontFaces extends WP_Font_Face_UnitTestCase { + const FONTS_THEME = 'fonts-block-theme'; + + public static function set_up_before_class() { + self::$requires_switch_theme_fixtures = true; + + parent::set_up_before_class(); + } + + public function test_should_not_print_when_no_fonts() { + switch_theme( 'block-theme' ); + + $this->expectOutputString( '' ); + wp_print_font_faces(); + } + + /** + * @dataProvider data_should_print_given_fonts + * + * @param array $fonts Fonts to process. + * @param string $expected Expected CSS. + */ + public function test_should_print_given_fonts( array $fonts, $expected ) { + $expected_output = $this->get_expected_styles_output( $expected ); + + $output = get_echo( 'wp_print_font_faces', array( $fonts ) ); + $this->assertEqualHTML( $expected_output, $output ); + } + + public function test_should_escape_tags() { + $fonts = array( + 'Source Serif Pro' => array( + array( + 'src' => array( 'http://example.com/assets/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2' ), + 'font-family' => 'Source Serif Pro', + 'font-style' => 'normal', + 'font-weight' => '200 900', + 'font-stretch' => '</style><script>console.log("Hello")</script><style>', + ), + ), + ); + + $expected_output = <<<CSS +<style class='wp-fonts-local'> +@font-face{font-family:"Source Serif Pro";font-style:normal;font-weight:200 900;font-display:fallback;src:url('http://example.com/assets/source-serif-pro/SourceSerif4Variable-Roman.ttf.woff2') format('woff2');font-stretch:;} +</style> + +CSS; + + $output = get_echo( 'wp_print_font_faces', array( $fonts ) ); + $this->assertEqualHTML( $expected_output, $output ); + } + + public function test_should_print_fonts_in_merged_data() { + switch_theme( static::FONTS_THEME ); + + $expected = $this->get_expected_fonts_for_fonts_block_theme( 'font_face_styles' ); + $expected_output = $this->get_expected_styles_output( $expected ); + + $output = get_echo( 'wp_print_font_faces' ); + $this->assertEqualHTML( $expected_output, $output ); + } + + private function get_expected_styles_output( $styles ) { + $style_element = "<style class='wp-fonts-local'>\n%s\n</style>\n"; + return sprintf( $style_element, $styles ); + } +} diff --git a/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php b/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php new file mode 100644 index 0000000000000..664404d552b45 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-face/wpPrintFontFacesFromStyleVariations.php @@ -0,0 +1,54 @@ +<?php +/** + * Test case for wp_print_font_faces_from_style_variations(). + * + * @package WordPress + * @subpackage Fonts + * + * @since 6.7.0 + * + * @group fonts + * @group fontface + * + * @covers wp_print_font_faces_from_style_variations + */ +class Tests_Fonts_WpPrintFontFacesFromStyleVariations extends WP_Font_Face_UnitTestCase { + const FONTS_THEME = 'fonts-block-theme'; + + public static function set_up_before_class() { + parent::set_up_before_class(); + self::$requires_switch_theme_fixtures = true; + } + + /** + * Ensure that no fonts are printed when the theme has no fonts. + * + * @ticket 62231 + */ + public function test_should_not_print_when_no_fonts() { + switch_theme( 'block-theme' ); + + $this->expectOutputString( '' ); + wp_print_font_faces_from_style_variations(); + } + + /** + * Ensure that all fonts are printed from the theme style variations. + * + * @ticket 62231 + */ + public function test_should_print_fonts_in_style_variations() { + switch_theme( static::FONTS_THEME ); + + $expected = $this->get_custom_style_variations( 'expected_styles' ); + $expected_output = $this->get_expected_styles_output( $expected ); + + $output = get_echo( 'wp_print_font_faces_from_style_variations' ); + $this->assertEqualHTML( $expected_output, $output ); + } + + private function get_expected_styles_output( $styles ) { + $style_element = "<style class='wp-fonts-local'>\n%s\n</style>\n"; + return sprintf( $style_element, $styles ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php new file mode 100644 index 0000000000000..3850c4fd1be05 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/fontLibraryHooks.php @@ -0,0 +1,87 @@ +<?php +/** + * Test deleting wp_font_family and wp_font_face post types. + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + */ +class Tests_Fonts_FontLibraryHooks extends WP_UnitTestCase { + + public function test_deleting_font_family_deletes_child_font_faces() { + $font_family_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $font_family_id, + ) + ); + $other_font_family_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_family', + ) + ); + $other_font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => $other_font_family_id, + ) + ); + + wp_delete_post( $font_family_id, true ); + + $this->assertNull( get_post( $font_face_id ), 'Font face post should also have been deleted.' ); + $this->assertNotNull( get_post( $other_font_face_id ), 'The other post should exist.' ); + } + + public function test_deleting_font_faces_deletes_associated_font_files() { + list( $font_face_id, $font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.woff2' ); + list( , $other_font_path ) = $this->create_font_face_with_file( 'OpenSans-Regular.ttf' ); + + wp_delete_post( $font_face_id, true ); + + $this->assertFileDoesNotExist( $font_path, 'The font file should have been deleted when the post was deleted.' ); + $this->assertFileExists( $other_font_path, 'The other font file should exist.' ); + } + + protected function create_font_face_with_file( $filename ) { + $font_face_id = self::factory()->post->create( + array( + 'post_type' => 'wp_font_face', + ) + ); + + $font_file = $this->upload_font_file( $filename ); + + // Make sure the font file uploaded successfully. + $this->assertFalse( $font_file['error'] ); + + $font_path = $font_file['file']; + $font_filename = basename( $font_path ); + add_post_meta( $font_face_id, '_wp_font_face_file', $font_filename ); + + return array( $font_face_id, $font_path ); + } + + protected function upload_font_file( $font_filename ) { + $font_file_path = DIR_TESTDATA . '/fonts/' . $font_filename; + + add_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); + add_filter( 'upload_dir', '_wp_filter_font_directory' ); + $font_file = wp_upload_bits( + $font_filename, + null, + file_get_contents( $font_file_path ) + ); + remove_filter( 'upload_dir', '_wp_filter_font_directory' ); + remove_filter( 'upload_mimes', array( 'WP_Font_Utils', 'get_allowed_font_mime_types' ) ); + + return $font_file; + } +} diff --git a/tests/phpunit/tests/fonts/font-library/postTypes.php b/tests/phpunit/tests/fonts/font-library/postTypes.php new file mode 100644 index 0000000000000..7b24163e46705 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/postTypes.php @@ -0,0 +1,45 @@ +<?php +/** + * Test the wp_font_family and wp_font_face post types. + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + */ +class Tests_Fonts_Post_Types extends WP_UnitTestCase { + /** + * @ticket 41172 + */ + public function test_wp_font_family_does_not_support_autosaves() { + $this->assertFalse( post_type_supports( 'wp_font_family', 'autosave' ) ); + } + + /** + * @ticket 41172 + */ + public function test_wp_font_face_does_not_support_autosaves() { + $this->assertFalse( post_type_supports( 'wp_font_face', 'autosave' ) ); + } + + /** + * @ticket 41172 + */ + public function test_wp_font_family_does_not_have_an_autosave_controller() { + $post_type_object = get_post_type_object( 'wp_font_family' ); + $controller = $post_type_object->get_autosave_rest_controller(); + + $this->assertNull( $controller ); + } + + /** + * @ticket 41172 + */ + public function test_wp_font_face_does_not_have_an_autosave_controller() { + $post_type_object = get_post_type_object( 'wp_font_face' ); + $controller = $post_type_object->get_autosave_rest_controller(); + + $this->assertNull( $controller ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php new file mode 100644 index 0000000000000..a0693ce341456 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/__construct.php @@ -0,0 +1,26 @@ +<?php +/** + * Test WP_Font_Collection constructor. + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Collection::__construct + */ +class Tests_Fonts_WpFontCollection_Construct extends WP_UnitTestCase { + + public function test_should_do_it_wrong_with_invalid_slug() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::__construct' ); + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock ' ), + ); + + $collection = new WP_Font_Collection( 'slug with spaces', $mock_collection_data ); + + $this->assertSame( 'slug-with-spaces', $collection->slug, 'Slug is not sanitized.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php new file mode 100644 index 0000000000000..97ea664d4867d --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontCollection/getData.php @@ -0,0 +1,400 @@ +<?php +/** + * Test WP_Font_Collection::get_data. + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Collection::get_data + */ +class Tests_Fonts_WpFontCollection_GetData extends WP_UnitTestCase { + + private static $mock_collection_data; + + /** + * @dataProvider data_create_font_collection + * + * @param string $slug Font collection slug. + * @param array $config Font collection config. + * @param array $expected_data Expected collection data. + */ + public function test_should_get_data_from_config_array( $slug, $config, $expected_data ) { + $collection = new WP_Font_Collection( $slug, $config ); + $data = $collection->get_data(); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertSame( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * @dataProvider data_create_font_collection + * + * @param string $slug Font collection slug. + * @param array $config Font collection config. + * @param array $expected_data Expected collection data. + */ + public function test_should_get_data_from_json_file( $slug, $config, $expected_data ) { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, wp_json_encode( $config ) ); + + $collection = new WP_Font_Collection( + $slug, + array_merge( + $config, + array( 'font_families' => $mock_file ) + ) + ); + $data = $collection->get_data(); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertEqualSetsWithIndex( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * @dataProvider data_create_font_collection + * + * @param string $slug Font collection slug. + * @param array $config Font collection config. + * @param array $expected_data Expected collection data. + */ + public function test_should_get_data_from_json_url( $slug, $config, $expected_data ) { + add_filter( 'pre_http_request', array( $this, 'mock_request' ), 10, 3 ); + + self::$mock_collection_data = $config; + $collection = new WP_Font_Collection( + $slug, + array_merge( + $config, + array( + 'font_families' => 'https://example.com/fonts/mock-font-collection.json', + ) + ) + ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request' ) ); + + $this->assertSame( $slug, $collection->slug, 'The slug should match.' ); + $this->assertEqualSetsWithIndex( $expected_data, $data, 'The collection data should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_font_collection() { + return array( + 'font collection with required data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + 'expected_data' => array( + 'description' => '', + 'categories' => array(), + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + ), + + 'font collection with all data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection', + 'description' => 'My collection description', + 'font_families' => array( array() ), + 'categories' => array(), + ), + 'expected_data' => array( + 'description' => 'My collection description', + 'categories' => array(), + 'name' => 'My Collection', + 'font_families' => array( array() ), + ), + ), + + 'font collection with risky data' => array( + 'slug' => 'my-collection', + 'config' => array( + 'name' => 'My Collection<script>alert("xss")</script>', + 'description' => 'My collection description<script>alert("xss")</script>', + 'font_families' => array( + array( + 'font_family_settings' => array( + 'fontFamily' => 'Open Sans, sans-serif<script>alert("xss")</script>', + 'slug' => 'open-sans', + 'name' => 'Open Sans<script>alert("xss")</script>', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'https://example.com/src-as-string.ttf?a=<script>alert("xss")</script>', + ), + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'https://example.com/src-as-array.woff2?a=<script>alert("xss")</script>', + 'https://example.com/src-as-array.ttf', + ), + ), + ), + 'unwanted_property' => 'potentially evil value', + ), + 'categories' => array( 'sans-serif<script>alert("xss")</script>' ), + ), + ), + 'categories' => array( + array( + 'name' => 'Mock col<script>alert("xss")</script>', + 'slug' => 'mock-col<script>alert("xss")</script>', + 'unwanted_property' => 'potentially evil value', + ), + ), + 'unwanted_property' => 'potentially evil value', + ), + 'expected_data' => array( + 'description' => 'My collection description', + 'categories' => array( + array( + 'name' => 'Mock col', + 'slug' => 'mock-colalertxss', + ), + ), + 'name' => 'My Collection', + 'font_families' => array( + array( + 'font_family_settings' => array( + 'fontFamily' => '"Open Sans", sans-serif', + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => 'https://example.com/src-as-string.ttf?a=', + ), + array( + 'fontFamily' => 'Open Sans', + 'fontStyle' => 'normal', + 'fontWeight' => '400', + 'src' => array( + 'https://example.com/src-as-array.woff2?a=', + 'https://example.com/src-as-array.ttf', + ), + ), + ), + ), + 'categories' => array( 'sans-serifalertxss' ), + ), + ), + ), + ), + ); + } + + /** + * @dataProvider data_should_error_when_missing_properties + * + * @param array $config Font collection config. + */ + public function test_should_error_when_missing_properties( $config ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::sanitize_and_validate_data' ); + + $collection = new WP_Font_Collection( 'my-collection', $config ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when property is missing or invalid.' ); + $this->assertSame( + 'font_collection_missing_property', + $data->get_error_code(), + 'Incorrect error code when property is missing or invalid.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_error_when_missing_properties() { + return array( + 'missing name' => array( + 'config' => array( + 'font_families' => array( 'mock' ), + ), + ), + 'empty name' => array( + 'config' => array( + 'name' => '', + 'font_families' => array( 'mock' ), + ), + ), + 'missing font_families' => array( + 'config' => array( + 'name' => 'My Collection', + ), + ), + 'empty font_families' => array( + 'config' => array( + 'name' => 'My Collection', + 'font_families' => array(), + ), + ), + ); + } + + public function test_should_error_with_invalid_json_file_path() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + $collection = new WP_Font_Collection( + 'my-collection', + array( + 'name' => 'My collection', + 'font_families' => 'non-existing.json', + ) + ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when invalid file path is provided.' ); + $this->assertSame( + 'font_collection_json_missing', + $data->get_error_code(), + 'Incorrect error code when invalid file path is provided.' + ); + } + + public function test_should_error_with_invalid_json_from_file() { + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, 'invalid-json' ); + + $collection = new WP_Font_Collection( + 'my-collection', + array( + 'name' => 'Invalid collection', + 'font_families' => $mock_file, + ) + ); + + // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged -- Testing error response returned by `load_from_json`, not the underlying error from `wp_json_file_decode`. + $data = @$collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned with invalid json file contents.' ); + $this->assertSame( + 'font_collection_decode_error', + $data->get_error_code(), + 'Incorrect error code with invalid json file contents.' + ); + } + + public function test_should_error_with_invalid_url() { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + $collection = new WP_Font_Collection( + 'my-collection', + array( + 'name' => 'Invalid collection', + 'font_families' => 'not-a-url', + ) + ); + $data = $collection->get_data(); + + $this->assertWPError( $data, 'Error is not returned when invalid url is provided.' ); + $this->assertSame( + 'font_collection_json_missing', + $data->get_error_code(), + 'Incorrect error code when invalid url is provided.' + ); + } + + public function test_should_error_with_unsuccessful_response_status() { + add_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ), 10, 3 ); + + $collection = new WP_Font_Collection( + 'my-collection', + array( + 'name' => 'Missing collection', + 'font_families' => 'https://example.com/fonts/missing-collection.json', + ) + ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request_unsuccessful_response' ) ); + + $this->assertWPError( $data, 'Error is not returned when response is unsuccessful.' ); + $this->assertSame( + 'font_collection_request_error', + $data->get_error_code(), + 'Incorrect error code when response is unsuccessful.' + ); + } + + public function test_should_error_with_invalid_json_from_url() { + add_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ), 10, 3 ); + + $collection = new WP_Font_Collection( + 'my-collection', + array( + 'name' => 'Invalid collection', + 'font_families' => 'https://example.com/fonts/invalid-collection.json', + ) + ); + $data = $collection->get_data(); + + remove_filter( 'pre_http_request', array( $this, 'mock_request_invalid_json' ) ); + + $this->assertWPError( $data, 'Error is not returned when response is invalid json.' ); + $this->assertSame( + 'font_collection_decode_error', + $data->get_error_code(), + 'Incorrect error code when response is invalid json.' + ); + } + + public function mock_request( $preempt, $args, $url ) { + if ( 'https://example.com/fonts/mock-font-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => wp_json_encode( self::$mock_collection_data ), + 'response' => array( + 'code' => 200, + ), + ); + } + + public function mock_request_unsuccessful_response( $preempt, $args, $url ) { + if ( 'https://example.com/fonts/missing-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => '', + 'response' => array( + 'code' => 404, + ), + ); + } + + public function mock_request_invalid_json( $preempt, $args, $url ) { + if ( 'https://example.com/fonts/invalid-collection.json' !== $url ) { + return false; + } + + return array( + 'body' => 'invalid', + 'response' => array( + 'code' => 200, + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php new file mode 100644 index 0000000000000..135329e5add73 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/base.php @@ -0,0 +1,25 @@ +<?php +/** + * Test Case for WP_Font_Library tests. + * + * @package WordPress + * @subpackage Font Library + */ +abstract class WP_Font_Library_UnitTestCase extends WP_UnitTestCase { + public function reset_font_collections() { + $collections = WP_Font_Library::get_instance()->get_font_collections(); + foreach ( $collections as $slug => $collection ) { + WP_Font_Library::get_instance()->unregister_font_collection( $slug ); + } + } + + public function set_up() { + parent::set_up(); + $this->reset_font_collections(); + } + + public function tear_down() { + parent::tear_down(); + $this->reset_font_collections(); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php new file mode 100644 index 0000000000000..e01cc2c1a105a --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollection.php @@ -0,0 +1,30 @@ +<?php +/** + * Test WP_Font_Library::get_font_collections(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Library::get_font_collection + */ +class Tests_Fonts_WpFontLibrary_GetFontCollection extends WP_Font_Library_UnitTestCase { + + public function test_should_get_font_collection() { + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + wp_register_font_collection( 'my-font-collection', $mock_collection_data ); + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'my-font-collection' ); + $this->assertInstanceOf( 'WP_Font_Collection', $font_collection ); + } + + public function test_should_get_no_font_collection_if_the_slug_is_not_registered() { + $font_collection = WP_Font_Library::get_instance()->get_font_collection( 'not-registered-font-collection' ); + $this->assertNull( $font_collection ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php new file mode 100644 index 0000000000000..f5ca6389b8ff5 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/getFontCollections.php @@ -0,0 +1,34 @@ +<?php +/** + * Test WP_Font_Library::get_font_collections(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Library::get_font_collections + */ +class Tests_Fonts_WpFontLibrary_GetFontCollections extends WP_Font_Library_UnitTestCase { + public function test_should_get_an_empty_list() { + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertEmpty( $font_collections, 'Should return an empty array.' ); + } + + public function test_should_get_mock_font_collection() { + $my_font_collection_config = array( + 'name' => 'My Font Collection', + 'description' => 'Demo about how to a font collection to your WordPress Font Library.', + 'font_families' => array( 'mock' ), + ); + + WP_Font_Library::get_instance()->register_font_collection( 'my-font-collection', $my_font_collection_config ); + + $font_collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertNotEmpty( $font_collections, 'Should return an array of font collections.' ); + $this->assertCount( 1, $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.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php new file mode 100644 index 0000000000000..d3b0f126e2e7e --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/registerFontCollection.php @@ -0,0 +1,40 @@ +<?php +/** + * Test WP_Font_Library::register_font_collection(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Library::register_font_collection + */ +class Tests_Fonts_WpFontLibrary_RegisterFontCollection extends WP_Font_Library_UnitTestCase { + public function test_should_register_font_collection() { + $config = array( + 'name' => 'My Collection', + 'font_families' => array( 'mock' ), + ); + + $collection = WP_Font_Library::get_instance()->register_font_collection( 'my-collection', $config ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection ); + } + + public function test_should_return_error_if_slug_is_repeated() { + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + // Register first collection. + $collection1 = WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); + $this->assertInstanceOf( 'WP_Font_Collection', $collection1, 'A collection should be registered.' ); + + // Expects a _doing_it_wrong notice. + $this->setExpectedIncorrectUsage( 'WP_Font_Library::register_font_collection' ); + + // Try to register a second collection with same slug. + WP_Font_Library::get_instance()->register_font_collection( 'my-collection-1', $mock_collection_data ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php new file mode 100644 index 0000000000000..ddb0fa91c1d60 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontLibrary/unregisterFontCollection.php @@ -0,0 +1,46 @@ +<?php +/** + * Test WP_Font_Library::unregister_font_collection(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Library::unregister_font_collection + */ +class Tests_Fonts_WpFontLibrary_UnregisterFontCollection extends WP_Font_Library_UnitTestCase { + + public function test_should_unregister_font_collection() { + $mock_collection_data = array( + 'name' => 'Test Collection', + 'font_families' => array( 'mock' ), + ); + + // Registers two mock font collections. + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-1', $mock_collection_data ); + WP_Font_Library::get_instance()->register_font_collection( 'mock-font-collection-2', $mock_collection_data ); + + // Unregister mock font collection. + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-1' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertArrayNotHasKey( 'mock-font-collection-1', $collections, 'Font collection was not unregistered.' ); + $this->assertArrayHasKey( 'mock-font-collection-2', $collections, 'Font collection was unregistered by mistake.' ); + + // Unregisters remaining mock font collection. + WP_Font_Library::get_instance()->unregister_font_collection( 'mock-font-collection-2' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertArrayNotHasKey( 'mock-font-collection-2', $collections, 'Mock font collection was not unregistered.' ); + + // Checks that all font collections were unregistered. + $this->assertEmpty( $collections, 'Font collections were not unregistered.' ); + } + + public function unregister_non_existing_collection() { + // Unregisters non-existing font collection. + WP_Font_Library::get_instance()->unregister_font_collection( 'non-existing-collection' ); + $collections = WP_Font_Library::get_instance()->get_font_collections(); + $this->assertEmpty( $collections, 'No collections should be registered.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php new file mode 100644 index 0000000000000..de0b02e63185e --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/getFontFaceSlug.php @@ -0,0 +1,92 @@ +<?php +/** + * Test WP_Font_Utils::get_font_face_slug(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Utils::get_font_face_slug + */ +class Tests_Fonts_WpFontUtils_GetFontFaceSlug extends WP_UnitTestCase { + /** + * @dataProvider data_get_font_face_slug_normalizes_values + * + * @param string[] $settings Settings to test. + * @param string $expected_slug Expected slug results. + */ + public function test_get_font_face_slug_normalizes_values( $settings, $expected_slug ) { + $slug = WP_Font_Utils::get_font_face_slug( $settings ); + + $this->assertSame( $expected_slug, $slug ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_font_face_slug_normalizes_values() { + return array( + 'Sets defaults' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts normal weight to 400' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Converts bold weight to 700' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontWeight' => 'bold', + ), + 'expected_slug' => 'open sans;normal;700;100%;U+0-10FFFF', + ), + 'Converts normal font-stretch to 100%' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans', + 'fontStretch' => 'normal', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes double quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => '"Open Sans"', + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes single quotes from fontFamilies' => array( + 'settings' => array( + 'fontFamily' => "'Open Sans'", + ), + 'expected_slug' => 'open sans;normal;400;100%;U+0-10FFFF', + ), + 'Removes spaces between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, serif', + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes tabs between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\tserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + 'Removes new lines between comma separated font families' => array( + 'settings' => array( + 'fontFamily' => "Open Sans,\nserif", + ), + 'expected_slug' => 'open sans,serif;normal;400;100%;U+0-10FFFF', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php new file mode 100644 index 0000000000000..ff6b083ecaebd --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFontFamily.php @@ -0,0 +1,63 @@ +<?php +/** + * Test WP_Font_Utils::sanitize_font_family(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Utils::sanitize_font_family + */ +class Tests_Fonts_WpFontUtils_SanitizeFontFamily extends WP_UnitTestCase { + + /** + * @dataProvider data_should_sanitize_font_family + * + * @param string $font_family Font family to test. + * @param string $expected Expected family. + */ + public function test_should_sanitize_font_family( $font_family, $expected ) { + $this->assertSame( + $expected, + WP_Font_Utils::sanitize_font_family( + $font_family + ) + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_sanitize_font_family() { + return array( + 'data_families_with_spaces_and_numbers' => array( + 'font_family' => 'Arial, Rock 3D , Open Sans,serif', + 'expected' => 'Arial, "Rock 3D", "Open Sans", serif', + ), + 'data_single_font_family' => array( + 'font_family' => 'Rock 3D', + 'expected' => '"Rock 3D"', + ), + 'data_many_spaces_and_existing_quotes' => array( + 'font_family' => 'Rock 3D serif, serif,sans-serif, "Open Sans"', + 'expected' => '"Rock 3D serif", serif, sans-serif, "Open Sans"', + ), + 'data_empty_family' => array( + 'font_family' => ' ', + 'expected' => '', + ), + 'data_font_family_with_whitespace_tags_new_lines' => array( + 'font_family' => " Rock 3D</style><script>alert('XSS');</script>\n ", + 'expected' => '"Rock 3D"', + ), + 'data_font_family_with_generic_names' => array( + 'font_family' => 'generic(kai), generic(font[name]), generic(fangsong), Rock 3D', + 'expected' => 'generic(kai), "generic(font[name])", generic(fangsong), "Rock 3D"', + ), + ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php new file mode 100644 index 0000000000000..88983fe15a14e --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontUtils/sanitizeFromSchema.php @@ -0,0 +1,310 @@ +<?php +/** + * Test WP_Font_Utils::sanitize_from_schema(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers WP_Font_Utils::sanitize_from_schema + */ +class Tests_Fonts_WpFontUtils_SanitizeFromSchema extends WP_UnitTestCase { + /** + * @dataProvider data_sanitize_from_schema + * + * @param array $data Data to sanitize. + * @param array $schema Schema to use for sanitization. + * @param array $expected Expected result. + */ + public function test_sanitize_from_schema( $data, $schema, $expected ) { + $result = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $this->assertSame( $result, $expected ); + } + + public function data_sanitize_from_schema() { + return array( + 'One level associative array' => array( + 'data' => array( + 'slug' => 'open - sans</style><script>alert("xss")</script>', + 'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>', + 'src' => 'https://wordpress.org/example.json</style><script>alert("xss")</script>', + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + ), + 'expected' => array( + 'slug' => 'open-sansalertxss', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + + 'Nested associative arrays' => array( + 'data' => array( + 'slug' => 'open - sans</style><script>alert("xss")</script>', + 'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>', + 'src' => 'https://wordpress.org/example.json</style><script>alert("xss")</script>', + 'nested' => array( + 'key1' => 'value1</style><script>alert("xss")</script>', + 'key2' => 'value2</style><script>alert("xss")</script>', + 'nested2' => array( + 'key3' => 'value3</style><script>alert("xss")</script>', + 'key4' => 'value4</style><script>alert("xss")</script>', + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + 'nested' => array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + 'nested2' => array( + 'key3' => 'sanitize_text_field', + 'key4' => 'sanitize_text_field', + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sansalertxss', + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + 'nested' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nested2' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + ), + ), + + 'Indexed arrays' => array( + 'data' => array( + 'slug' => 'oPeN SaNs', + 'enum' => array( + 'value1<script>alert("xss")</script>', + 'value2<script>alert("xss")</script>', + 'value3<script>alert("xss")</script>', + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'enum' => array( 'sanitize_text_field' ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'enum' => array( 'value1', 'value2', 'value3' ), + ), + ), + + 'Nested indexed arrays' => array( + 'data' => array( + 'slug' => 'OPEN-SANS', + 'name' => 'Open Sans</style><script>alert("xss")</script>', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + array( + 'fontFamily' => 'Open Sans, sans-serif</style><script>alert("xss")</script>', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'name' => 'sanitize_text_field', + 'fontFace' => array( + array( + 'fontFamily' => 'sanitize_text_field', + 'src' => 'sanitize_url', + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'name' => 'Open Sans', + 'fontFace' => array( + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + array( + 'fontFamily' => 'Open Sans, sans-serif', + 'src' => 'https://wordpress.org/example.json/stylescriptalert(xss)/script', + ), + ), + ), + ), + + 'Custom sanitization function' => array( + 'data' => array( + 'key1' => 'abc123edf456ghi789', + 'key2' => 'value2', + ), + 'schema' => array( + 'key1' => function ( $value ) { + // Remove the six first character. + return substr( $value, 6 ); + }, + 'key2' => function ( $value ) { + // Capitalize the value. + return strtoupper( $value ); + }, + ), + 'expected' => array( + 'key1' => 'edf456ghi789', + 'key2' => 'VALUE2', + ), + ), + + 'Null as schema value' => array( + 'data' => array( + 'key1' => 'value1<script>alert("xss")</script>', + 'key2' => 'value2', + 'nested' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + 'schema' => array( + 'key1' => null, + 'key2' => 'sanitize_text_field', + 'nested' => null, + ), + 'expected' => array( + 'key1' => 'value1<script>alert("xss")</script>', + 'key2' => 'value2', + 'nested' => array( + 'key3' => 'value3', + 'key4' => 'value4', + ), + ), + ), + + 'Keys to remove' => array( + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'unwanted1' => 'value', + 'unwanted2' => 'value', + 'nestedAssociative' => array( + 'key5' => 'value5', + 'unwanted3' => 'value', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'value7', + 'unwanted4' => 'value', + ), + array( + 'key6' => 'value7', + 'unwanted5' => 'value', + ), + ), + + ), + 'schema' => array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + 'nestedAssociative' => array( + 'key5' => 'sanitize_text_field', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'sanitize_text_field', + ), + ), + ), + 'expected' => array( + 'key1' => 'value1', + 'key2' => 'value2', + 'nestedAssociative' => array( + 'key5' => 'value5', + ), + 'nestedIndexed' => array( + array( + 'key6' => 'value7', + ), + array( + 'key6' => 'value7', + ), + ), + ), + ), + + 'With empty structure' => array( + 'data' => array( + 'slug' => 'open-sans', + 'nested' => array( + 'key1' => 'value</style><script>alert("xss")</script>', + 'nested2' => array( + 'key2' => 'value</style><script>alert("xss")</script>', + 'nested3' => array( + 'nested4' => array(), + ), + ), + ), + ), + 'schema' => array( + 'slug' => 'sanitize_title', + 'nested' => array( + 'key1' => 'sanitize_text_field', + 'nested2' => array( + 'key2' => 'sanitize_text_field', + 'nested3' => array( + 'key3' => 'sanitize_text_field', + 'nested4' => array( + 'key4' => 'sanitize_text_field', + ), + ), + ), + ), + ), + 'expected' => array( + 'slug' => 'open-sans', + 'nested' => array( + 'key1' => 'value', + 'nested2' => array( + 'key2' => 'value', + ), + ), + ), + ), + ); + } + + public function test_sanitize_from_schema_with_invalid_data() { + $data = 'invalid data'; + $schema = array( + 'key1' => 'sanitize_text_field', + 'key2' => 'sanitize_text_field', + ); + + $result = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $this->assertSame( $result, array() ); + } + + + public function test_sanitize_from_schema_with_invalid_schema() { + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + $schema = 'invalid schema'; + + $result = WP_Font_Utils::sanitize_from_schema( $data, $schema ); + + $this->assertSame( $result, array() ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpFontsDir.php b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php new file mode 100644 index 0000000000000..22208ca7b2e26 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpFontsDir.php @@ -0,0 +1,149 @@ +<?php +/** + * Test wp_get_font_dir(). + * + * @package WordPress + * @subpackage Font Library + * + * @group fonts + * @group font-library + * + * @covers ::wp_get_font_dir + */ +class Tests_Fonts_WpFontDir extends WP_UnitTestCase { + private static $dir_defaults; + + public static function set_up_before_class() { + parent::set_up_before_class(); + $upload_dir = wp_get_upload_dir(); + + static::$dir_defaults = array( + 'path' => untrailingslashit( $upload_dir['basedir'] ) . '/fonts', + 'url' => untrailingslashit( $upload_dir['baseurl'] ) . '/fonts', + 'subdir' => '', + 'basedir' => untrailingslashit( $upload_dir['basedir'] ) . '/fonts', + 'baseurl' => untrailingslashit( $upload_dir['baseurl'] ) . '/fonts', + 'error' => false, + ); + } + + /** + * Ensure the font directory is correct. + */ + public function test_fonts_dir() { + $font_dir = wp_get_font_dir(); + + $this->assertSame( $font_dir, static::$dir_defaults ); + } + + /** + * Ensure that the fonts directory is correct for a multisite installation. + * + * The main site will use the default location and others will follow a pattern of `/sites/{$blog_id}/fonts` + * + * @group multisite + * @group ms-required + */ + public function test_fonts_dir_for_multisite() { + $blog_id = self::factory()->blog->create(); + $main_site_upload_dir = wp_get_upload_dir(); + switch_to_blog( $blog_id ); + + $actual = wp_get_font_dir(); + $expected = array( + 'path' => untrailingslashit( $main_site_upload_dir['basedir'] ) . "/sites/{$blog_id}/fonts", + 'url' => untrailingslashit( $main_site_upload_dir['baseurl'] ) . "/sites/{$blog_id}/fonts", + 'subdir' => '', + 'basedir' => untrailingslashit( $main_site_upload_dir['basedir'] ) . "/sites/{$blog_id}/fonts", + 'baseurl' => untrailingslashit( $main_site_upload_dir['baseurl'] ) . "/sites/{$blog_id}/fonts", + 'error' => false, + ); + + // Restore blog prior to assertions. + restore_current_blog(); + $this->assertSameSets( $expected, $actual ); + } + + /** + * Ensure modifying the font directory via the 'font_dir' filter works. + */ + public function test_fonts_dir_with_filter() { + // Define a callback function to pass to the filter. + function set_new_values( $defaults ) { + $defaults['path'] = '/custom-path/fonts/my-custom-subdir'; + $defaults['url'] = 'http://example.com/custom-path/fonts/my-custom-subdir'; + $defaults['subdir'] = 'my-custom-subdir'; + $defaults['basedir'] = '/custom-path/fonts'; + $defaults['baseurl'] = 'http://example.com/custom-path/fonts'; + $defaults['error'] = false; + return $defaults; + } + + // Add the filter. + add_filter( 'font_dir', 'set_new_values' ); + + // Gets the fonts dir. + $font_dir = wp_get_font_dir(); + + $expected = array( + 'path' => '/custom-path/fonts/my-custom-subdir', + 'url' => 'http://example.com/custom-path/fonts/my-custom-subdir', + 'subdir' => 'my-custom-subdir', + 'basedir' => '/custom-path/fonts', + 'baseurl' => 'http://example.com/custom-path/fonts', + 'error' => false, + ); + + // Remove the filter. + remove_filter( 'font_dir', 'set_new_values' ); + + $this->assertSame( $expected, $font_dir, 'The wp_get_font_dir() method should return the expected values.' ); + + // Gets the fonts dir. + $font_dir = wp_get_font_dir(); + + $this->assertSame( static::$dir_defaults, $font_dir, 'The wp_get_font_dir() method should return the default values.' ); + } + + /** + * Ensure infinite loops are not triggered when filtering the font uploads directory. + * + * @ticket 60652 + */ + public function test_fonts_dir_filters_do_not_trigger_infinite_loop() { + /* + * Naive filtering of uploads directory to return font directory. + * + * This emulates the approach a plugin developer may take to + * add the filter when extending the font library functionality. + */ + add_filter( 'upload_dir', '_wp_filter_font_directory' ); + + add_filter( + 'upload_dir', + function ( $upload_dir ) { + static $count = 0; + ++$count; + // The filter may be applied a couple of times, at five iterations assume an infinite loop. + if ( $count >= 5 ) { + $this->fail( 'Filtering the uploads directory triggered an infinite loop.' ); + } + return $upload_dir; + }, + 5 + ); + + /* + * Filter the font directory to return the uploads directory. + * + * This emulates moving font files back to the uploads directory due + * to file system structure. + */ + add_filter( 'font_dir', 'wp_get_upload_dir' ); + + wp_get_upload_dir(); + + // This will never be hit if an infinite loop is triggered. + $this->assertTrue( true ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php new file mode 100644 index 0000000000000..5bad5435472d4 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontCollectionsController.php @@ -0,0 +1,300 @@ +<?php +/** + * Unit tests covering WP_REST_Font_Collections_Controller functionality. + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + * + * @group restapi + * @group fonts + * @group font-library + * + * @coversDefaultClass WP_REST_Font_Collections_Controller + */ +class Tests_REST_WpRestFontCollectionsController extends WP_Test_REST_Controller_Testcase { + protected static $admin_id; + protected static $editor_id; + protected static $mock_file; + + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + // Clear the font collections. + $collections = WP_Font_Library::get_instance()->get_font_collections(); + foreach ( $collections as $slug => $collection ) { + WP_Font_Library::get_instance()->unregister_font_collection( $slug ); + } + + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + $mock_file = wp_tempnam( 'my-collection-data-' ); + file_put_contents( $mock_file, '{"name": "Mock Collection", "font_families": [ "mock" ], "categories": [ "mock" ] }' ); + + wp_register_font_collection( + 'mock-col-slug', + array( + 'name' => 'My collection', + 'font_families' => $mock_file, + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + wp_unregister_font_collection( 'mock-col-slug' ); + } + + /** + * @covers WP_REST_Font_Collections_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertCount( 1, $routes['/wp/v2/font-collections'], 'Rest server has not the collections path initialized.' ); + $this->assertCount( 1, $routes['/wp/v2/font-collections/(?P<slug>[\/\w-]+)'], 'Rest server has not the collection path initialized.' ); + + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections'][0]['methods'], 'Rest server has not the GET method for collections initialized.' ); + $this->assertArrayHasKey( 'GET', $routes['/wp/v2/font-collections/(?P<slug>[\/\w-]+)'][0]['methods'], 'Rest server has not the GET method for collection initialized.' ); + } + + /** + * @covers WP_REST_Font_Collections_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections' ); + $response = rest_get_server()->dispatch( $request ); + $content = $response->get_data(); + $this->assertIsArray( $content ); + $this->assertSame( 200, $response->get_status() ); + } + + /** + * @dataProvider data_readable_http_methods + * @covers WP_REST_Font_Collections_Controller::get_items + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_should_only_return_valid_collections( $method ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + wp_set_current_user( self::$admin_id ); + wp_register_font_collection( + 'invalid-collection', + array( + 'name' => 'My collection', + 'font_families' => 'invalid-collection-file', + ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/font-collections' ); + $response = rest_get_server()->dispatch( $request ); + $content = $response->get_data(); + + wp_unregister_font_collection( 'invalid-collection' ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 1, $content, 'The response should only contain valid collections.' ); + return null; + } + + $this->assertSame( array(), $content, 'The response should be empty.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers, 'The "X-WP-Total" header should be present in the response.' ); + // Includes non-valid collections. + $this->assertSame( 2, $headers['X-WP-Total'], 'The "X-WP-Total" header value should be equal to 1.' ); + } + + /** + * @covers WP_REST_Font_Collections_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-collections/mock-col-slug' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + + $response_data = $response->get_data(); + $this->assertArrayHasKey( 'name', $response_data, 'Response data does not have the name key.' ); + $this->assertArrayHasKey( 'slug', $response_data, 'Response data does not have the slug key.' ); + $this->assertArrayHasKey( 'description', $response_data, 'Response data does not have the description key.' ); + $this->assertArrayHasKey( 'font_families', $response_data, 'Response data does not have the font_families key.' ); + $this->assertArrayHasKey( 'categories', $response_data, 'Response data does not have the categories key.' ); + + $this->assertIsString( $response_data['name'], 'name is not a string.' ); + $this->assertIsString( $response_data['slug'], 'slug is not a string.' ); + $this->assertIsString( $response_data['description'], 'description is not a string.' ); + + $this->assertIsArray( $response_data['font_families'], 'font_families is not an array.' ); + $this->assertIsArray( $response_data['categories'], 'categories is not an array.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $hook_name = 'rest_prepare_font_collection'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/mock-col-slug' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_slug( $method ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/non-existing-collection' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_font_collection_not_found', $response, 404 ); + } + + /** + * @dataProvider data_readable_http_methods + * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_collection( $method ) { + $this->setExpectedIncorrectUsage( 'WP_Font_Collection::load_from_json' ); + + wp_set_current_user( self::$admin_id ); + $slug = 'invalid-collection'; + wp_register_font_collection( + $slug, + array( + 'name' => 'My collection', + 'font_families' => 'invalid-collection-file', + ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/' . $slug ); + $response = rest_get_server()->dispatch( $request ); + + wp_unregister_font_collection( $slug ); + + $this->assertErrorResponse( 'font_collection_json_missing', $response, 500 ); + } + + /** + * @dataProvider data_readable_http_methods + * @covers WP_REST_Font_Collections_Controller::get_item + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_id_permission( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/font-collections/mock-col-slug' ); + + wp_set_current_user( 0 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not use test_create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not use test_update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not use test_delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not use test_prepare_item(). + } + + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-collections' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $properties = $data['schema']['properties']; + $this->assertCount( 5, $properties, 'There should be 5 properties in the response data schema.' ); + $this->assertArrayHasKey( 'slug', $properties, 'The slug property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'name', $properties, 'The name property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'description', $properties, 'The description property should exist in the response data schema.' ); + $this->assertArrayHasKey( 'font_families', $properties, 'The slug font_families should exist in the response data schema.' ); + $this->assertArrayHasKey( 'categories', $properties, 'The categories property should exist in the response data schema.' ); + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php new file mode 100644 index 0000000000000..19a2a6a86b8b0 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFacesController.php @@ -0,0 +1,1160 @@ +<?php +/** + * Unit tests covering WP_REST_Font_Faces_Controller_Test functionality. + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + * + * @group restapi + * @group fonts + * @group font-library + * + * @coversDefaultClass WP_REST_Font_Faces_Controller + */ +class Tests_REST_WpRestFontFacesController extends WP_Test_REST_Controller_Testcase { + protected static $admin_id; + protected static $editor_id; + + protected static $font_family_id; + protected static $other_font_family_id; + + protected static $font_face_id1; + protected static $font_face_id2; + + private static $post_ids_for_cleanup = array(); + + protected static $default_settings = array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ); + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post(); + self::$other_font_family_id = Tests_REST_WpRestFontFamiliesController::create_font_family_post(); + + self::$font_face_id1 = self::create_font_face_post( + self::$font_family_id, + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), + ) + ); + self::$font_face_id2 = self::create_font_face_post( + self::$font_family_id, + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '900', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), + ) + ); + + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + + self::$post_ids_for_cleanup = array(); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + + wp_delete_post( self::$font_family_id, true ); + wp_delete_post( self::$other_font_family_id, true ); + wp_delete_post( self::$font_face_id1, true ); + wp_delete_post( self::$font_face_id2, true ); + } + + public function tear_down() { + foreach ( self::$post_ids_for_cleanup as $post_id ) { + wp_delete_post( $post_id, true ); + } + self::$post_ids_for_cleanup = array(); + parent::tear_down(); + } + + public static function create_font_face_post( $parent_id, $settings = array() ) { + $settings = array_merge( self::$default_settings, $settings ); + $title = WP_Font_Utils::get_font_face_slug( $settings ); + $post_id = self::factory()->post->create( + wp_slash( + array( + 'post_type' => 'wp_font_face', + 'post_status' => 'publish', + 'post_title' => $title, + 'post_name' => sanitize_title( $title ), + 'post_content' => wp_json_encode( $settings ), + 'post_parent' => $parent_id, + ) + ) + ); + + self::$post_ids_for_cleanup[] = $post_id; + + return $post_id; + } + + /** + * @covers WP_REST_Font_Faces_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces', + $routes, + 'Font faces collection for the given font family does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + $this->assertArrayHasKey( + '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)', + $routes, + 'Single font face route for the given font family does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + } + + public function test_font_faces_no_autosave_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayNotHasKey( + '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<id>[\d]+)/autosaves', + $routes, + 'Font faces autosaves route exists.' + ); + $this->assertArrayNotHasKey( + '/wp/v2/font-families/(?P<font_family_id>[\d]+)/font-faces/(?P<parent>[\d]+)/autosaves/(?P<id>[\d]+)', + $routes, + 'Font faces autosaves by id route exists.' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // See test_get_context_param(). + } + + /** + * @dataProvider data_get_context_param + * + * @covers WP_REST_Font_Faces_Controller::get_context_param + * + * @param bool $single_route Whether to test a single route. + */ + public function test_get_context_param( $single_route ) { + $route = '/wp/v2/font-families/' . self::$font_family_id . '/font-faces'; + if ( $single_route ) { + $route .= '/' . self::$font_face_id1; + } + + $request = new WP_REST_Request( 'OPTIONS', $route ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $endpoint_data = $data['endpoints'][0]; + $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' ); + $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_context_param() { + return array( + 'Collection' => array( false ), + 'Single' => array( true ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200' ); + $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' ); + $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' ); + $this->check_font_face_data( $data[0], self::$font_face_id2, $data[0]['_links'] ); + $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' ); + $this->check_font_face_data( $data[1], self::$font_face_id1, $data[1]['_links'] ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_items + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_items + */ + public function test_get_items_missing_parent() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->check_font_face_data( $data, self::$font_face_id1, $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + */ + public function test_get_item_removes_extra_settings() { + $font_face_id = self::create_font_face_post( self::$font_family_id, array( 'extra' => array() ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertArrayNotHasKey( 'extra', $data['font_face_settings'], 'The extra property should exist in the font_face_settings data.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + */ + public function test_get_item_malformed_post_content_returns_empty_settings() { + $font_face_id = wp_insert_post( + array( + 'post_type' => 'wp_font_face', + 'post_parent' => self::$font_family_id, + 'post_status' => 'publish', + 'post_content' => 'invalid', + ) + ); + + self::$post_ids_for_cleanup[] = $font_face_id; + + $empty_settings = array( + 'fontFamily' => '', + 'src' => array(), + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertSame( $empty_settings, $data['font_face_settings'], 'The empty settings should exist in the font_face_settings data.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item_invalid_font_face_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item_missing_parent() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces/' . self::$font_face_id1 ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item_valid_parent_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_get_item_invalid_parent_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$other_font_family_id . '/font-faces/' . self::$font_face_id1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404 ); + + $expected_message = 'The font face does not belong to the specified font family with id of "' . self::$other_font_family_id . '".'; + $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + $files = $this->setup_font_file_upload( array( 'woff2' ) ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + 'src' => array_keys( $files )[0], + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->check_font_face_data( $data, $data['id'], $response->get_links() ); + $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) ); + + $settings = $data['font_face_settings']; + unset( $settings['src'] ); + $this->assertSame( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + ), + $settings, + 'The font_face_settings data should match the expected data.' + ); + + $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); + } + + /** + * Ensure that setting a subdirectory on font uploads stores and deletes files as expected. + * + * @ticket 61297 + * + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_sub_dir() { + wp_set_current_user( self::$admin_id ); + add_filter( + 'font_dir', + function ( $font_dir ) { + $subdir = '/subdir'; + $font_dir['subdir'] = $subdir; + $font_dir['path'] .= $subdir; + $font_dir['url'] .= $subdir; + return $font_dir; + } + ); + + $files = $this->setup_font_file_upload( array( 'woff2' ) ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + 'src' => array_keys( $files )[0], + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->check_font_face_data( $data, $data['id'], $response->get_links() ); + $this->check_file_meta( $data['id'], array( $data['font_face_settings']['src'] ) ); + + $settings = $data['font_face_settings']; + unset( $settings['src'] ); + $this->assertSame( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + ), + $settings, + 'The font_face_settings data should match the expected data.' + ); + + $expected_file_path = WP_CONTENT_DIR . '/uploads/fonts/subdir/' . reset( $files )['name']; + $expected_post_meta = 'subdir/' . reset( $files )['name']; + $this->assertFileExists( $expected_file_path, 'The font file should exist in the expected subdirectory.' ); + $this->assertSame( $expected_post_meta, get_post_meta( $data['id'], '_wp_font_face_file', true ), 'The post meta should match the expected subdirectory.' ); + $this->assertSame( self::$font_family_id, $data['parent'], 'The returned parent id should match the font family id.' ); + + // Delete the post. + wp_delete_post( $data['id'], true ); + $this->assertFileDoesNotExist( $expected_file_path, 'The font file should have been deleted when the post was deleted.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_with_multiple_font_files() { + wp_set_current_user( self::$admin_id ); + $files = $this->setup_font_file_upload( array( 'ttf', 'otf', 'woff', 'woff2' ) ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + 'src' => array_keys( $files ), + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->check_font_face_data( $data, $data['id'], $response->get_links() ); + $this->check_file_meta( $data['id'], $data['font_face_settings']['src'] ); + + $settings = $data['font_face_settings']; + $this->assertCount( 4, $settings['src'], 'There should be 4 items in the font_face_settings::src data.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_invalid_file_type() { + $image_file = DIR_TESTDATA . '/images/canola.jpg'; + $image_path = wp_tempnam( 'canola.jpg' ); + copy( $image_file, $image_path ); + + $files = array( + 'file-0' => array( + 'name' => 'canola.jpg', + 'full_path' => 'canola.jpg', + 'type' => 'font/woff2', + 'tmp_name' => $image_path, + 'error' => 0, + 'size' => filesize( $image_path ), + ), + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( + self::$default_settings, + array( + 'fontWeight' => '200', + 'src' => array_keys( $files )[0], + ) + ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_font_upload_invalid_file_type', $response, 400 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_with_url_src() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'normal', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ) + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->check_font_face_data( $data, $data['id'], $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_with_all_properties() { + wp_set_current_user( self::$admin_id ); + + $properties = array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '300 500', + 'fontStyle' => 'oblique 30deg 50deg', + 'fontDisplay' => 'swap', + 'fontStretch' => 'expanded', + 'ascentOverride' => '70%', + 'descentOverride' => '30%', + 'fontVariant' => 'normal', + 'fontFeatureSettings' => '"swsh" 2', + 'fontVariationSettings' => '"xhgt" 0.7', + 'lineGapOverride' => '10%', + 'sizeAdjust' => '90%', + 'unicodeRange' => 'U+0025-00FF, U+4??', + 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/open-sans/open-sans-400-normal.svg', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_face_settings', wp_json_encode( $properties ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + wp_delete_post( $data['id'], true ); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in the response data.' ); + $this->assertSame( $properties, $data['font_face_settings'], 'The font_face_settings should match the expected properties.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_missing_parent() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces' ); + $request->set_param( + 'font_face_settings', + wp_json_encode( array_merge( self::$default_settings, array( 'fontWeight' => '100' ) ) ) + ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + public function test_create_item_with_duplicate_properties() { + $settings = array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'fontStyle' => 'italic', + 'src' => home_url( '/wp-content/fonts/open-sans-italic-light.ttf' ), + ); + self::create_font_face_post( self::$font_family_id, $settings ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_duplicate_font_face', $response, 400, 'The response should return an error for "rest_duplicate_font_face" with 400 status.' ); + $expected_message = 'A font face matching those settings already exists.'; + $message = $response->as_error()->get_error_messages()[0]; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_request + */ + public function test_create_item_default_theme_json_version() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '200', + 'src' => 'https://fonts.gstatic.com/s/open-sans/v30/KFOkCnqEu92Fr1MmgWxPKTM1K9nz.ttf', + ) + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + wp_delete_post( $data['id'], true ); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' ); + $this->assertSame( WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The default theme.json version should match the latest version supported by the controller.' ); + } + + /** + * @dataProvider data_create_item_invalid_theme_json_version + * + * @covers WP_REST_Font_Faces_Controller::create_item + * + * @param int $theme_json_version Version input to test. + */ + public function test_create_item_invalid_theme_json_version( $theme_json_version ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', $theme_json_version ); + $request->set_param( 'font_face_settings', '' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_invalid_theme_json_version() { + return array( + array( 1 ), + array( 4 ), + ); + } + + /** + * @dataProvider data_create_item_invalid_settings + * + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + * + * @param mixed $settings Settings to test. + */ + public function test_create_item_invalid_settings( $settings ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_invalid_settings() { + return array( + 'Missing fontFamily' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Empty fontFamily' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Wrong fontFamily type' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ), + ), + 'Invalid fontDisplay' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontDisplay' => 'invalid' ) ), + ), + 'Missing src' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'src' => '' ) ), + ), + 'Empty src string' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => '' ) ), + ), + 'Empty src array' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => array() ) ), + ), + 'Empty src array values' => array( + 'settings' => array_merge( self::$default_settings, array( '', '' ) ), + ), + 'Wrong src type' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => 1234 ) ), + ), + 'Wrong src array types' => array( + 'settings' => array_merge( self::$default_settings, array( 'src' => array( 1234, 5678 ) ) ), + ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_invalid_settings_json() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_face_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_face_settings parameter must be a valid JSON string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_non_string_settings() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_face_settings', self::$default_settings ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_face_settings is not of type string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_invalid_file_src() { + $files = $this->setup_font_file_upload( array( 'woff2' ) ); + + wp_set_current_user( self::$admin_id ); + $src = 'invalid'; + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => $src ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_face_settings[src] value "' . $src . '" must be a valid URL or file reference.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::validate_create_font_face_settings + */ + public function test_create_item_missing_file_src() { + $files = $this->setup_font_file_upload( array( 'woff2', 'woff' ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( + 'font_face_settings', + wp_json_encode( + array_merge( self::$default_settings, array( 'src' => array( array_keys( $files )[0] ) ) ) + ) + ); + $request->set_file_params( $files ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'File ' . array_keys( $files )[1] . ' must be used in font_face_settings[src].'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_face_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @dataProvider data_sanitize_font_face_settings + * + * @covers WP_REST_Font_Face_Controller::sanitize_font_face_settings + * + * @param string $settings Settings to test. + * @param string $expected Expected settings result. + */ + public function test_create_item_sanitize_font_face_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $request->set_param( 'font_face_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + wp_delete_post( $data['id'], true ); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertSame( $expected, $data['font_face_settings'], 'The response font_face_settings should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_font_face_settings() { + return array( + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'fontFamily' => " Open Sans</style><script>alert('XSS');</script>\n ", + 'fontStyle' => " oblique 20deg 50deg</style><script>alert('XSS');</script>\n ", + 'fontWeight' => " 200</style><script>alert('XSS');</script>\n ", + 'src' => " https://example.com/</style><script>alert('XSS');</script> ", + 'fontStretch' => " expanded</style><script>alert('XSS');</script>\n ", + 'ascentOverride' => " 70%</style><script>alert('XSS');</script>\n ", + 'descentOverride' => " 30%</style><script>alert('XSS');</script>\n ", + 'fontVariant' => " normal</style><script>alert('XSS');</script>\n ", + 'fontFeatureSettings' => " \"swsh\" 2</style><script>alert('XSS');</script>\n ", + 'fontVariationSettings' => " \"xhgt\" 0.7</style><script>alert('XSS');</script>\n ", + 'lineGapOverride' => " 10%</style><script>alert('XSS');</script>\n ", + 'sizeAdjust' => " 90%</style><script>alert('XSS');</script>\n ", + 'unicodeRange' => " U+0025-00FF, U+4??</style><script>alert('XSS');</script>\n ", + 'preview' => " https://example.com/</style><script>alert('XSS');</script> ", + ), + 'expected' => array( + 'fontFamily' => '"Open Sans"', + 'fontStyle' => 'oblique 20deg 50deg', + 'fontWeight' => '200', + 'src' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + 'fontStretch' => 'expanded', + 'ascentOverride' => '70%', + 'descentOverride' => '30%', + 'fontVariant' => 'normal', + 'fontFeatureSettings' => '"swsh" 2', + 'fontVariationSettings' => '"xhgt" 0.7', + 'lineGapOverride' => '10%', + 'sizeAdjust' => '90%', + 'unicodeRange' => 'U+0025-00FF, U+4??', + 'preview' => 'https://example.com//stylescriptalert(\'XSS\');/script%20%20%20%20%20%20', + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'fontFamily' => '"Libre Barcode 128 Text"', + ), + ), + 'multiword font family name' => array( + 'settings' => array( + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'fontFamily' => '"B612 Mono"', + ), + ), + 'comma-separated font family names' => array( + 'settings' => array( + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), + ), + ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::create_item + */ + // public function test_create_item_no_permission() {} + + /** + * @covers WP_REST_Font_Faces_Controller::update_item + */ + public function test_update_item() { + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_no_route', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item() { + wp_set_current_user( self::$admin_id ); + $font_face_id = self::create_font_face_post( self::$font_family_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 201.' ); + $this->assertNull( get_post( $font_face_id ), 'The deleted post should not exist.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item_no_trash() { + wp_set_current_user( self::$admin_id ); + $font_face_id = self::create_font_face_post( self::$font_family_id ); + + // Attempt trashing. + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' ); + + $request->set_param( 'force', 'false' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' ); + + // Ensure the post still exists. + $post = get_post( $font_face_id ); + $this->assertNotEmpty( $post, 'The post should still exists.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item_invalid_font_face_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete + */ + public function test_delete_item_missing_parent() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/font-faces/' . self::$font_face_id1 ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item + */ + public function test_delete_item_invalid_parent_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$other_font_family_id . '/font-faces/' . self::$font_face_id1 ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_font_face_parent_id_mismatch', $response, 404, 'The response should return an error for "rest_font_face_parent_id_mismatch" with 404 status.' ); + + $expected_message = 'The font face does not belong to the specified font family with id of "' . self::$other_font_family_id . '".'; + $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::delete_item + */ + public function test_delete_item_no_permissions() { + $font_face_id = $this->create_font_face_post( self::$font_family_id ); + + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . $font_face_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::prepare_item_for_response + */ + public function test_prepare_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces/' . self::$font_face_id2 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->check_font_face_data( $data, self::$font_face_id2, $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families/' . self::$font_family_id . '/font-faces' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' ); + $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'theme_json_version', $properties, 'The theme_json_version property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'parent', $properties, 'The parent property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_face_settings', $properties, 'The font_face_settings property should exist in the schema::properties data.' ); + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_item_schema + */ + public function test_get_item_schema_font_face_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'The sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Faces_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Faces_Controller( 'wp_font_face' ) )->get_public_item_schema(); + $font_face_settings_schema = $schema['properties']['font_face_settings']; + + $this->assertArrayHasKey( 'properties', $font_face_settings_schema, 'font_face_settings schema is missing properties.' ); + $this->assertIsArray( $font_face_settings_schema['properties'], 'font_face_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_face_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } + } + + /** + * If WP_Theme_JSON::LATEST_SCHEMA is changed, the controller should be updated to handle any differences + * in `fontFace` structure to ensure support for the latest theme.json schema, and backwards compatibility + * for existing wp_font_face posts. + */ + public function test_controller_supports_latest_theme_json_version() { + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + } + + protected function check_font_face_data( $data, $post_id, $links ) { + self::$post_ids_for_cleanup[] = $post_id; + $post = get_post( $post_id ); + + $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' ); + $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' ); + + $this->assertArrayHasKey( 'parent', $data, 'The parent property should exist in response data.' ); + $this->assertSame( $post->post_parent, $data['parent'], 'The "parent" from the response data should match the post parent.' ); + + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' ); + $this->assertSame( WP_REST_Font_Faces_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The "theme_json_version" from the response data should match the latest version supported by the controller.' ); + + $this->assertArrayHasKey( 'font_face_settings', $data, 'The font_face_settings property should exist in response data.' ); + $this->assertSame( $post->post_content, wp_json_encode( $data['font_face_settings'] ), 'The encoded "font_face_settings" from the response data should match the post content.' ); + + $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces/' . $post->ID ); + $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent . '/font-faces' ); + $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->post_parent ); + $this->assertSame( $expected, $links['parent'][0]['href'], 'The links for a parent URL from the response data should match the parent\'s REST endpoint.' ); + } + + protected function check_file_meta( $font_face_id, $src_attributes ) { + $file_meta = get_post_meta( $font_face_id, '_wp_font_face_file' ); + + foreach ( $file_meta as $file ) { + $base_directory = wp_get_font_dir()['basedir']; + $this->assertStringStartsNotWith( $base_directory, $file, 'The base directory should not be stored in the post meta.' ); + } + } + + protected function setup_font_file_upload( $formats ) { + $files = array(); + foreach ( $formats as $format ) { + $font_file = DIR_TESTDATA . '/fonts/OpenSans-Regular.' . $format; + $font_path = wp_tempnam( 'OpenSans-Regular.' . $format ); + copy( $font_file, $font_path ); + + $files[ 'file-' . count( $files ) ] = array( + 'name' => 'OpenSans-Regular.' . $format, + 'full_path' => 'OpenSans-Regular.' . $format, + 'type' => 'font/' . $format, + 'tmp_name' => $font_path, + 'error' => 0, + 'size' => filesize( $font_path ), + ); + } + + return $files; + } +} diff --git a/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php new file mode 100644 index 0000000000000..fada1d1643890 --- /dev/null +++ b/tests/phpunit/tests/fonts/font-library/wpRestFontFamiliesController.php @@ -0,0 +1,1088 @@ +<?php +/** + * Unit tests covering WP_REST_Font_Families_Controller_Test functionality. + * + * @package WordPress + * @subpackage REST_API + * @since 6.5.0 + * + * @group restapi + * @group fonts + * @group font-library + * + * @coversDefaultClass WP_REST_Font_Families_Controller + */ +class Tests_REST_WpRestFontFamiliesController extends WP_Test_REST_Controller_Testcase { + protected static $admin_id; + protected static $editor_id; + + protected static $font_family_id1; + protected static $font_family_id2; + + protected static $font_face_id1; + protected static $font_face_id2; + + private static $post_ids_to_cleanup = array(); + + protected static $default_settings = array( + 'name' => 'Open Sans', + 'slug' => 'open-sans', + 'fontFamily' => '"Open Sans", sans-serif', + 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/open-sans/open-sans-400-normal.svg', + ); + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + + self::$font_family_id1 = self::create_font_family_post( + array( + 'name' => 'Open Sans', + 'slug' => 'open-sans', + 'fontFamily' => '"Open Sans", sans-serif', + 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/open-sans/open-sans-400-normal.svg', + ) + ); + self::$font_family_id2 = self::create_font_family_post( + array( + 'name' => 'Helvetica', + 'slug' => 'helvetica', + 'fontFamily' => 'Helvetica, Arial, sans-serif', + ) + ); + self::$font_face_id1 = Tests_REST_WpRestFontFacesController::create_font_face_post( + self::$font_family_id1, + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '400', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-medium.ttf' ), + ) + ); + self::$font_face_id2 = Tests_REST_WpRestFontFacesController::create_font_face_post( + self::$font_family_id1, + array( + 'fontFamily' => '"Open Sans"', + 'fontWeight' => '900', + 'fontStyle' => 'normal', + 'src' => home_url( '/wp-content/fonts/open-sans-bold.ttf' ), + ) + ); + + static::$post_ids_to_cleanup = array(); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + + wp_delete_post( self::$font_family_id1 ); + wp_delete_post( self::$font_family_id2 ); + wp_delete_post( self::$font_face_id1 ); + wp_delete_post( self::$font_face_id2 ); + } + + public function tear_down() { + foreach ( static::$post_ids_to_cleanup as $post_id ) { + wp_delete_post( $post_id, true ); + } + static::$post_ids_to_cleanup = array(); + + parent::tear_down(); + } + + public static function create_font_family_post( $settings = array() ) { + $settings = array_merge( self::$default_settings, $settings ); + $post_id = self::factory()->post->create( + wp_slash( + array( + 'post_type' => 'wp_font_family', + 'post_status' => 'publish', + 'post_title' => $settings['name'], + 'post_name' => $settings['slug'], + 'post_content' => wp_json_encode( + array( + 'fontFamily' => $settings['fontFamily'], + 'preview' => $settings['preview'], + ) + ), + ) + ) + ); + + static::$post_ids_to_cleanup[] = $post_id; + + return $post_id; + } + + /** + * @covers WP_REST_Font_Families_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/font-families', + $routes, + 'Font faces collection for the given font family does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/font-families'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + $this->assertArrayHasKey( + '/wp/v2/font-families/(?P<id>[\d]+)', + $routes, + 'Single font face route for the given font family does not exist' + ); + $this->assertCount( + 3, + $routes['/wp/v2/font-families/(?P<id>[\d]+)'], + 'Font faces collection for the given font family does not have exactly two elements' + ); + } + + public function test_font_families_no_autosave_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayNotHasKey( + '/wp/v2/font-families/(?P<id>[\d]+)/autosaves', + $routes, + 'Font families autosaves route exists.' + ); + $this->assertArrayNotHasKey( + '/wp/v2/font-families/(?P<parent>[\d]+)/autosaves/(?P<id>[\d]+)', + $routes, + 'Font families autosaves by id route exists.' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // See test_get_context_param(). + } + + /** + * @dataProvider data_get_context_param + * + * @covers WP_REST_Font_Families_Controller::get_context_param + * + * @param bool $single_route Whether to test a single route. + */ + public function test_get_context_param( $single_route ) { + $route = '/wp/v2/font-families'; + if ( $single_route ) { + $route .= '/' . self::$font_family_id1; + } + + $request = new WP_REST_Request( 'OPTIONS', $route ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $endpoint_data = $data['endpoints'][0]; + $this->assertArrayNotHasKey( 'allow_batch', $endpoint_data, 'The allow_batch property should not exist in the endpoint data.' ); + $this->assertSame( 'view', $endpoint_data['args']['context']['default'], 'The endpoint\'s args::context::default should be set to view.' ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $endpoint_data['args']['context']['enum'], 'The endpoint\'s args::context::enum should be set to [ view, embed, edit ].' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_context_param() { + return array( + 'Collection' => array( false ), + 'Single' => array( true ), + ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertCount( 2, $data, 'There should be 2 properties in the response data.' ); + $this->assertArrayHasKey( '_links', $data[0], 'The _links property should exist in the response data 0.' ); + $this->check_font_family_data( $data[0], self::$font_family_id2, $data[0]['_links'] ); + $this->assertArrayHasKey( '_links', $data[1], 'The _links property should exist in the response data 1.' ); + $this->check_font_family_data( $data[1], self::$font_family_id1, $data[1]['_links'] ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_items + */ + public function test_get_items_by_slug() { + $font_family = get_post( self::$font_family_id2 ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + $request->set_param( 'slug', $font_family->post_name ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertCount( 1, $data, 'There should be 1 property in the response data.' ); + $this->assertArrayHasKey( 'id', $data[0], 'The id property should exist in the response data.' ); + $this->assertSame( $font_family->ID, $data[0]['id'], 'The id should match the expected ID in the response data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_items + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->check_font_family_data( $data, self::$font_family_id1, $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response + */ + public function test_get_item_embedded_font_faces() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( '_embed', true ); + $response = rest_get_server()->dispatch( $request ); + $data = rest_get_server()->response_to_data( $response, true ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayHasKey( '_embedded', $data, 'The _embedded property should exist in the response data.' ); + $this->assertArrayHasKey( 'font_faces', $data['_embedded'], 'The font_faces property should exist in _embedded data.' ); + $this->assertCount( 2, $data['_embedded']['font_faces'], 'There should be 2 font_faces in the _embedded data.' ); + + foreach ( $data['_embedded']['font_faces'] as $font_face ) { + $this->assertArrayHasKey( 'id', $font_face, 'The id property should exist in the _embedded font_face data.' ); + + $font_face_request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 . '/font-faces/' . $font_face['id'] ); + $font_face_response = rest_get_server()->dispatch( $font_face_request ); + $font_face_data = rest_get_server()->response_to_data( $font_face_response, true ); + + $this->assertSame( $font_face_data, $font_face, 'The embedded font_face data should match when the data from a single request.' ); + } + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item + */ + public function test_get_item_removes_extra_settings() { + $font_family_id = self::create_font_family_post( array( 'fontFace' => array() ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertArrayNotHasKey( 'fontFace', $data['font_family_settings'], 'The fontFace property should not exist in the font_family_settings data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response + */ + public function test_get_item_malformed_post_content_returns_empty_settings() { + $font_family_id = wp_insert_post( + array( + 'post_type' => 'wp_font_family', + 'post_status' => 'publish', + 'post_content' => 'invalid', + ) + ); + + static::$post_ids_to_cleanup[] = $font_family_id; + + $empty_settings = array( + 'name' => '', + // Slug will default to the post id. + 'slug' => (string) $font_family_id, + 'fontFamily' => '', + 'preview' => '', + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( $empty_settings, $data['font_family_settings'], 'The empty settings should exist in the font_family_settings data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item + */ + public function test_get_item_invalid_font_family_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id1 ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 401, 'The response should return an error with a "rest_cannot_read" code and 401 status.' ); + + wp_set_current_user( self::$editor_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, 403, 'The response should return an error with a "rest_cannot_read" code and 403 status.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::create_item + */ + public function test_create_item() { + $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->check_font_family_data( $data, $data['id'], $response->get_links() ); + + $response_settings = $data['font_family_settings']; + $this->assertSame( $settings, $response_settings, 'The expected settings should exist in the font_family_settings data.' ); + $this->assertEmpty( $data['font_faces'], 'The font_faces should be empty or not exist in the response data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_request + */ + public function test_create_item_default_theme_json_version() { + $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + static::$post_ids_to_cleanup[] = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in the response data.' ); + $this->assertSame( WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The default theme.json version should match the latest version supported by the controller.' ); + } + + /** + * @dataProvider data_create_item_invalid_theme_json_version + * + * @covers WP_REST_Font_Families_Controller::create_item + * + * @param int $theme_json_version Version to test. + */ + public function test_create_item_invalid_theme_json_version( $theme_json_version ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', $theme_json_version ); + $request->set_param( 'font_family_settings', wp_json_encode( self::$default_settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_invalid_theme_json_version() { + return array( + array( 1 ), + array( 4 ), + ); + } + + /** + * @dataProvider data_create_item_with_default_preview + * + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param array $settings Settings to test. + */ + public function test_create_item_with_default_preview( $settings ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + static::$post_ids_to_cleanup[] = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $response_settings = $data['font_family_settings']; + $this->assertArrayHasKey( 'preview', $response_settings, 'The preview property should exist in the font_family_settings data.' ); + $this->assertSame( '', $response_settings['preview'], 'The preview data should be an empty string.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_with_default_preview() { + $default_settings = array( + 'name' => 'Open Sans', + 'slug' => 'open-sans-2', + 'fontFamily' => '"Open Sans", sans-serif', + ); + return array( + 'No preview param' => array( + 'settings' => $default_settings, + ), + 'Empty preview' => array( + 'settings' => array_merge( $default_settings, array( 'preview' => '' ) ), + ), + ); + } + + /** + * @dataProvider data_sanitize_font_family_settings + * + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. + */ + public function test_create_item_sanitize_font_family_settings( $settings, $expected ) { + $settings = array_merge( self::$default_settings, $settings ); + $expected = array_merge( self::$default_settings, $expected ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + static::$post_ids_to_cleanup[] = $data['id']; + + $this->assertSame( 201, $response->get_status(), 'The response status should be 201.' ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_font_family_settings() { + return array( + 'settings with tags, extra whitespace, new lines' => array( + 'settings' => array( + 'name' => " Opening Sans</style><script>alert('XSS');</script>\n ", + 'slug' => " OPENing SanS </style><script>alert('XSS');</script>\n ", + 'fontFamily' => " Opening Sans</style><script>alert('XSS');</script>\n ", + 'preview' => " https://example.com/</style><script>alert('XSS');</script> ", + ), + 'expected' => array( + 'name' => 'Opening Sans', + 'slug' => 'opening-sans-alertxss', + 'fontFamily' => '"Opening Sans"', + 'preview' => "https://example.com//stylescriptalert('XSS');/script%20%20%20%20%20%20", + ), + ), + 'multiword font family name with integer' => array( + 'settings' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => 'Libre Barcode 128 Text', + ), + 'expected' => array( + 'slug' => 'libre-barcode-128-text', + 'fontFamily' => '"Libre Barcode 128 Text"', + ), + ), + 'multiword font family name' => array( + 'settings' => array( + 'slug' => 'b612-mono', + 'fontFamily' => 'B612 Mono', + ), + 'expected' => array( + 'slug' => 'b612-mono', + 'fontFamily' => '"B612 Mono"', + ), + ), + 'comma-separated font family names' => array( + 'settings' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => 'Open Sans, Noto Sans, sans-serif', + ), + 'expected' => array( + 'slug' => 'open-sans-noto-sans', + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + ), + ), + ); + } + + /** + * @dataProvider data_create_item_invalid_settings + * + * @covers WP_REST_Font_Families_Controller::validate_create_font_face_settings + * + * @param array $settings Settings to test. + */ + public function test_create_item_invalid_settings( $settings ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_invalid_settings() { + return array( + 'Missing name' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'name' => '' ) ), + ), + 'Empty name' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => '' ) ), + ), + 'Wrong name type' => array( + 'settings' => array_merge( self::$default_settings, array( 'name' => 1234 ) ), + ), + 'Missing slug' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'slug' => '' ) ), + ), + 'Empty slug' => array( + 'settings' => array_merge( self::$default_settings, array( 'slug' => '' ) ), + ), + 'Wrong slug type' => array( + 'settings' => array_merge( self::$default_settings, array( 'slug' => 1234 ) ), + ), + 'Missing fontFamily' => array( + 'settings' => array_diff_key( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Empty fontFamily' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => '' ) ), + ), + 'Wrong fontFamily type' => array( + 'settings' => array_merge( self::$default_settings, array( 'fontFamily' => 1234 ) ), + ), + ); + } + + /** + * @covers WP_REST_Font_Family_Controller::validate_font_family_settings + */ + public function test_create_item_invalid_settings_json() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', 'invalid' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_family_settings parameter must be a valid JSON string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Family_Controller::validate_font_family_settings + */ + public function test_create_item_non_string_settings() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', self::$default_settings ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_family_settings is not of type string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Family_Controller::create_item + */ + public function test_create_item_with_duplicate_slug() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'theme_json_version', WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + $request->set_param( 'font_family_settings', wp_json_encode( array_merge( self::$default_settings, array( 'slug' => 'helvetica' ) ) ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_duplicate_font_family', $response, 400, 'The response should return an error for "rest_duplicate_font_family" with 400 status.' ); + $expected_message = 'A font family with slug "helvetica" already exists.'; + $message = $response->as_error()->get_error_messages()[0]; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::create_item + */ + public function test_create_item_no_permission() { + $settings = array_merge( self::$default_settings, array( 'slug' => 'open-sans-2' ) ); + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_create', $response, 401, 'The response should return an error for "rest_cannot_create" with 401 status.' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families' ); + $request->set_param( + 'font_family_settings', + wp_json_encode( + array( + 'name' => 'Open Sans', + 'slug' => 'open-sans', + 'fontFamily' => '"Open Sans", sans-serif', + 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/open-sans/open-sans-400-normal.svg', + ) + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_create', $response, 403, 'The response should return an error for "rest_cannot_create" with 403 status.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + + $settings = array( + 'name' => 'Open Sans', + 'fontFamily' => 'Open Sans, "Noto Sans", sans-serif', + 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/open-sans/open-sans-400-normal.svg', + ); + + $font_family_id = self::create_font_family_post( array( 'slug' => 'open-sans-2' ) ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( + 'font_family_settings', + wp_json_encode( $settings ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->check_font_family_data( $data, $font_family_id, $response->get_links() ); + + $expected_settings = array( + 'name' => $settings['name'], + 'slug' => 'open-sans-2', + 'fontFamily' => '"Open Sans", "Noto Sans", sans-serif', + 'preview' => $settings['preview'], + ); + $this->assertSame( $expected_settings, $data['font_family_settings'], 'The response font_family_settings should match expected settings.' ); + } + + /** + * @dataProvider data_update_item_individual_settings + * + * @covers WP_REST_Font_Families_Controller::update_item + * + * @param array $settings Settings to test. + */ + public function test_update_item_individual_settings( $settings ) { + wp_set_current_user( self::$admin_id ); + + $font_family_id = self::create_font_family_post(); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $key = key( $settings ); + $value = current( $settings ); + $this->assertArrayHasKey( $key, $data['font_family_settings'], 'The expected key should exist in the font_family_settings data.' ); + $this->assertSame( $value, $data['font_family_settings'][ $key ], 'The font_family_settings data should match.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_update_item_individual_settings() { + return array( + array( array( 'name' => 'Opened Sans' ) ), + array( array( 'fontFamily' => '"Opened Sans", sans-serif' ) ), + array( array( 'preview' => 'https://s.w.org/images/fonts/wp-7.0/previews/opened-sans/opened-sans-400-normal.svg' ) ), + // Empty preview is allowed. + array( array( 'preview' => '' ) ), + ); + } + + /** + * @dataProvider data_sanitize_font_family_settings + * + * @covers WP_REST_Font_Families_Controller::sanitize_font_family_settings + * + * @param string $settings Font family settings to test. + * @param string $expected Expected settings result. + */ + public function test_update_item_sanitize_font_family_settings( $settings, $expected ) { + // Unset/modify slug from the data provider, since we're updating rather than creating. + unset( $settings['slug'] ); + $initial_settings = array( 'slug' => 'open-sans-update' ); + $expected = array_merge( self::$default_settings, $expected, $initial_settings ); + + wp_set_current_user( self::$admin_id ); + $font_family_id = self::create_font_family_post( $initial_settings ); + static::$post_ids_to_cleanup[] = $font_family_id; + + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . $font_family_id ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( $expected, $data['font_family_settings'], 'The response font_family_settings should match.' ); + } + + /** + * @dataProvider data_update_item_invalid_settings + * + * @covers WP_REST_Font_Families_Controller::update_item + * + * @param array $settings Settings to test. + */ + public function test_update_item_empty_settings( $settings ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( + 'font_family_settings', + wp_json_encode( $settings ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_update_item_invalid_settings() { + return array( + 'Empty name' => array( + array( 'name' => '' ), + ), + 'Wrong name type' => array( + array( 'name' => 1234 ), + ), + 'Empty fontFamily' => array( + array( 'fontFamily' => '' ), + ), + 'Wrong fontFamily type' => array( + array( 'fontFamily' => 1234 ), + ), + ); + } + + /** + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item_update_slug_not_allowed() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( + 'font_family_settings', + wp_json_encode( array( 'slug' => 'new-slug' ) ) + ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_family_settings[slug] cannot be updated.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Family_Controller::validate_font_family_settings + */ + public function test_update_item_non_string_settings() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( 'font_family_settings', self::$default_settings ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400, 'The response should return an error for "rest_invalid_param" with 400 status.' ); + $expected_message = 'font_family_settings is not of type string.'; + $message = $response->as_error()->get_all_error_data()[0]['params']['font_family_settings']; + $this->assertSame( $expected_message, $message, 'The response error message should match.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item_invalid_font_family_id() { + $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404, 'The response should return an error for "rest_post_invalid_id" with 404 status.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::update_item + */ + public function test_update_item_no_permission() { + $settings = array_diff_key( self::$default_settings, array( 'slug' => '' ) ); + + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401, 'The response should return an error for "rest_cannot_edit" with 401 status for an invalid user.' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/font-families/' . self::$font_family_id1 ); + $request->set_param( 'font_family_settings', wp_json_encode( $settings ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403, 'The response should return an error for "rest_cannot_edit" with 403 status for a user without permission.' ); + } + + + /** + * @covers WP_REST_Font_Families_Controller::delete_item + */ + public function test_delete_item() { + wp_set_current_user( self::$admin_id ); + $font_family_id = self::create_font_family_post(); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); + $request['force'] = true; + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertNull( get_post( $font_family_id ), 'The post should not exist after deleting.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::delete_item + */ + public function test_delete_item_no_trash() { + wp_set_current_user( self::$admin_id ); + $font_family_id = self::create_font_family_post(); + + // Attempt trashing. + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'The response should return an error for "rest_trash_not_supported" with 501 status.' ); + + $request->set_param( 'force', 'false' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_trash_not_supported', $response, 501, 'When "force" is false, the response should return an error for "rest_trash_not_supported" with 501 status.' ); + + // Ensure the post still exists. + $post = get_post( $font_family_id ); + $this->assertNotEmpty( $post, 'The post should still exist.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::delete_item + */ + public function test_delete_item_invalid_font_family_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @covers WP_REST_Font_Families_Controller::delete_item + */ + public function test_delete_item_no_permissions() { + $font_family_id = self::create_font_family_post(); + + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 401, 'The response should return an error for "rest_cannot_delete" with 401 status for an invalid user.' ); + + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/font-families/' . $font_family_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, 403, 'The response should return an error for "rest_cannot_delete" with 403 status for a user without permission.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::prepare_item_for_response + */ + public function test_prepare_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/font-families/' . self::$font_family_id2 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->check_font_family_data( $data, self::$font_family_id2, $response->get_links() ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/font-families' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties, 'There should be 4 properties in the schema::properties data.' ); + $this->assertArrayHasKey( 'id', $properties, 'The id property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'theme_json_version', $properties, 'The theme_json_version property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_faces', $properties, 'The font_faces property should exist in the schema::properties data.' ); + $this->assertArrayHasKey( 'font_family_settings', $properties, 'The font_family_settings property should exist in the schema::properties data.' ); + } + + /** + * @covers WP_REST_Font_Families_Controller::get_item_schema + */ + public function test_get_item_schema_font_family_settings_should_all_have_sanitize_callbacks() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayHasKey( 'arg_options', $property, 'Setting schema should have arg_options.' ); + $this->assertArrayHasKey( 'sanitize_callback', $property['arg_options'], 'Setting schema should have a sanitize_callback.' ); + $this->assertIsCallable( $property['arg_options']['sanitize_callback'], 'That sanitize_callback value should be callable.' ); + } + } + + /** + * @covers WP_REST_Font_Families_Controller::get_public_item_schema + */ + public function test_get_public_item_schema_should_not_have_arg_options() { + $schema = ( new WP_REST_Font_Families_Controller( 'wp_font_family' ) )->get_public_item_schema(); + $font_family_settings_schema = $schema['properties']['font_family_settings']; + + $this->assertArrayHasKey( 'properties', $font_family_settings_schema, 'font_family_settings schema is missing properties.' ); + $this->assertIsArray( $font_family_settings_schema['properties'], 'font_family_settings properties should be an array.' ); + + // arg_options should be removed for each setting property. + foreach ( $font_family_settings_schema['properties'] as $property ) { + $this->assertArrayNotHasKey( 'arg_options', $property, 'arg_options should be removed from the schema for each setting.' ); + } + } + + /** + * If WP_Theme_JSON::LATEST_SCHEMA is changed, the controller should be updated to handle any differences + * in `fontFamilies` structure to ensure support for the latest theme.json schema, and backwards compatibility + * for existing wp_font_family posts. + */ + public function test_controller_supports_latest_theme_json_version() { + $this->assertSame( WP_Theme_JSON::LATEST_SCHEMA, WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED ); + } + + protected function check_font_family_data( $data, $post_id, $links ) { + static::$post_ids_to_cleanup[] = $post_id; + $post = get_post( $post_id ); + + $this->assertArrayHasKey( 'id', $data, 'The id property should exist in response data.' ); + $this->assertSame( $post->ID, $data['id'], 'The "id" from the response data should match the post ID.' ); + + $this->assertArrayHasKey( 'theme_json_version', $data, 'The theme_json_version property should exist in response data.' ); + $this->assertSame( WP_REST_Font_Families_Controller::LATEST_THEME_JSON_VERSION_SUPPORTED, $data['theme_json_version'], 'The "theme_json_version" from the response data should match the latest version supported by the controller.' ); + + $font_face_ids = get_children( + array( + 'fields' => 'ids', + 'post_parent' => $post_id, + 'post_type' => 'wp_font_face', + 'order' => 'ASC', + 'orderby' => 'ID', + ) + ); + $this->assertArrayHasKey( 'font_faces', $data, 'The font_faces property should exist in the response data.' ); + + foreach ( $font_face_ids as $font_face_id ) { + $this->assertContains( $font_face_id, $data['font_faces'], 'The ID is in the font_faces data.' ); + } + + $this->assertArrayHasKey( 'font_family_settings', $data, 'The font_family_settings property should exist in the response data.' ); + $settings = $data['font_family_settings']; + $expected_settings = array( + 'name' => $post->post_title, + 'slug' => $post->post_name, + 'fontFamily' => $settings['fontFamily'], + 'preview' => $settings['preview'], + ); + $this->assertSame( $expected_settings, $settings, 'The font_family_settings should match.' ); + + $this->assertNotEmpty( $links, 'The links should not be empty in the response data.' ); + $expected = rest_url( 'wp/v2/font-families/' . $post->ID ); + $this->assertSame( $expected, $links['self'][0]['href'], 'The links URL from the response data should match the post\'s REST endpoint.' ); + $expected = rest_url( 'wp/v2/font-families' ); + $this->assertSame( $expected, $links['collection'][0]['href'], 'The links collection URL from the response data should match the REST endpoint.' ); + + if ( ! $font_face_ids ) { + return; + } + + // Check font_face links, if present. + $this->assertArrayHasKey( 'font_faces', $links ); + foreach ( $links['font_faces'] as $index => $link ) { + $expected = rest_url( 'wp/v2/font-families/' . $post->ID . '/font-faces/' . $font_face_ids[ $index ] ); + $this->assertSame( $expected, $link['href'], 'The links for a font faces URL from the response data should match the REST endpoint.' ); + + $embeddable = $link['attributes']['embeddable'] ?? $link['embeddable']; + $this->assertTrue( $embeddable, 'The embeddable should be true.' ); + } + } +} diff --git a/tests/phpunit/tests/formatting/antispambot.php b/tests/phpunit/tests/formatting/antispambot.php new file mode 100644 index 0000000000000..159d907ada9b0 --- /dev/null +++ b/tests/phpunit/tests/formatting/antispambot.php @@ -0,0 +1,73 @@ +<?php +/** + * Tests for the antispambot() function. + * + * @group formatting + * @covers ::antispambot + */ +class Tests_Formatting_Antispambot extends WP_UnitTestCase { + /** + * Ensures that antispambot will not produce invalid UTF-8 when hiding email addresses. + * + * Were a non-US-ASCII email address be sent into `antispambot()`, then a naive approach + * to obfuscation could break apart multibyte characters and leave invalid UTF-8 as a + * result. + * + * @ticket 31992 + * + * @dataProvider data_returns_valid_utf8 + * + * @param string $email The email address to obfuscate. + */ + public function test_returns_valid_utf8( $email ) { + $this->assertTrue( wp_is_valid_utf8( antispambot( $email ) ) ); + } + + /** + * Data provider. + * + * return array[] + */ + public function data_returns_valid_utf8() { + return array( + 'plain' => array( 'bob@example.com' ), + 'plain with ip' => array( 'ace@204.32.222.14' ), + 'deep subdomain' => array( 'kevin@many.subdomains.make.a.happy.man.edu' ), + 'short address' => array( 'a@b.co' ), + 'weird but legal dots' => array( '..@example.com' ), + ); + } + + /** + * This tests that antispambot performs some sort of obfuscation + * and that the obfuscation maps back to the original value. + * + * @ticket 31992 + * + * @dataProvider data_antispambot_obfuscates + * + * @param string $provided The email address to obfuscate. + */ + public function test_antispambot_obfuscates( $provided ) { + // The only token should be the email address, so advance once and treat as a text node. + $obfuscated = antispambot( $provided ); + $p = new WP_HTML_Tag_Processor( $obfuscated ); + $p->next_token(); + $decoded = rawurldecode( $p->get_modifiable_text() ); + + $this->assertNotSame( $provided, $obfuscated, 'Should have produced an obfuscated representation.' ); + $this->assertSame( $provided, $decoded, 'Should have decoded to the original email after restoring.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_antispambot_obfuscates() { + return array( + array( 'example@example.com' ), + array( '#@example.com' ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/balanceTags.php b/tests/phpunit/tests/formatting/balanceTags.php index 4726cf24db234..47e505c362924 100644 --- a/tests/phpunit/tests/formatting/balanceTags.php +++ b/tests/phpunit/tests/formatting/balanceTags.php @@ -2,42 +2,22 @@ /** * @group formatting + * + * @covers ::balanceTags */ class Tests_Formatting_BalanceTags extends WP_UnitTestCase { - public function nestable_tags() { - return array( - array( 'blockquote' ), - array( 'div' ), - array( 'object' ), - array( 'q' ), - array( 'span' ), - ); - } + /** + * @ticket 47014 + * @dataProvider data_supported_traditional_tag_names + */ + public function test_detects_traditional_tag_names( $tag ) { + $normalized = strtolower( $tag ); - // This is a complete(?) listing of valid single/self-closing tags. - public function single_tags() { - return array( - array( 'area' ), - array( 'base' ), - array( 'basefont' ), - array( 'br' ), - array( 'col' ), - array( 'command' ), - array( 'embed' ), - array( 'frame' ), - array( 'hr' ), - array( 'img' ), - array( 'input' ), - array( 'isindex' ), - array( 'link' ), - array( 'meta' ), - array( 'param' ), - array( 'source' ), - ); + $this->assertSame( "<$normalized>inside</$normalized>", balanceTags( "<$tag>inside", true ) ); } - public function supported_traditional_tag_names() { + public function data_supported_traditional_tag_names() { return array( array( 'a' ), array( 'div' ), @@ -49,7 +29,15 @@ public function supported_traditional_tag_names() { ); } - public function supported_custom_element_tag_names() { + /** + * @ticket 47014 + * @dataProvider data_supported_custom_element_tag_names + */ + public function test_detects_supported_custom_element_tag_names( $tag ) { + $this->assertSame( "<$tag>inside</$tag>", balanceTags( "<$tag>inside", true ) ); + } + + public function data_supported_custom_element_tag_names() { return array( array( 'custom-element' ), array( 'my-custom-element' ), @@ -61,19 +49,35 @@ public function supported_custom_element_tag_names() { ); } - public function invalid_tag_names() { + /** + * @ticket 47014 + * @dataProvider data_invalid_tag_names + */ + public function test_ignores_invalid_tag_names( $input, $output ) { + $this->assertSame( $output, balanceTags( $input, true ) ); + } + + public function data_invalid_tag_names() { return array( array( '<0-day>inside', '<0-day>inside' ), // Can't start with a number - handled by the "<3" fix. array( '<UPPERCASE-TAG>inside', '<UPPERCASE-TAG>inside' ), // Custom elements cannot be uppercase. ); } + /** + * @ticket 47014 + * @dataProvider data_unsupported_valid_tag_names + */ + public function test_ignores_unsupported_custom_tag_names( $tag ) { + $this->assertSame( "<$tag>inside", balanceTags( "<$tag>inside", true ) ); + } + /** * These are valid custom elements but we don't support them yet. * - * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name + * @see https://html.spec.whatwg.org/multipage/custom-elements.html#valid-custom-element-name */ - public function unsupported_valid_tag_names() { + public function data_unsupported_valid_tag_names() { return array( // We don't allow ending in a dash. array( '<what->inside' ), @@ -128,12 +132,20 @@ public function unsupported_valid_tag_names() { ); } + /** + * @ticket 47014 + * @dataProvider data_supported_invalid_tag_names + */ + public function test_detects_supported_invalid_tag_names( $tag ) { + $this->assertSame( "<$tag>inside</$tag>", balanceTags( "<$tag>inside", true ) ); + } + /** * These are invalid custom elements but we support them right now in order to keep the parser simpler. * * @see https://w3c.github.io/webcomponents/spec/custom/#valid-custom-element-name */ - public function supported_invalid_tag_names() { + public function data_supported_invalid_tag_names() { return array( // Reserved names for custom elements. array( 'annotation-xml' ), @@ -147,53 +159,11 @@ public function supported_invalid_tag_names() { ); } - /** - * @ticket 47014 - * @dataProvider supported_traditional_tag_names - */ - public function test_detects_traditional_tag_names( $tag ) { - $normalized = strtolower( $tag ); - - $this->assertSame( "<$normalized>inside</$normalized>", balanceTags( "<$tag>inside", true ) ); - } - - /** - * @ticket 47014 - * @dataProvider supported_custom_element_tag_names - */ - public function test_detects_supported_custom_element_tag_names( $tag ) { - $this->assertSame( "<$tag>inside</$tag>", balanceTags( "<$tag>inside", true ) ); - } - - /** - * @ticket 47014 - * @dataProvider invalid_tag_names - */ - public function test_ignores_invalid_tag_names( $input, $output ) { - $this->assertSame( $output, balanceTags( $input, true ) ); - } - - /** - * @ticket 47014 - * @dataProvider unsupported_valid_tag_names - */ - public function test_ignores_unsupported_custom_tag_names( $tag ) { - $this->assertSame( "<$tag>inside", balanceTags( "<$tag>inside", true ) ); - } - - /** - * @ticket 47014 - * @dataProvider supported_invalid_tag_names - */ - public function test_detects_supported_invalid_tag_names( $tag ) { - $this->assertSame( "<$tag>inside</$tag>", balanceTags( "<$tag>inside", true ) ); - } - /** * If a recognized valid single tag appears unclosed, it should get self-closed * * @ticket 1597 - * @dataProvider single_tags + * @dataProvider data_single_tags */ public function test_selfcloses_unclosed_known_single_tags( $tag ) { $this->assertSame( "<$tag />", balanceTags( "<$tag>", true ) ); @@ -204,36 +174,60 @@ public function test_selfcloses_unclosed_known_single_tags( $tag ) { * should get removed and tag should be self-closed. * * @ticket 1597 - * @dataProvider single_tags + * @dataProvider data_single_tags */ public function test_selfcloses_known_single_tags_having_closing_tag( $tag ) { $this->assertSame( "<$tag />", balanceTags( "<$tag></$tag>", true ) ); } + // This is a complete(?) listing of valid single/self-closing tags. + public function data_single_tags() { + return array( + array( 'area' ), + array( 'base' ), + array( 'basefont' ), + array( 'br' ), + array( 'col' ), + array( 'command' ), + array( 'embed' ), + array( 'frame' ), + array( 'hr' ), + array( 'img' ), + array( 'input' ), + array( 'isindex' ), + array( 'link' ), + array( 'meta' ), + array( 'param' ), + array( 'source' ), + array( 'track' ), + array( 'wbr' ), + ); + } + /** + * Tests closing unknown single tags. + * * @ticket 1597 + * @dataProvider data_unknown_single_tags_with_closing_tag */ - public function test_closes_unknown_single_tags_with_closing_tag() { + public function test_closes_unknown_single_tags_with_closing_tag( $input, $expected ) { + $this->assertSame( $expected, balanceTags( $input, true ) ); + } - $inputs = array( - '<strong/>', - '<em />', - '<p class="main1"/>', - '<p class="main2" />', - '<STRONG/>', - ); - $expected = array( - '<strong></strong>', - '<em></em>', - '<p class="main1"></p>', - '<p class="main2"></p>', + /** + * Data provider for test_closes_unknown_single_tags_with_closing_tag. + * + * @return array<string, array<string, string>> + */ + public function data_unknown_single_tags_with_closing_tag() { + return array( + 'default' => array( '<strong/>', '<strong></strong>' ), + 'with-space' => array( '<em />', '<em></em>' ), + 'with-class' => array( '<p class="main1"/>', '<p class="main1"></p>' ), + 'with-class-and-space' => array( '<p class="main2" />', '<p class="main2"></p>' ), // Valid tags are transformed to lowercase. - '<strong></strong>', + 'uppercase' => array( '<STRONG/>', '<strong></strong>' ), ); - - foreach ( $inputs as $key => $input ) { - $this->assertSame( $expected[ $key ], balanceTags( $inputs[ $key ], true ) ); - } } public function test_closes_unclosed_single_tags_having_attributes() { @@ -265,7 +259,7 @@ public function test_allows_validly_closed_single_tags() { } /** - * @dataProvider nestable_tags + * @dataProvider data_nestable_tags */ public function test_balances_nestable_tags( $tag ) { $inputs = array( @@ -284,6 +278,21 @@ public function test_balances_nestable_tags( $tag ) { } } + public function data_nestable_tags() { + return array( + array( 'article' ), + array( 'aside' ), + array( 'blockquote' ), + array( 'details' ), + array( 'div' ), + array( 'figure' ), + array( 'object' ), + array( 'q' ), + array( 'section' ), + array( 'span' ), + ); + } + public function test_allows_adjacent_nestable_tags() { $inputs = array( '<blockquote><blockquote>Example quote</blockquote></blockquote>', diff --git a/tests/phpunit/tests/formatting/capitalPDangit.php b/tests/phpunit/tests/formatting/capitalPDangit.php index 3e78e1a3649d0..850dcdaec9a19 100644 --- a/tests/phpunit/tests/formatting/capitalPDangit.php +++ b/tests/phpunit/tests/formatting/capitalPDangit.php @@ -1,8 +1,10 @@ <?php -// phpcs:disable WordPress.WP.CapitalPDangit.Misspelled -- 🙃 +// phpcs:disable WordPress.WP.CapitalPDangit.MisspelledInText -- 🙃 /** * @group formatting + * + * @covers ::capital_P_dangit */ class Tests_Formatting_CapitalPDangit extends WP_UnitTestCase { public function test_esc_attr_quotes() { diff --git a/tests/phpunit/tests/formatting/cleanPre.php b/tests/phpunit/tests/formatting/cleanPre.php index 8444ae7bd5e71..724b89bb76256 100644 --- a/tests/phpunit/tests/formatting/cleanPre.php +++ b/tests/phpunit/tests/formatting/cleanPre.php @@ -1,11 +1,13 @@ <?php /** - * The clean_pre() removes pararaph and line break + * The clean_pre() removes paragraph and line break * tags within `<pre>` elements as part of wpautop(). * * @group formatting * @expectedDeprecated clean_pre + * + * @covers ::clean_pre */ class Tests_Formatting_CleanPre extends WP_UnitTestCase { @@ -22,10 +24,13 @@ public function test_removes_self_closing_br_without_space() { $this->assertSame( $res, clean_pre( $source ) ); } - // I don't think this can ever happen in production; - // <br> is changed to <br /> elsewhere. Left in because - // that replacement shouldn't happen (what if you want - // HTML 4 output?). + + /** + * I don't think this can ever happen in production; + * <br> is changed to <br /> elsewhere. Left in because + * that replacement shouldn't happen (what if you want + * HTML 4 output?). + */ public function test_removes_html_br() { $source = 'a b c\n<br>sldfj<br>'; $res = 'a b c\nsldfj'; diff --git a/tests/phpunit/tests/formatting/convertInvalidEntities.php b/tests/phpunit/tests/formatting/convertInvalidEntities.php new file mode 100644 index 0000000000000..61011651914c2 --- /dev/null +++ b/tests/phpunit/tests/formatting/convertInvalidEntities.php @@ -0,0 +1,30 @@ +<?php + +/** + * @group formatting + * + * @covers ::convert_invalid_entities + */ +class Tests_Formatting_ConvertInvalidEntities extends WP_UnitTestCase { + public function test_replaces_windows1252_entities_with_unicode_ones() { + $input = '‚ƒ„…†‡ˆ‰Š‹Œ‘’“”•–—˜™š›œŸ'; + $output = '‚ƒ„…†‡ˆ‰Š‹Œ‘’“”•–—˜™š›œŸ'; + $this->assertSame( $output, convert_invalid_entities( $input ) ); + } + + /** + * @ticket 20503 + */ + public function test_replaces_latin_letter_z_with_caron() { + $input = 'Žž'; + $output = 'Žž'; + $this->assertSame( $output, convert_invalid_entities( $input ) ); + } + + /** + * @covers ::convert_chars + */ + public function test_escapes_lone_ampersands() { + $this->assertSame( 'at&t', convert_chars( 'at&t' ) ); + } +} diff --git a/tests/phpunit/tests/formatting/convertInvalidEntries.php b/tests/phpunit/tests/formatting/convertInvalidEntries.php deleted file mode 100644 index 9b243ee4213ac..0000000000000 --- a/tests/phpunit/tests/formatting/convertInvalidEntries.php +++ /dev/null @@ -1,25 +0,0 @@ -<?php - -/** - * @group formatting - */ -class Tests_Formatting_ConvertInvalidEntities extends WP_UnitTestCase { - public function test_replaces_windows1252_entities_with_unicode_ones() { - $input = '‚ƒ„…†‡ˆ‰Š‹Œ‘’“”•–—˜™š›œŸ'; - $output = '‚ƒ„…†‡ˆ‰Š‹Œ‘’“”•–—˜™š›œŸ'; - $this->assertSame( $output, convert_invalid_entities( $input ) ); - } - - /** - * @ticket 20503 - */ - public function test_replaces_latin_letter_z_with_caron() { - $input = 'Žž'; - $output = 'Žž'; - $this->assertSame( $output, convert_invalid_entities( $input ) ); - } - - public function test_escapes_lone_ampersands() { - $this->assertSame( 'at&t', convert_chars( 'at&t' ) ); - } -} diff --git a/tests/phpunit/tests/formatting/convertSmilies.php b/tests/phpunit/tests/formatting/convertSmilies.php index 760625e40a066..24c2a8087a93f 100644 --- a/tests/phpunit/tests/formatting/convertSmilies.php +++ b/tests/phpunit/tests/formatting/convertSmilies.php @@ -3,21 +3,49 @@ /** * @group formatting * @group emoji + * + * @covers ::convert_smilies */ class Tests_Formatting_ConvertSmilies extends WP_UnitTestCase { + public function set_up() { + parent::set_up(); + + smilies_init(); + } + /** - * Basic Test Content DataProvider + * Basic validation test to confirm that smilies are converted to image + * when use_smilies = 1 and not when use_smilies = 0. * - * array ( input_txt, converted_output_txt) + * @dataProvider data_convert_standard_smilies */ - public function get_smilies_input_output() { + public function test_convert_standard_smilies( $input, $converted ) { + $this->assertSame( $converted, convert_smilies( $input ) ); + + // Disable smilies. + update_option( 'use_smilies', 0 ); + + $this->assertSame( $input, convert_smilies( $input ) ); + } + + /** + * Data provider. + * + * @return array { + * @type array { + * @type string $input Input content. + * @type string $converted Converted output. + * } + * } + */ + public function data_convert_standard_smilies() { $includes_path = includes_url( 'images/smilies/' ); return array( array( 'Lorem ipsum dolor sit amet mauris ;-) Praesent gravida sodales. :lol: Vivamus nec diam in faucibus eu, bibendum varius nec, imperdiet purus est, at augue at lacus malesuada elit dapibus a, :eek: mauris. Cras mauris viverra elit. Nam laoreet viverra. Pellentesque tortor. Nam libero ante, porta urna ut turpis. Nullam wisi magna, :mrgreen: tincidunt nec, sagittis non, fringilla enim. Nam consectetuer nec, ullamcorper pede eu dui odio consequat vel, vehicula tortor quis pede turpis cursus quis, egestas ipsum ultricies ut, eleifend velit. Mauris vestibulum iaculis. Sed in nunc. Vivamus elit porttitor egestas. Mauris purus :?:', - "Lorem ipsum dolor sit amet mauris \xf0\x9f\x98\x89 Praesent gravida sodales. \xf0\x9f\x98\x86 Vivamus nec diam in faucibus eu, bibendum varius nec, imperdiet purus est, at augue at lacus malesuada elit dapibus a, \xf0\x9f\x98\xae mauris. Cras mauris viverra elit. Nam laoreet viverra. Pellentesque tortor. Nam libero ante, porta urna ut turpis. Nullam wisi magna, <img src=\"${includes_path}mrgreen.png\" alt=\":mrgreen:\" class=\"wp-smiley\" style=\"height: 1em; max-height: 1em;\" /> tincidunt nec, sagittis non, fringilla enim. Nam consectetuer nec, ullamcorper pede eu dui odio consequat vel, vehicula tortor quis pede turpis cursus quis, egestas ipsum ultricies ut, eleifend velit. Mauris vestibulum iaculis. Sed in nunc. Vivamus elit porttitor egestas. Mauris purus \xe2\x9d\x93", + "Lorem ipsum dolor sit amet mauris \xf0\x9f\x98\x89 Praesent gravida sodales. \xf0\x9f\x98\x86 Vivamus nec diam in faucibus eu, bibendum varius nec, imperdiet purus est, at augue at lacus malesuada elit dapibus a, \xf0\x9f\x98\xae mauris. Cras mauris viverra elit. Nam laoreet viverra. Pellentesque tortor. Nam libero ante, porta urna ut turpis. Nullam wisi magna, <img src=\"{$includes_path}mrgreen.png\" alt=\":mrgreen:\" class=\"wp-smiley\" style=\"height: 1em; max-height: 1em;\" /> tincidunt nec, sagittis non, fringilla enim. Nam consectetuer nec, ullamcorper pede eu dui odio consequat vel, vehicula tortor quis pede turpis cursus quis, egestas ipsum ultricies ut, eleifend velit. Mauris vestibulum iaculis. Sed in nunc. Vivamus elit porttitor egestas. Mauris purus \xe2\x9d\x93", ), array( '<strong>Welcome to the jungle!</strong> We got fun n games! :) We got everything you want 8-) <em>Honey we know the names :)</em>', @@ -28,8 +56,8 @@ public function get_smilies_input_output() { "<strong;)>a little bit of this\na little bit:other: of that \xf0\x9f\x98\x80\n\xf0\x9f\x98\x80 a little bit of good\nyeah with a little bit of bad8O", ), array( - '<strong style="here comes the sun :-D">and I say it\'s allright:D:D', - '<strong style="here comes the sun :-D">and I say it\'s allright:D:D', + '<strong style="here comes the sun :-D">and I say it\'s alright:D:D', + '<strong style="here comes the sun :-D">and I say it\'s alright:D:D', ), array( '<!-- Woo-hoo, I\'m a comment, baby! :x > -->', @@ -43,31 +71,45 @@ public function get_smilies_input_output() { } /** - * @dataProvider get_smilies_input_output + * Tests that custom smilies are converted to images when use_smilies = 1. * - * Basic Validation Test to confirm that smilies are converted to image - * when use_smilies = 1 and not when use_smilies = 0 + * @dataProvider data_convert_custom_smilies */ - public function test_convert_standard_smilies( $in_txt, $converted_txt ) { - // Standard smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); + public function test_convert_custom_smilies( $input, $converted ) { + global $wpsmiliestrans; + + $trans_orig = $wpsmiliestrans; // Save original translations array. + + $wpsmiliestrans = array( + ':PP' => 'icon_tongue.gif', + ':arrow:' => 'icon_arrow.gif', + ':monkey:' => 'icon_shock_the_monkey.gif', + ':nervou:' => 'icon_nervou.gif', + ); smilies_init(); - $this->assertSame( $converted_txt, convert_smilies( $in_txt ) ); + $this->assertSame( $converted, convert_smilies( $input ) ); - // Standard smilies, use_smilies: OFF. + // Disable smilies. update_option( 'use_smilies', 0 ); - $this->assertSame( $in_txt, convert_smilies( $in_txt ) ); + $wpsmiliestrans = $trans_orig; // Reset original translations array. + + $this->assertSame( $input, convert_smilies( $input ) ); } /** - * Custom Smilies Test Content DataProvider + * Data provider. * - * array ( input_txt, converted_output_txt) + * @return array { + * @type array { + * @type string $input Input content. + * @type string $converted Converted output. + * } + * } */ - public function get_custom_smilies_input_output() { + public function data_convert_custom_smilies() { $includes_path = includes_url( 'images/smilies/' ); return array( @@ -87,46 +129,31 @@ public function get_custom_smilies_input_output() { } /** - * @dataProvider get_custom_smilies_input_output + * Tests that conversion of smilies is ignored in pre-determined tags: + * pre, code, script, style. * - * Validate Custom Smilies are converted to images when use_smilies = 1 + * @ticket 16448 + * @dataProvider data_ignore_smilies_in_tags */ - public function test_convert_custom_smilies( $in_txt, $converted_txt ) { - global $wpsmiliestrans; - - // Custom smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); - - if ( ! isset( $wpsmiliestrans ) ) { - smilies_init(); - } - - $trans_orig = $wpsmiliestrans; // Save original translations array. - - $wpsmiliestrans = array( - ':PP' => 'icon_tongue.gif', - ':arrow:' => 'icon_arrow.gif', - ':monkey:' => 'icon_shock_the_monkey.gif', - ':nervou:' => 'icon_nervou.gif', - ); - - smilies_init(); - - $this->assertSame( $converted_txt, convert_smilies( $in_txt ) ); - - // Standard smilies, use_smilies: OFF. - update_option( 'use_smilies', 0 ); + public function test_ignore_smilies_in_tags( $element ) { + $includes_path = includes_url( 'images/smilies/' ); - $this->assertSame( $in_txt, convert_smilies( $in_txt ) ); + $input = 'Do we ignore smilies ;-) in ' . $element . ' tags <' . $element . ' class="foo">My Content Here :?: </' . $element . '>'; + $expected = "Do we ignore smilies \xf0\x9f\x98\x89 in $element tags <$element class=\"foo\">My Content Here :?: </$element>"; - $wpsmiliestrans = $trans_orig; // Reset original translations array. + $this->assertSame( $expected, convert_smilies( $input ) ); } - /** - * DataProvider of HTML elements/tags that smilie matches should be ignored in + * Data provider. + * + * @return array { + * @type array { + * @type string $element HTML tag name. + * } + * } */ - public function get_smilies_ignore_tags() { + public function data_ignore_smilies_in_tags() { return array( array( 'pre' ), array( 'code' ), @@ -137,32 +164,32 @@ public function get_smilies_ignore_tags() { } /** - * Validate Conversion of Smilies is ignored in pre-determined tags - * pre, code, script, style + * Tests that combinations of smilies separated by a single space + * are converted correctly. * - * @ticket 16448 - * @dataProvider get_smilies_ignore_tags + * @ticket 20124 + * @dataProvider data_smilies_combinations */ - public function test_ignore_smilies_in_tags( $element ) { - $includes_path = includes_url( 'images/smilies/' ); - - $in_str = 'Do we ingore smilies ;-) in ' . $element . ' tags <' . $element . ' class="foo">My Content Here :?: </' . $element . '>'; - $exp_str = "Do we ingore smilies \xf0\x9f\x98\x89 in $element tags <$element class=\"foo\">My Content Here :?: </$element>"; - - // Standard smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); - smilies_init(); + public function test_smilies_combinations( $input, $converted ) { + $this->assertSame( $converted, convert_smilies( $input ) ); - $this->assertSame( $exp_str, convert_smilies( $in_str ) ); - - // Standard smilies, use_smilies: OFF. + // Disable smilies. update_option( 'use_smilies', 0 ); + + $this->assertSame( $input, convert_smilies( $input ) ); } /** - * DataProvider of Smilie Combinations + * Data provider. + * + * @return array { + * @type array { + * @type string $input Input content. + * @type string $converted Converted output. + * } + * } */ - public function get_smilies_combinations() { + public function data_smilies_combinations() { $includes_path = includes_url( 'images/smilies/' ); return array( @@ -194,29 +221,44 @@ public function get_smilies_combinations() { } /** - * Validate Combinations of Smilies separated by single space - * are converted correctly + * Tests that smilies are converted for single smilie in + * the $wpsmiliestrans global array. * - * @ticket 20124 - * @dataProvider get_smilies_combinations + * @ticket 25303 + * @dataProvider data_single_smilies_in_wpsmiliestrans */ - public function test_smilies_combinations( $in_txt, $converted_txt ) { - // Custom smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); + public function test_single_smilies_in_wpsmiliestrans( $input, $converted ) { + global $wpsmiliestrans; + + $orig_trans = $wpsmiliestrans; // Save original translations array. + + $wpsmiliestrans = array( + ':)' => 'simple-smile.png', + ); + smilies_init(); - $this->assertSame( $converted_txt, convert_smilies( $in_txt ) ); + $this->assertSame( $converted, convert_smilies( $input ) ); - // Custom smilies, use_smilies: OFF. + // Disable smilies. update_option( 'use_smilies', 0 ); - $this->assertSame( $in_txt, convert_smilies( $in_txt ) ); + $wpsmiliestrans = $orig_trans; // Reset original translations array. + + $this->assertSame( $input, convert_smilies( $input ) ); } /** - * DataProvider of Single Smilies input and converted output + * Data provider. + * + * @return array { + * @type array { + * @type string $input Input content. + * @type string $converted Converted output. + * } + * } */ - public function get_single_smilies_input_output() { + public function data_single_smilies_in_wpsmiliestrans() { $includes_path = includes_url( 'images/smilies/' ); return array( @@ -236,41 +278,30 @@ public function get_single_smilies_input_output() { } /** - * Validate Smilies are converted for single smilie in - * the $wpsmiliestrans global array + * Tests that $wp_smiliessearch pattern will match smilies + * between spaces, but never capture those spaces. * - * @ticket 25303 - * @dataProvider get_single_smilies_input_output + * Further tests that spaces aren't randomly deleted + * or added when replacing the text with an image. + * + * @ticket 22692 + * @dataProvider data_spaces_around_smilies */ - public function test_single_smilies_in_wpsmiliestrans( $in_txt, $converted_txt ) { - global $wpsmiliestrans; - - // Standard smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); - - if ( ! isset( $wpsmiliestrans ) ) { - smilies_init(); - } - - $orig_trans = $wpsmiliestrans; // Save original tranlations array. - - $wpsmiliestrans = array( - ':)' => 'simple-smile.png', - ); - - smilies_init(); - - $this->assertSame( $converted_txt, convert_smilies( $in_txt ) ); - - // Standard smilies, use_smilies: OFF. - update_option( 'use_smilies', 0 ); - - $this->assertSame( $in_txt, convert_smilies( $in_txt ) ); - - $wpsmiliestrans = $orig_trans; // Reset original translations array. + public function test_spaces_around_smilies( $input, $converted ) { + $this->assertSame( $converted, convert_smilies( $input ) ); } - public function get_spaces_around_smilies() { + /** + * Data provider. + * + * @return array { + * @type array { + * @type string $input Input content. + * @type string $converted Converted output. + * } + * } + */ + public function data_spaces_around_smilies() { $nbsp = "\xC2\xA0"; return array( @@ -289,28 +320,6 @@ public function get_spaces_around_smilies() { ); } - /** - * Check that $wp_smiliessearch pattern will match smilies - * between spaces, but never capture those spaces. - * - * Further check that spaces aren't randomly deleted - * or added when replacing the text with an image. - * - * @ticket 22692 - * @dataProvider get_spaces_around_smilies - */ - public function test_spaces_around_smilies( $in_txt, $converted_txt ) { - // Standard smilies, use_smilies: ON. - update_option( 'use_smilies', 1 ); - - smilies_init(); - - $this->assertSame( $converted_txt, convert_smilies( $in_txt ) ); - - // Standard smilies, use_smilies: OFF. - update_option( 'use_smilies', 0 ); - } - /** * Test to ensure smilies can be removed with a filter * @@ -352,4 +361,16 @@ public function _filter_add_smilies( $wpsmiliestrans ) { $wpsmiliestrans['<3'] = '\xe2\x9d\xa4'; return $wpsmiliestrans; } + + + /** + * Tests that the function does not throw a fatal error from count() + * when preg_split() fails on large input. + * + * @ticket 51019 + */ + public function test_smilies_with_large_text_input() { + $text = '<p><img alt="" src="data:image/png;base64,' . str_repeat( 'iVBORw0KGgoAAAAN', 65536 ) . '="></p> :)'; + $this->assertStringContainsString( "\xf0\x9f\x99\x82", convert_smilies( $text ) ); + } } diff --git a/tests/phpunit/tests/formatting/date.php b/tests/phpunit/tests/formatting/date.php index 9f0f975d4c09e..60635c5bdc939 100644 --- a/tests/phpunit/tests/formatting/date.php +++ b/tests/phpunit/tests/formatting/date.php @@ -6,10 +6,23 @@ */ class Tests_Formatting_Date extends WP_UnitTestCase { + /** + * Cleans up. + */ + public function tear_down() { + // Reset changed options to their default value. + update_option( 'gmt_offset', 0 ); + update_option( 'timezone_string', '' ); + + parent::tear_down(); + } + /** * Unpatched, this test passes only when Europe/London is not observing DST. * * @ticket 20328 + * + * @covers ::get_date_from_gmt */ public function test_get_date_from_gmt_outside_of_dst() { update_option( 'timezone_string', 'Europe/London' ); @@ -22,6 +35,8 @@ public function test_get_date_from_gmt_outside_of_dst() { * Unpatched, this test passes only when Europe/London is observing DST. * * @ticket 20328 + * + * @covers ::get_date_from_gmt */ public function test_get_date_from_gmt_during_dst() { update_option( 'timezone_string', 'Europe/London' ); @@ -32,6 +47,8 @@ public function test_get_date_from_gmt_during_dst() { /** * @ticket 20328 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_outside_of_dst() { update_option( 'timezone_string', 'Europe/London' ); @@ -42,6 +59,8 @@ public function test_get_gmt_from_date_outside_of_dst() { /** * @ticket 20328 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_during_dst() { update_option( 'timezone_string', 'Europe/London' ); @@ -52,6 +71,9 @@ public function test_get_gmt_from_date_during_dst() { /** * @ticket 34279 + * + * @covers ::get_date_from_gmt + * */ public function test_get_date_and_time_from_gmt_no_timezone() { $local = '2012-01-01 12:34:56'; @@ -61,6 +83,8 @@ public function test_get_date_and_time_from_gmt_no_timezone() { /** * @ticket 34279 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_no_timezone() { $gmt = '2012-12-01 00:00:00'; @@ -70,6 +94,8 @@ public function test_get_gmt_from_date_no_timezone() { /** * @ticket 34279 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_short_date() { update_option( 'timezone_string', 'Europe/London' ); @@ -80,6 +106,8 @@ public function test_get_gmt_from_date_short_date() { /** * @ticket 34279 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_string_date() { update_option( 'timezone_string', 'Europe/London' ); @@ -90,6 +118,8 @@ public function test_get_gmt_from_date_string_date() { /** * @ticket 34279 + * + * @covers ::get_gmt_from_date */ public function test_get_gmt_from_date_string_date_no_timezone() { $local = 'now'; @@ -100,9 +130,11 @@ public function test_get_gmt_from_date_string_date_no_timezone() { /** * @ticket 31809 * - * @dataProvider timezone_provider + * @dataProvider data_timezone_provider + * + * @covers ::get_gmt_from_date */ - public function test_gmt_from_date_correct_time( $timezone_string, $gmt_offset ) { + public function test_get_gmt_from_date_correct_time( $timezone_string, $gmt_offset ) { update_option( 'timezone_string', $timezone_string ); update_option( 'gmt_offset', $gmt_offset ); @@ -116,9 +148,11 @@ public function test_gmt_from_date_correct_time( $timezone_string, $gmt_offset ) /** * @ticket 31809 * - * @dataProvider timezone_provider + * @dataProvider data_timezone_provider + * + * @covers ::get_date_from_gmt */ - public function test_date_from_gmt_correct_time( $timezone_string, $gmt_offset ) { + public function test_get_date_from_gmt_correct_time( $timezone_string, $gmt_offset ) { update_option( 'timezone_string', $timezone_string ); update_option( 'gmt_offset', $gmt_offset ); @@ -131,8 +165,11 @@ public function test_date_from_gmt_correct_time( $timezone_string, $gmt_offset ) /** * @ticket 31809 + * @ticket 56468 * - * @dataProvider timezone_provider + * @dataProvider data_timezone_provider + * + * @covers ::iso8601_to_datetime */ public function test_is8601_to_datetime_correct_time( $timezone_string, $gmt_offset ) { update_option( 'timezone_string', $timezone_string ); @@ -194,16 +231,20 @@ public function test_is8601_to_datetime_correct_time( $timezone_string, $gmt_off * * @return array */ - public function timezone_provider() { + public function data_timezone_provider() { return array( - array( - 'timezone_string' => 'Europe/Kiev', + 'valid timezone string and GMT offset' => array( + 'timezone_string' => 'Europe/Helsinki', 'gmt_offset' => 3, ), - array( + 'empty timezone string, valid GMT offset' => array( 'timezone_string' => '', 'gmt_offset' => 3, ), + 'deprecated timezone string, no GMT offset' => array( + 'timezone_string' => 'America/Buenos_Aires', + 'gmt_offset' => 0, + ), ); } } diff --git a/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php b/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php new file mode 100644 index 0000000000000..319890c457701 --- /dev/null +++ b/tests/phpunit/tests/formatting/deprecatedUtfEncodeDecode.php @@ -0,0 +1,114 @@ +<?php + +/** + * @group formatting + */ +class Tests_DeprecatedUtf8EncodeDecodeTest extends WP_UnitTestCase { + /** + * Ensures that the fallback for {@see \utf8_encode()} maps the ISO-8859-1 characters properly. + * + * @ticket 63863. + */ + public function test_utf8_encode_characters() { + for ( $i = 0; $i <= 0xFF; $i++ ) { + $c = chr( $i ); + $hex_i = strtoupper( str_pad( dechex( $i ), 2, '0', STR_PAD_LEFT ) ); + + $this->assertSame( + bin2hex( mb_convert_encoding( $c, 'UTF-8', 'ISO-8859-1' ) ), + bin2hex( _wp_utf8_encode_fallback( $c ) ), + "Failed to convert U+{$hex_i} properly." + ); + } + } + + /** + * Ensures that the fallback for {@see \utf8_encode()} properly + * matches the legacy behavior for a given set of test cases. + * + * @ticket 63863. + * + * @dataProvider data_utf8_strings + */ + public function test_utf8_encode_cases( $input ) { + $this->assertSame( + mb_convert_encoding( $input, 'UTF-8', 'ISO-8859-1' ), + _wp_utf8_encode_fallback( $input ), + 'Failed to properly convert.' + ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_utf8_strings() { + return array( + 'Basic valid string' => array( 'Dan eats cinnamon toast.' ), + 'Valid with Emoji' => array( 'The best Emoji is 🅰.' ), + 'Truncated bytes' => array( substr( 'England has 🏴󠁧󠁢󠁥󠁮󠁧󠁿', 0, -1 ) ), + 'Minimal subpart' => array( "One \xC0, two \xE2\x80, three \xF0\x95\x85." ), + ); + } + + /** + * Ensures that the fallback for {@see \utf8_decode()} maps the UTF-8 characters properly. + * + * @ticket 63863. + */ + public function test_utf8_decode_characters() { + for ( $i = 0; $i <= 0x10FFFF; $i++ ) { + $hex_i = strtoupper( str_pad( dechex( $i ), 2, '0', STR_PAD_LEFT ) ); + + if ( $i < 0xD800 || $i > 0xE000 ) { + $c = mb_chr( $i ); + } else { + /* + * Since the UTF-16 surrogate halves are not valid Unicode characters, + * these have to be manually constructed as invalid UTF-8. + */ + $byte1 = 0xE0 | ( $i >> 12 ); + $byte2 = 0x80 | ( ( $i >> 6 ) & 0x3F ); + $byte3 = 0x80 | ( $i & 0x3F ); + + $c = "{$byte1}{$byte2}{$byte3}"; + } + + $this->assertSame( + bin2hex( mb_convert_encoding( $c, 'ISO-8859-1', 'UTF-8' ) ), + bin2hex( _wp_utf8_decode_fallback( $c ) ), + "Failed to convert U+{$hex_i} properly." + ); + } + } + + /** + * Ensures that the fallback for {@see \utf8_encode()} properly + * matches the legacy behavior for a given set of test cases. + * + * @ticket 63863. + * + * @dataProvider data_iso_8859_1_strings + */ + public function test_utf8_decode_cases( $input ) { + $this->assertSame( + mb_convert_encoding( $input, 'ISO-8859-1', 'UTF-8' ), + _wp_utf8_decode_fallback( $input ), + 'Failed to properly convert.' + ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_iso_8859_1_strings() { + return array( + 'Basic valid string' => array( 'Dan eats cinnamon toast' ), + 'Latin1 supplement' => array( 'Pi\xF1a is another name for Pineapple.' ), + 'Bytes as invalid UTF-8' => array( 'The \x95 is invalid UTF-8.' ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/emoji.php b/tests/phpunit/tests/formatting/emoji.php index 85951a072660b..f16a51cb8b62c 100644 --- a/tests/phpunit/tests/formatting/emoji.php +++ b/tests/phpunit/tests/formatting/emoji.php @@ -6,42 +6,83 @@ */ class Tests_Formatting_Emoji extends WP_UnitTestCase { - private $png_cdn = 'https://s.w.org/images/core/emoji/13.1.0/72x72/'; - private $svn_cdn = 'https://s.w.org/images/core/emoji/13.1.0/svg/'; + private $png_cdn = 'https://s.w.org/images/core/emoji/17.0.2/72x72/'; + private $svg_cdn = 'https://s.w.org/images/core/emoji/17.0.2/svg/'; + + /** + * @ticket 63842 + * + * @covers ::_print_emoji_detection_script + */ + public function test_script_tag_printing() { + // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: + self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); + $output = get_echo( '_print_emoji_detection_script' ); + + $processor = new WP_HTML_Tag_Processor( $output ); + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SCRIPT', $processor->get_tag() ); + $this->assertSame( 'wp-emoji-settings', $processor->get_attribute( 'id' ) ); + $this->assertSame( 'application/json', $processor->get_attribute( 'type' ) ); + $text = $processor->get_modifiable_text(); + $settings = json_decode( $text, true ); + $this->assertIsArray( $settings ); + + $this->assertEqualSets( + array( 'baseUrl', 'ext', 'svgUrl', 'svgExt', 'source' ), + array_keys( $settings ) + ); + $this->assertSame( $this->png_cdn, $settings['baseUrl'] ); + $this->assertSame( '.png', $settings['ext'] ); + $this->assertSame( $this->svg_cdn, $settings['svgUrl'] ); + $this->assertSame( '.svg', $settings['svgExt'] ); + $this->assertIsArray( $settings['source'] ); + $this->assertArrayHasKey( 'wpemoji', $settings['source'] ); + $this->assertArrayHasKey( 'twemoji', $settings['source'] ); + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SCRIPT', $processor->get_tag() ); + $this->assertSame( 'module', $processor->get_attribute( 'type' ) ); + $this->assertNull( $processor->get_attribute( 'src' ) ); + $this->assertFalse( $processor->next_tag() ); + } /** * @ticket 36525 + * + * @covers ::_print_emoji_detection_script */ public function test_unfiltered_emoji_cdns() { // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); } - public function _filtered_emoji_svn_cdn( $cdn = '' ) { + public function _filtered_emoji_svg_cdn( $cdn = '' ) { return 'https://s.wordpress.org/images/core/emoji/svg/'; } /** * @ticket 36525 + * + * @covers ::_print_emoji_detection_script */ public function test_filtered_emoji_svn_cdn() { - $filtered_svn_cdn = $this->_filtered_emoji_svn_cdn(); + $filtered_svn_cdn = $this->_filtered_emoji_svg_cdn(); - add_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) ); + add_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svg_cdn' ) ); // `_print_emoji_detection_script()` assumes `wp-includes/js/wp-emoji-loader.js` is present: self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringNotContainsString( wp_json_encode( $this->svn_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringNotContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $filtered_svn_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); - remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svn_cdn' ) ); + remove_filter( 'emoji_svg_url', array( $this, '_filtered_emoji_svg_cdn' ) ); } public function _filtered_emoji_png_cdn( $cdn = '' ) { @@ -50,6 +91,8 @@ public function _filtered_emoji_png_cdn( $cdn = '' ) { /** * @ticket 36525 + * + * @covers ::_print_emoji_detection_script */ public function test_filtered_emoji_png_cdn() { $filtered_png_cdn = $this->_filtered_emoji_png_cdn(); @@ -60,27 +103,34 @@ public function test_filtered_emoji_png_cdn() { self::touch( ABSPATH . WPINC . '/js/wp-emoji-loader.js' ); $output = get_echo( '_print_emoji_detection_script' ); - $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn ), $output ); - $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn ), $output ); - $this->assertStringContainsString( wp_json_encode( $this->svn_cdn ), $output ); + $this->assertStringContainsString( wp_json_encode( $filtered_png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringNotContainsString( wp_json_encode( $this->png_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); + $this->assertStringContainsString( wp_json_encode( $this->svg_cdn, JSON_HEX_TAG | JSON_UNESCAPED_SLASHES ), $output ); remove_filter( 'emoji_url', array( $this, '_filtered_emoji_png_cdn' ) ); } /** * @ticket 41501 + * + * @covers ::_wp_emoji_list */ public function test_wp_emoji_list_returns_data() { $default = _wp_emoji_list(); - $this->assertNotEmpty( $default ); + $this->assertNotEmpty( $default, 'Default should not be empty' ); $entities = _wp_emoji_list( 'entities' ); - $this->assertNotEmpty( $entities ); - - $this->assertSame( $default, $entities ); + $this->assertNotEmpty( $entities, 'Entities should not be empty' ); + $this->assertIsArray( $entities, 'Entities should be an array' ); + // Emoji 17 contains 4007 entities, this number will only increase. + $this->assertGreaterThanOrEqual( 4007, count( $entities ), 'Entities should contain at least 4007 items' ); + $this->assertSame( $default, $entities, 'Entities should be returned by default' ); $partials = _wp_emoji_list( 'partials' ); - $this->assertNotEmpty( $partials ); + $this->assertNotEmpty( $partials, 'Partials should not be empty' ); + $this->assertIsArray( $partials, 'Partials should be an array' ); + // Emoji 17 contains 1438 partials, this number will only increase. + $this->assertGreaterThanOrEqual( 1438, count( $partials ), 'Partials should contain at least 1438 items' ); $this->assertNotSame( $default, $partials ); } @@ -98,21 +148,28 @@ public function data_wp_encode_emoji() { '🙂', ), array( - // Skin tone, gender, ZWJ, emoji selector. - '👮🏼‍♀️', - '👮🏼‍♀️', + // Bird, ZWJ, black large square, emoji selector. + '🐦‍⬛', + '🐦‍⬛', ), array( // Unicode 10. '🧚', '🧚', ), + array( + // Hairy creature (Unicode 17). + '🫈', + '🫈', + ), ); } /** * @ticket 35293 * @dataProvider data_wp_encode_emoji + * + * @covers ::wp_encode_emoji */ public function test_wp_encode_emoji( $emoji, $expected ) { $this->assertSame( $expected, wp_encode_emoji( $emoji ) ); @@ -140,6 +197,11 @@ public function data_wp_staticize_emoji() { '🧚', '<img src="' . $this->png_cdn . '1f9da.png" alt="🧚" class="wp-smiley" style="height: 1em; max-height: 1em;" />', ), + array( + // Hairy creature (Unicode 17). + '🫈', + '<img src="' . $this->png_cdn . '1fac8.png" alt="🫈" class="wp-smiley" style="height: 1em; max-height: 1em;" />', + ), ); return $data; @@ -148,6 +210,8 @@ public function data_wp_staticize_emoji() { /** * @ticket 35293 * @dataProvider data_wp_staticize_emoji + * + * @covers ::wp_staticize_emoji */ public function test_wp_staticize_emoji( $emoji, $expected ) { $this->assertSame( $expected, wp_staticize_emoji( $emoji ) ); diff --git a/tests/phpunit/tests/formatting/ent2ncr.php b/tests/phpunit/tests/formatting/ent2ncr.php index f5859974efb8e..db60ba97ffa45 100644 --- a/tests/phpunit/tests/formatting/ent2ncr.php +++ b/tests/phpunit/tests/formatting/ent2ncr.php @@ -2,10 +2,12 @@ /** * @group formatting + * + * @covers ::ent2ncr */ class Tests_Formatting_Ent2ncr extends WP_UnitTestCase { /** - * @dataProvider entities + * @dataProvider data_entities */ public function test_converts_named_entities_to_numeric_character_references( $entity, $ncr ) { $entity = '&' . $entity . ';'; @@ -17,7 +19,7 @@ public function test_converts_named_entities_to_numeric_character_references( $e * Get test data from files, one test per line. * Comments start with "###". */ - public function entities() { + public function data_entities() { $entities = file( DIR_TESTDATA . '/formatting/entities.txt' ); $data_provided = array(); foreach ( $entities as $line ) { @@ -34,4 +36,3 @@ public function entities() { return $data_provided; } } - diff --git a/tests/phpunit/tests/formatting/escAttr.php b/tests/phpunit/tests/formatting/escAttr.php index e60c5e955294d..879b86ed05b36 100644 --- a/tests/phpunit/tests/formatting/escAttr.php +++ b/tests/phpunit/tests/formatting/escAttr.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::esc_attr */ class Tests_Formatting_EscAttr extends WP_UnitTestCase { public function test_esc_attr_quotes() { diff --git a/tests/phpunit/tests/formatting/escHtml.php b/tests/phpunit/tests/formatting/escHtml.php index 3c6b918cfca3d..bcd21043d75f2 100644 --- a/tests/phpunit/tests/formatting/escHtml.php +++ b/tests/phpunit/tests/formatting/escHtml.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::esc_html */ class Tests_Formatting_EscHtml extends WP_UnitTestCase { public function test_esc_html_basics() { diff --git a/tests/phpunit/tests/formatting/escJs.php b/tests/phpunit/tests/formatting/escJs.php index ddb5420174304..0f4365d1b3041 100644 --- a/tests/phpunit/tests/formatting/escJs.php +++ b/tests/phpunit/tests/formatting/escJs.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::esc_js */ class Tests_Formatting_EscJs extends WP_UnitTestCase { public function test_js_escape_simple() { diff --git a/tests/phpunit/tests/formatting/escTextarea.php b/tests/phpunit/tests/formatting/escTextarea.php index 8582216ec8409..8b3d0cf94487a 100644 --- a/tests/phpunit/tests/formatting/escTextarea.php +++ b/tests/phpunit/tests/formatting/escTextarea.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::esc_textarea */ class Tests_Formatting_EscTextarea extends WP_UnitTestCase { @@ -10,7 +12,6 @@ public function charset_iso_8859_1() { } /* - * Only fails in PHP 5.4 onwards * @ticket 23688 */ public function test_esc_textarea_charset_iso_8859_1() { diff --git a/tests/phpunit/tests/formatting/escUrl.php b/tests/phpunit/tests/formatting/escUrl.php index 4958c115d30f9..e994ecdebd30b 100644 --- a/tests/phpunit/tests/formatting/escUrl.php +++ b/tests/phpunit/tests/formatting/escUrl.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::esc_url */ class Tests_Formatting_EscUrl extends WP_UnitTestCase { @@ -40,6 +42,9 @@ public function test_relative() { $this->assertSame( '?foo=bar', esc_url( '?foo=bar' ) ); } + /** + * @covers ::sanitize_url + */ public function test_all_url_parts() { $url = 'https://user:pass@host.example.com:1234/path;p=1?query=2&r[]=3#fragment'; @@ -56,7 +61,7 @@ public function test_all_url_parts() { ), parse_url( $url ) ); - $this->assertSame( 'https://user:pass@host.example.com:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url_raw( $url ) ); + $this->assertSame( 'https://user:pass@host.example.com:1234/path;p=1?query=2&r%5B%5D=3#fragment', sanitize_url( $url ) ); $this->assertSame( 'https://user:pass@host.example.com:1234/path;p=1?query=2&r%5B%5D=3#fragment', esc_url( $url ) ); } @@ -68,10 +73,13 @@ public function test_bare() { $this->assertSame( 'http://баба.org/баба', esc_url( 'баба.org/баба' ) ); } + /** + * @covers ::sanitize_url + */ public function test_encoding() { - $this->assertSame( 'http://example.com?foo=1&bar=2', esc_url_raw( 'http://example.com?foo=1&bar=2' ) ); - $this->assertSame( 'http://example.com?foo=1&bar=2', esc_url_raw( 'http://example.com?foo=1&bar=2' ) ); - $this->assertSame( 'http://example.com?foo=1&bar=2', esc_url_raw( 'http://example.com?foo=1&bar=2' ) ); + $this->assertSame( 'http://example.com?foo=1&bar=2', sanitize_url( 'http://example.com?foo=1&bar=2' ) ); + $this->assertSame( 'http://example.com?foo=1&bar=2', sanitize_url( 'http://example.com?foo=1&bar=2' ) ); + $this->assertSame( 'http://example.com?foo=1&bar=2', sanitize_url( 'http://example.com?foo=1&bar=2' ) ); $this->assertSame( 'http://example.com?foo=1&bar=2', esc_url( 'http://example.com?foo=1&bar=2' ) ); $this->assertSame( 'http://example.com?foo=1&bar=2', esc_url( 'http://example.com?foo=1&bar=2' ) ); @@ -81,14 +89,49 @@ public function test_encoding() { $this->assertSame( "http://example.com?url={$param}", esc_url( "http://example.com?url={$param}" ) ); } + /** + * @ticket 23605 + * @ticket 52886 + * + * @covers ::wp_allowed_protocols + */ public function test_protocol() { $this->assertSame( 'http://example.com', esc_url( 'http://example.com' ) ); $this->assertSame( '', esc_url( 'nasty://example.com/' ) ); $this->assertSame( - '', + 'https://example.com', + esc_url( + 'example.com', + array( + 'https', + ) + ) + ); + $this->assertSame( + 'http://example.com', + esc_url( + 'example.com', + array( + 'http', + ) + ) + ); + $this->assertSame( + 'https://example.com', + esc_url( + 'example.com', + array( + 'https', + 'http', + ) + ) + ); + $this->assertSame( + 'http://example.com', esc_url( 'example.com', array( + 'http', 'https', ) ) @@ -113,7 +156,11 @@ public function test_protocol() { ) ); - foreach ( wp_allowed_protocols() as $scheme ) { + $protocols = wp_allowed_protocols(); + + $this->assertNotEmpty( $protocols ); + + foreach ( $protocols as $scheme ) { $this->assertSame( "{$scheme}://example.com", esc_url( "{$scheme}://example.com" ), $scheme ); $this->assertSame( "{$scheme}://example.com", @@ -140,7 +187,6 @@ public function test_protocol() { ) ) ); - } /** @@ -187,10 +233,12 @@ public function test_square_brackets() { /** * Courtesy of http://blog.lunatech.com/2009/02/03/what-every-web-developer-must-know-about-url-encoding + * + * @covers ::sanitize_url */ public function test_reserved_characters() { $url = "http://example.com/:@-._~!$&'()*+,=;:@-._~!$&'()*+,=:@-._~!$&'()*+,==?/?:@-._~!$%27()*+,;=/?:@-._~!$%27()*+,;==#/?:@-._~!$&'()*+,;="; - $this->assertSame( $url, esc_url_raw( $url ) ); + $this->assertSame( $url, sanitize_url( $url ) ); } /** @@ -243,9 +291,11 @@ public function test_mailto_with_spaces() { /** * @ticket 28015 + * + * @covers ::sanitize_url */ - public function test_invalid_charaters() { - $this->assertEmpty( esc_url_raw( '"^<>{}`' ) ); + public function test_invalid_characters() { + $this->assertEmpty( sanitize_url( '"^<>{}`' ) ); } /** @@ -261,5 +311,4 @@ public function test_ipv6_hosts() { $this->assertSame( '//[::FFFF::127.0.0.1]/?foo%5Bbar%5D=baz', esc_url( '//[::FFFF::127.0.0.1]/?foo[bar]=baz' ) ); $this->assertSame( 'http://[::FFFF::127.0.0.1]/?foo%5Bbar%5D=baz', esc_url( 'http://[::FFFF::127.0.0.1]/?foo[bar]=baz' ) ); } - } diff --git a/tests/phpunit/tests/formatting/escXml.php b/tests/phpunit/tests/formatting/escXml.php index fa6738dca0983..6fdbd136a7236 100644 --- a/tests/phpunit/tests/formatting/escXml.php +++ b/tests/phpunit/tests/formatting/escXml.php @@ -2,12 +2,14 @@ /** * @group formatting + * + * @covers ::esc_xml */ class Tests_Formatting_EscXml extends WP_UnitTestCase { /** * Test basic escaping * - * @dataProvider _test_esc_xml_basics_dataprovider + * @dataProvider data_esc_xml_basics * * @param string $source The source string to be escaped. * @param string $expected The expected escaped value of `$source`. @@ -25,7 +27,7 @@ public function test_esc_xml_basics( $source, $expected ) { * @type string $expected The expected escaped value of `$source`. * } */ - public function _test_esc_xml_basics_dataprovider() { + public function data_esc_xml_basics() { return array( // Simple string. array( @@ -42,6 +44,11 @@ public function _test_esc_xml_basics_dataprovider() { "SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1", 'SELECT meta_key, meta_value FROM wp_trunk_sitemeta WHERE meta_key IN ('site_name', 'siteurl', 'active_sitewide_plugins', '_site_transient_timeout_theme_roots', '_site_transient_theme_roots', 'site_admins', 'can_compress_scripts', 'global_terms_enabled') AND site_id = 1', ), + // Zero string. + array( + '0', + '0', + ), ); } @@ -77,7 +84,7 @@ public function test_ignores_existing_entities() { /** * Test that CDATA Sections are not escaped. * - * @dataProvider _test_ignores_cdata_sections_dataprovider + * @dataProvider data_ignores_cdata_sections * * @param string $source The source string to be escaped. * @param string $expected The expected escaped value of `$source`. @@ -95,7 +102,7 @@ public function test_ignores_cdata_sections( $source, $expected ) { * @type string $expected The expected escaped value of `$source`. * } */ - public function _test_ignores_cdata_sections_dataprovider() { + public function data_ignores_cdata_sections() { return array( // basic CDATA Section containing chars that would otherwise be escaped if not in a CDATA Section // not to mention the CDATA Section markup itself :-) diff --git a/tests/phpunit/tests/formatting/excerptRemoveBlocks.php b/tests/phpunit/tests/formatting/excerptRemoveBlocks.php index 56570475569a5..2097c35bbf5b8 100644 --- a/tests/phpunit/tests/formatting/excerptRemoveBlocks.php +++ b/tests/phpunit/tests/formatting/excerptRemoveBlocks.php @@ -2,8 +2,9 @@ /** * @group formatting - * @covers ::excerpt_remove_blocks * @ticket 46133 + * + * @covers ::excerpt_remove_blocks */ class Tests_Formatting_ExcerptRemoveBlocks extends WP_UnitTestCase { @@ -11,7 +12,7 @@ class Tests_Formatting_ExcerptRemoveBlocks extends WP_UnitTestCase { public $content = ' <!-- wp:paragraph --> -<p>paragraph</p> +<p class="wp-block-paragraph">paragraph</p> <!-- /wp:paragraph --> <!-- wp:latest-posts {"postsToShow":3,"displayPostDate":true,"order":"asc","orderBy":"title"} /--> <!-- wp:spacer --> @@ -24,7 +25,7 @@ class Tests_Formatting_ExcerptRemoveBlocks extends WP_UnitTestCase { <!-- wp:archives {"displayAsDropdown":false,"showPostCounts":false} /--> <!-- wp:paragraph --> - <p>paragraph inside column</p> + <p class="wp-block-paragraph">paragraph inside column</p> <!-- /wp:paragraph --> </div> <!-- /wp:column --> @@ -34,12 +35,12 @@ class Tests_Formatting_ExcerptRemoveBlocks extends WP_UnitTestCase { public $filtered_content = ' -<p>paragraph</p> +<p class="wp-block-paragraph">paragraph</p> - <p>paragraph inside column</p> + <p class="wp-block-paragraph">paragraph inside column</p> '; @@ -61,7 +62,7 @@ public function render_fake_block() { */ public function set_up() { parent::set_up(); - self::$post_id = $this->factory()->post->create( + self::$post_id = self::factory()->post->create( array( 'post_excerpt' => '', // Empty excerpt, so it has to be generated. 'post_content' => '<!-- wp:core/fake /-->', @@ -116,6 +117,8 @@ public function test_excerpt_remove_blocks() { * `the_content` gets applied, just like shortcodes. * * @ticket 46133 + * + * @covers ::do_blocks */ public function test_excerpt_infinite_loop() { $query = new WP_Query( diff --git a/tests/phpunit/tests/formatting/excerptRemoveFootnotes.php b/tests/phpunit/tests/formatting/excerptRemoveFootnotes.php new file mode 100644 index 0000000000000..12ea3acb04cbe --- /dev/null +++ b/tests/phpunit/tests/formatting/excerptRemoveFootnotes.php @@ -0,0 +1,48 @@ +<?php +/** + * @group formatting + * @ticket 58805 + * + * @covers ::excerpt_remove_footnotes + */ + +class Tests_Formatting_ExcerptRemoveFootnotes extends WP_UnitTestCase { + /** + * @ticket 58805 + * + * @dataProvider data_remove_footnotes + * + * @param string $expected Expected output. + * @param string $content Content to run strip_shortcodes() on. + */ + public function test_remove_footnotes( $expected, $content ) { + $this->assertSame( $expected, excerpt_remove_footnotes( $content ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_remove_footnotes() { + return array( + 'no footnote' => array( + 'expected' => '<p>This is a paragraph<sup class="fn" id="1"><a href="#1" id="1a">1</a></sup>.</p>', + 'content' => '<p>This is a paragraph<sup class="fn" id="1"><a href="#1" id="1a">1</a></sup>.</p>', + ), + 'one footnote' => array( + 'expected' => '<p>This is a <a href="https://wordpress.org" data-type="URL" data-id="https://wordpress.org">paragraph</a>.</p>', + 'content' => '<p>This is a <a href="https://wordpress.org" data-type="URL" data-id="https://wordpress.org">paragraph</a><sup data-fn="d3b825b6-1890-4cb3-b276-002137515e99" class="fn"><a href="#d3b825b6-1890-4cb3-b276-002137515e99" id="d3b825b6-1890-4cb3-b276-002137515e99-link">1</a></sup>.</p>', + + ), + 'multiple footnotes in block content' => array( + 'expected' => '<!-- wp:list --><ul><!-- wp:list-item --><li><strong>This</strong><em><strong><sup></sup></strong></em><strong> is a list</strong></li><!-- /wp:list-item --></ul><!-- /wp:list -->', + 'content' => '<!-- wp:list --><ul><!-- wp:list-item --><li><strong>This</strong><em><strong><sup><sup data-fn="e2fce624-74a5-4068-a20c-6ef793f1644c" class="fn"><a href="#e2fce624-74a5-4068-a20c-6ef793f1644c" id="e2fce624-74a5-4068-a20c-6ef793f1644c-link">2</a></sup></sup></strong></em><strong> is a list</strong><sup data-fn="ea7e892e-7bc2-424b-936b-36ec64f1c2fc" class="fn"><a href="#ea7e892e-7bc2-424b-936b-36ec64f1c2fc" id="ea7e892e-7bc2-424b-936b-36ec64f1c2fc-link">3</a></sup></li><!-- /wp:list-item --></ul><!-- /wp:list -->', + ), + 'footnotes around non-latin script' => array( + 'expected' => '<h2 class="wp-block-heading has-background" style="background-color:#f93b3b">これは見出しです</h2>', + 'content' => '<h2 class="wp-block-heading has-background" style="background-color:#f93b3b">これは<sup data-fn="382b3e39-4b0d-4b83-8461-c13f82fdbcfb" class="fn"><a href="#382b3e39-4b0d-4b83-8461-c13f82fdbcfb" id="382b3e39-4b0d-4b83-8461-c13f82fdbcfb-link">1</a></sup>見出しです<sup data-fn="addb0459-a048-453a-9101-dba64f63a630" class="fn"><a href="#addb0459-a048-453a-9101-dba64f63a630" id="addb0459-a048-453a-9101-dba64f63a630-link">2</a></sup></h2>', + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/getBloginfo.php b/tests/phpunit/tests/formatting/getBloginfo.php index 2c80cd99b892d..37f449bac6ab5 100644 --- a/tests/phpunit/tests/formatting/getBloginfo.php +++ b/tests/phpunit/tests/formatting/getBloginfo.php @@ -2,11 +2,13 @@ /** * @group formatting + * + * @covers ::get_bloginfo */ class Tests_Formatting_GetBloginfo extends WP_UnitTestCase { /** - * @dataProvider locales + * @dataProvider data_get_bloginfo_language * @ticket 28303 */ public function test_get_bloginfo_language( $test_locale, $expected ) { @@ -20,7 +22,7 @@ public function test_get_bloginfo_language( $test_locale, $expected ) { $locale = $old_locale; } - public function locales() { + public function data_get_bloginfo_language() { return array( // Locale, language code. array( 'en_US', 'en-US' ), @@ -35,6 +37,8 @@ public function locales() { /** * @ticket 27942 + * + * @covers ::sanitize_option */ public function test_bloginfo_sanitize_option() { $old_values = array( diff --git a/tests/phpunit/tests/formatting/getUrlInContent.php b/tests/phpunit/tests/formatting/getUrlInContent.php index 242fe2db9c03a..ce99099ef5ad1 100644 --- a/tests/phpunit/tests/formatting/getUrlInContent.php +++ b/tests/phpunit/tests/formatting/getUrlInContent.php @@ -2,15 +2,31 @@ /** * @group formatting + * + * @covers ::get_url_in_content */ class Tests_Formatting_GetUrlInContent extends WP_UnitTestCase { /** - * URL Content Data Provider + * Tests the get_url_in_content() function. * - * array ( input_txt, converted_output_txt ) + * @dataProvider data_get_url_in_content */ - public function get_input_output() { + public function test_get_url_in_content( $input, $expected ) { + $this->assertSame( $expected, get_url_in_content( $input ) ); + } + + /** + * Data provider. + * + * @return array { + * @type array { + * @type string $input Input content. + * @type string $expected Expected output. + * } + * } + */ + public function data_get_url_in_content() { return array( array( // Empty content. '', @@ -38,13 +54,4 @@ public function get_input_output() { ), ); } - - /** - * Validate the get_url_in_content function - * - * @dataProvider get_input_output - */ - public function test_get_url_in_content( $in_str, $exp_str ) { - $this->assertSame( $exp_str, get_url_in_content( $in_str ) ); - } } diff --git a/tests/phpunit/tests/formatting/humanTimeDiff.php b/tests/phpunit/tests/formatting/humanTimeDiff.php index f4eb2e2ad6090..fcf4ff43d46c9 100644 --- a/tests/phpunit/tests/formatting/humanTimeDiff.php +++ b/tests/phpunit/tests/formatting/humanTimeDiff.php @@ -3,13 +3,15 @@ /** * @group formatting * @ticket 38773 + * + * @covers ::human_time_diff */ class Tests_Formatting_HumanTimeDiff extends WP_UnitTestCase { /** * @group formatting * @ticket 38773 - * @dataProvider data_test_human_time_diff + * @dataProvider data_human_time_diff */ public function test_human_time_diff( $expected, $stopdate, $message ) { $startdate = new DateTime( '2016-01-01 12:00:00' ); @@ -17,7 +19,7 @@ public function test_human_time_diff( $expected, $stopdate, $message ) { } // Data for test_human_time_diff. - public function data_test_human_time_diff() { + public function data_human_time_diff() { return array( array( '37 seconds', @@ -25,7 +27,7 @@ public function data_test_human_time_diff() { 'Test a difference of 37 seconds.', ), array( - '5 mins', + '5 minutes', new DateTime( '2016-01-01 12:05:00' ), 'Test a difference of 5 minutes.', ), diff --git a/tests/phpunit/tests/formatting/isEmail.php b/tests/phpunit/tests/formatting/isEmail.php index eea7b619262bc..b793af2c4a70d 100644 --- a/tests/phpunit/tests/formatting/isEmail.php +++ b/tests/phpunit/tests/formatting/isEmail.php @@ -1,34 +1,181 @@ <?php - /** + * Tests for the is_email() function. + * * @group formatting + * + * @covers ::is_email */ class Tests_Formatting_IsEmail extends WP_UnitTestCase { - public function test_returns_the_email_address_if_it_is_valid() { - $data = array( + /** + * Ensures that valid emails are returned unchanged. + * + * @ticket 31992 + * + * @dataProvider data_valid_email_provider + * + * @param string $email Valid email address. + */ + public function test_returns_the_email_address_if_it_is_valid( $email ) { + $this->assertSame( + $email, + is_email( $email ), + 'Should return the given email address unchanged when valid.' + ); + } + + /** + * Data provider. + * + * @return Generator + */ + public static function data_valid_email_provider() { + $valid_emails = array( 'bob@example.com', 'phil@example.info', + 'phil@TLA.example', 'ace@204.32.222.14', 'kevin@many.subdomains.make.a.happy.man.edu', 'a@b.co', 'bill+ted@example.com', + '..@example.com', ); - foreach ( $data as $datum ) { - $this->assertSame( $datum, is_email( $datum ), $datum ); + + foreach ( $valid_emails as $email ) { + yield $email => array( $email ); } } - public function test_returns_false_if_given_an_invalid_email_address() { - $data = array( + /** + * Ensures that unrecognized email addresses are rejected. + * + * @ticket 31992 + * + * @dataProvider data_invalid_email_provider + * + * @param string $email Invalid or unrecognized-to-WordPress email address. + */ + public function test_returns_false_if_given_an_invalid_email_address( $email ) { + $this->assertFalse( + is_email( $email ), + 'Should have rejected the email as invalid.' + ); + } + + /** + * Data provider. + * + * @return Generator + */ + public static function data_invalid_email_provider() { + $invalid_emails = array( 'khaaaaaaaaaaaaaaan!', 'http://bob.example.com/', "sif i'd give u it, spamer!1", 'com.exampleNOSPAMbob', 'bob@your mom', 'a@b.c', + '" "@b.c', + '"@"@b.c', + 'a@route.org@b.c', + 'h(aj@couc.ou', // bad comment. + 'hi@', + 'hi@hi@couc.ou', // double @. + + /* + * The next address is not deliverable as described, + * SMTP servers should strip the (ab), so it is very + * likely a source of confusion or a typo. + * Best rejected. + */ + '(ab)cd@couc.ou', + + /* + * The next address is not globally deliverable, + * so it may work with PHPMailer and break with + * mail sending services. Best not allow users + * to paint themselves into that corner. This also + * avoids security problems like those that were + * used to probe the WordPress server's local + * network. + */ + 'toto@to', + + /* + * Several addresses are best rejected because + * we don't want to allow sending to fe80::, 192.168 + * and other special addresses; that too might + * be used to probe the WordPress server's local + * network. + */ + 'to@[2001:db8::1]', + 'to@[IPv6:2001:db8::1]', + 'to@[192.168.1.1]', + + /* + * Ill-formed UTF-8 byte sequences must be rejected. + * A lone continuation byte (0x80) is not valid UTF-8 + * whether it appears in the local part or the domain. + */ + "a\x80b@example.com", // invalid UTF-8 in local part. + "abc@\x80.org", // invalid UTF-8 in domain subdomain. ); - foreach ( $data as $datum ) { - $this->assertFalse( is_email( $datum ), $datum ); + + foreach ( $invalid_emails as $email ) { + yield self::invalid_utf8_as_ascii( $email ) => array( $email ); } } + + /** + * Transforms invalid byte sequences in UTF-8 into representations of + * each byte value, according to the maximal subpart rule. + * + * Example: + * + * // For valid UTF-8 the output is the input. + * 'test' === invalid_utf8_as_ascii( 'test' ); + * + * // Invalid bytes are represented with their hex value. + * 'a(0x80)b' === invalid_utf8_as_ascii( "a\x80b" ); + * + * // Invalid byte sequences form maximal subparts. + * '(0xC2)(0xEF 0xBF)' === invalid_utf8_as_ascii( "\xC2\xEF\xBF" ); + * + * @param string $text + * @return string + */ + private static function invalid_utf8_as_ascii( string $text ): string { + $output = ''; + $at = 0; + $was_at = 0; + $end = strlen( $text ); + $invalid_bytes = 0; + + while ( $at < $end ) { + if ( 0 === _wp_scan_utf8( $text, $at, $invalid_bytes ) && 0 === $invalid_bytes ) { + break; + } + + if ( $at > $was_at ) { + $output .= substr( $text, $was_at, $at - $was_at ); + } + + if ( $invalid_bytes > 0 ) { + $output .= '('; + + for ( $i = 0; $i < $invalid_bytes; $i++ ) { + $space = $i > 0 ? ' ' : ''; + $as_hex = bin2hex( $text[ $at + $i ] ); + $output .= "{$space}0x{$as_hex}"; + } + + $output .= ')'; + } + + $at += $invalid_bytes; + $was_at = $at; + } + + return $output; + } } diff --git a/tests/phpunit/tests/formatting/likeEscape.php b/tests/phpunit/tests/formatting/likeEscape.php index d24f4e6675e1f..4902056730289 100644 --- a/tests/phpunit/tests/formatting/likeEscape.php +++ b/tests/phpunit/tests/formatting/likeEscape.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::like_escape */ class Tests_Formatting_LikeEscape extends WP_UnitTestCase { /** diff --git a/tests/phpunit/tests/formatting/linksAddBaseUrl.php b/tests/phpunit/tests/formatting/linksAddBaseUrl.php new file mode 100644 index 0000000000000..cbc8489a500e0 --- /dev/null +++ b/tests/phpunit/tests/formatting/linksAddBaseUrl.php @@ -0,0 +1,101 @@ +<?php + +/** + * Tests for the links_add_base_url() function. + * + * @group formatting + * + * @covers ::links_add_base_url + */ +class Tests_Formatting_LinksAddBaseUrl extends WP_UnitTestCase { + + /** + * @ticket 60389 + * + * @dataProvider data_links_add_base_url + */ + public function test_links_add_base_url( $content, $base, $attrs, $expected ) { + if ( is_null( $attrs ) ) { + $this->assertSame( $expected, links_add_base_url( $content, $base ) ); + } else { + $this->assertSame( $expected, links_add_base_url( $content, $base, $attrs ) ); + } + } + + /** + * Data provider. + * + * @return array { + * @type array { + * @type string $content String to search for links in. + * @type string $base The base URL to prefix to links. + * @type array|null $attrs The attributes which should be processed. + * @type string $expected Expected output. + * } + * } + */ + public function data_links_add_base_url() { + return array( + 'https' => array( + 'content' => '<a href="url" />', + 'base' => 'https://localhost', + 'attrs' => null, + 'expected' => '<a href="https://localhost/url" />', + ), + 'http' => array( + 'content' => '<a href="url" />', + 'base' => 'http://localhost', + 'attrs' => null, + 'expected' => '<a href="http://localhost/url" />', + ), + 'relative scheme' => array( + 'content' => '<a href="//localhost/url" />', + 'base' => 'http://localhost', + 'attrs' => null, + 'expected' => '<a href="http://localhost/url" />', + ), + 'empty array' => array( + 'content' => '<a href="url" target="_blank" />', + 'base' => 'https://localhost', + 'attrs' => array(), + 'expected' => '<a href="https://localhost/url" target="https://localhost/_blank" />', + ), + 'data-url' => array( + 'content' => '<a href="url" data-url="url" />', + 'base' => 'https://localhost', + 'attrs' => array( 'data-url', 'href' ), + 'expected' => '<a href="https://localhost/url" data-url="https://localhost/url" />', + ), + 'not relative' => array( + 'content' => '<a href="https://localhost/url" />', + 'base' => 'https://localbase', + 'attrs' => null, + 'expected' => '<a href="https://localhost/url" />', + ), + 'no href' => array( + 'content' => '<a data-url="/url" />', + 'base' => 'https://localhost', + 'attrs' => null, + 'expected' => '<a data-url="/url" />', + ), + 'img' => array( + 'content' => '<img src="/url" />', + 'base' => 'https://localhost', + 'attrs' => null, + 'expected' => '<img src="https://localhost/url" />', + ), + 'ftp' => array( + 'content' => '<a href="/url" >sss</a>', + 'base' => 'ftp://localhost', + 'attrs' => null, + 'expected' => '<a href="ftp://localhost/url" >sss</a>', + ), + 'ftps' => array( + 'content' => '<a href="/url" >sss</a>', + 'base' => 'ftps://localhost', + 'attrs' => null, + 'expected' => '<a href="ftps://localhost/url" >sss</a>', + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/linksAddTarget.php b/tests/phpunit/tests/formatting/linksAddTarget.php index ce8e506440dc9..31bc4cdcb3b84 100644 --- a/tests/phpunit/tests/formatting/linksAddTarget.php +++ b/tests/phpunit/tests/formatting/linksAddTarget.php @@ -1,14 +1,41 @@ <?php /** + * Tests for the links_add_target() function. + * * @group formatting + * + * @covers ::links_add_target */ class Tests_Formatting_LinksAddTarget extends WP_UnitTestCase { + + /** + * @ticket 26164 + * + * @dataProvider data_links_add_target + */ + public function test_links_add_target( $content, $target, $tags, $expected ) { + if ( is_null( $target ) ) { + $this->assertSame( $expected, links_add_target( $content ) ); + } elseif ( is_null( $tags ) ) { + $this->assertSame( $expected, links_add_target( $content, $target ) ); + } else { + $this->assertSame( $expected, links_add_target( $content, $target, $tags ) ); + } + } + /** - * Test Content DataProvider + * Data provider. * - * array ( input_txt, converted_output_txt) + * @return array { + * @type array { + * @type string $content String to search for links in. + * @type string $target The target to add to the links. + * @type array|null $tags An array of tags to apply to. + * @type string $expected Expected output. + * } + * } */ - public function get_input_output() { + public function data_links_add_target() { return array( array( 'MY CONTENT <div> SOME ADDITIONAL TEXT <a href="XYZ" src="ABC">LINK</a> HERE </div> END TEXT', @@ -90,19 +117,4 @@ public function get_input_output() { ), ); } - - /** - * Validate the normalize_whitespace function - * - * @dataProvider get_input_output - */ - public function test_normalize_whitespace( $content, $target, $tags, $exp_str ) { - if ( true === is_null( $target ) ) { - $this->assertSame( $exp_str, links_add_target( $content ) ); - } elseif ( true === is_null( $tags ) ) { - $this->assertSame( $exp_str, links_add_target( $content, $target ) ); - } else { - $this->assertSame( $exp_str, links_add_target( $content, $target, $tags ) ); - } - } } diff --git a/tests/phpunit/tests/formatting/makeClickable.php b/tests/phpunit/tests/formatting/makeClickable.php index a0db9860760a0..5e4c62a1494ef 100644 --- a/tests/phpunit/tests/formatting/makeClickable.php +++ b/tests/phpunit/tests/formatting/makeClickable.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::make_clickable */ class Tests_Formatting_MakeClickable extends WP_UnitTestCase { public function test_mailto_xss() { @@ -9,369 +11,430 @@ public function test_mailto_xss() { $this->assertSame( $in, make_clickable( $in ) ); } - public function test_valid_mailto() { - $valid_emails = array( - 'foo@example.com', - 'foo.bar@example.com', - 'Foo.Bar@a.b.c.d.example.com', - '0@example.com', - 'foo@example-example.com', - ); - foreach ( $valid_emails as $email ) { - $this->assertSame( '<a href="mailto:' . $email . '">' . $email . '</a>', make_clickable( $email ) ); - } - } - - public function test_invalid_mailto() { - $invalid_emails = array( - 'foo', - 'foo@', - 'foo@@example.com', - '@example.com', - 'foo @example.com', - 'foo@example', - ); - foreach ( $invalid_emails as $email ) { - $this->assertSame( $email, make_clickable( $email ) ); - } - } - /** - * Tests that make_clickable() will not link trailing periods, commas, - * and (semi-)colons in URLs with protocol (i.e. http://wordpress.org). + * @dataProvider data_valid_mailto + * + * @param string $email Email to test. */ - public function test_strip_trailing_with_protocol() { - $urls_before = array( - 'http://wordpress.org/hello.html', - 'There was a spoon named http://wordpress.org. Alice!', - 'There was a spoon named http://wordpress.org, said Alice.', - 'There was a spoon named http://wordpress.org; said Alice.', - 'There was a spoon named http://wordpress.org: said Alice.', - 'There was a spoon named (http://wordpress.org) said Alice.', - ); - $urls_expected = array( - '<a href="http://wordpress.org/hello.html" rel="nofollow">http://wordpress.org/hello.html</a>', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>. Alice!', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>, said Alice.', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>; said Alice.', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>: said Alice.', - 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>) said Alice.', - ); - - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } + public function test_valid_mailto( $email ) { + $this->assertSame( '<a href="mailto:' . $email . '">' . $email . '</a>', make_clickable( $email ) ); } /** - * Tests that make_clickable() will not link trailing periods, commas, - * and (semi-)colons in URLs with protocol (i.e. http://wordpress.org). + * Data provider. + * + * @return array */ - public function test_strip_trailing_with_protocol_nothing_afterwards() { - $urls_before = array( - 'http://wordpress.org/hello.html', - 'There was a spoon named http://wordpress.org.', - 'There was a spoon named http://wordpress.org,', - 'There was a spoon named http://wordpress.org;', - 'There was a spoon named http://wordpress.org:', - 'There was a spoon named (http://wordpress.org)', - 'There was a spoon named (http://wordpress.org)x', - ); - $urls_expected = array( - '<a href="http://wordpress.org/hello.html" rel="nofollow">http://wordpress.org/hello.html</a>', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>.', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>,', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>;', - 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>:', - 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>)', - 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>)x', + public function data_valid_mailto() { + return array( + array( 'foo@example.com' ), + array( 'foo.bar@example.com' ), + array( 'Foo.Bar@a.b.c.d.example.com' ), + array( '0@example.com' ), + array( 'foo@example-example.com' ), ); - - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } } /** - * Tests that make_clickable() will not link trailing periods, commas, - * and (semi-)colons in URLs without protocol (i.e. www.wordpress.org). + * @dataProvider data_invalid_mailto + * + * @param string $email Email to test. */ - public function test_strip_trailing_without_protocol() { - $urls_before = array( - 'www.wordpress.org', - 'There was a spoon named www.wordpress.org. Alice!', - 'There was a spoon named www.wordpress.org, said Alice.', - 'There was a spoon named www.wordpress.org; said Alice.', - 'There was a spoon named www.wordpress.org: said Alice.', - 'There was a spoon named www.wordpress.org) said Alice.', - ); - $urls_expected = array( - '<a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>. Alice!', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>, said Alice.', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>; said Alice.', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>: said Alice.', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>) said Alice.', - ); - - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } + public function test_invalid_mailto( $email ) { + $this->assertSame( $email, make_clickable( $email ) ); } /** - * Tests that make_clickable() will not link trailing periods, commas, - * and (semi-)colons in URLs without protocol (i.e. www.wordpress.org). + * Data provider. + * + * @return array */ - public function test_strip_trailing_without_protocol_nothing_afterwards() { - $urls_before = array( - 'www.wordpress.org', - 'There was a spoon named www.wordpress.org.', - 'There was a spoon named www.wordpress.org,', - 'There was a spoon named www.wordpress.org;', - 'There was a spoon named www.wordpress.org:', - 'There was a spoon named www.wordpress.org)', - ); - $urls_expected = array( - '<a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>.', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>,', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>;', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>:', - 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>)', + public function data_invalid_mailto() { + return array( + array( 'foo' ), + array( 'foo@' ), + array( 'foo@@example.com' ), + array( '@example.com' ), + array( 'foo @example.com' ), + array( 'foo@example' ), ); - - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } } /** * @ticket 4570 - */ - public function test_iri() { - $urls_before = array( - 'http://www.詹姆斯.com/', - 'http://bg.wikipedia.org/Баба', - 'http://example.com/?a=баба&b=дядо', - ); - $urls_expected = array( - '<a href="http://www.詹姆斯.com/" rel="nofollow">http://www.詹姆斯.com/</a>', - '<a href="http://bg.wikipedia.org/Баба" rel="nofollow">http://bg.wikipedia.org/Баба</a>', - '<a href="http://example.com/?a=баба&b=дядо" rel="nofollow">http://example.com/?a=баба&b=дядо</a>', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } - } - - /** * @ticket 10990 + * @ticket 11211 + * @ticket 14993 + * @ticket 16892 + * + * @dataProvider data_urls + * + * @param string $text Content to test. + * @param string $expected Expected results. */ - public function test_brackets_in_urls() { - $urls_before = array( - 'http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)', - '(http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software))', - 'blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software) blah', - 'blah (http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah', - 'blah blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software) blah blah', - 'blah blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah blah', - 'blah blah (http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah blah', - 'blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software).) blah blah', - 'blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software).)moreurl blah blah', - 'In his famous speech “You and Your research” (here: - http://www.cs.virginia.edu/~robins/YouAndYourResearch.html) - Richard Hamming wrote about people getting more done with their doors closed, but', - ); - $urls_expected = array( - '<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>', - '(<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>)', - 'blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a> blah', - 'blah (<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah', - 'blah blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a> blah blah', - 'blah blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah blah', - 'blah blah (<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah blah', - 'blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>.) blah blah', - 'blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>.)moreurl blah blah', - 'In his famous speech “You and Your research” (here: - <a href="http://www.cs.virginia.edu/~robins/YouAndYourResearch.html" rel="nofollow">http://www.cs.virginia.edu/~robins/YouAndYourResearch.html</a>) - Richard Hamming wrote about people getting more done with their doors closed, but', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } + public function test_urls( $text, $expected ) { + $this->assertSame( $expected, make_clickable( $text ) ); } /** - * Based on real comments which were incorrectly linked. + * Data provider. * - * @ticket 11211 + * @return array */ - public function test_real_world_examples() { - $urls_before = array( - 'Example: WordPress, test (some text), I love example.com (http://example.org), it is brilliant', - 'Example: WordPress, test (some text), I love example.com (http://example.com), it is brilliant', - 'Some text followed by a bracketed link with a trailing elipsis (http://example.com)...', - 'In his famous speech “You and Your research” (here: http://www.cs.virginia.edu/~robins/YouAndYourResearch.html) Richard Hamming wrote about people getting more done with their doors closed...', - ); - $urls_expected = array( - 'Example: WordPress, test (some text), I love example.com (<a href="http://example.org" rel="nofollow">http://example.org</a>), it is brilliant', - 'Example: WordPress, test (some text), I love example.com (<a href="http://example.com" rel="nofollow">http://example.com</a>), it is brilliant', - 'Some text followed by a bracketed link with a trailing elipsis (<a href="http://example.com" rel="nofollow">http://example.com</a>)...', - 'In his famous speech “You and Your research” (here: <a href="http://www.cs.virginia.edu/~robins/YouAndYourResearch.html" rel="nofollow">http://www.cs.virginia.edu/~robins/YouAndYourResearch.html</a>) Richard Hamming wrote about people getting more done with their doors closed...', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } - } + public function data_urls() { + return array( + // Does not link trailing periods, commas, and (semi-)colons in URLs with protocol (i.e. http://wordpress.org). + 'URL only' => array( + 'text' => 'http://wordpress.org/hello.html', + 'expected' => '<a href="http://wordpress.org/hello.html" rel="nofollow">http://wordpress.org/hello.html</a>', + ), + 'URL. with more content after' => array( + 'text' => 'There was a spoon named http://wordpress.org. Alice!', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>. Alice!', + ), + 'URL, with more content after' => array( + 'text' => 'There was a spoon named http://wordpress.org, said Alice.', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>, said Alice.', + ), + 'URL; with more content after' => array( + 'text' => 'There was a spoon named http://wordpress.org; said Alice.', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>; said Alice.', + ), + 'URL: with more content after' => array( + 'text' => 'There was a spoon named http://wordpress.org: said Alice.', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>: said Alice.', + ), + 'URL) with more content after' => array( + 'text' => 'There was a spoon named (http://wordpress.org) said Alice.', + 'expected' => 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>) said Alice.', + ), - /** - * @ticket 14993 - */ - public function test_twitter_hash_bang() { - $urls_before = array( - 'http://twitter.com/#!/wordpress/status/25907440233', - 'This is a really good tweet http://twitter.com/#!/wordpress/status/25907440233 !', - 'This is a really good tweet http://twitter.com/#!/wordpress/status/25907440233!', - ); - $urls_expected = array( - '<a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a>', - 'This is a really good tweet <a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a> !', - 'This is a really good tweet <a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a>!', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } - } + // Does not link trailing periods, commas, and (semi-)colons in URLs with protocol (i.e. http://wordpress.org) with nothing afterwards. + 'URL.' => array( + 'text' => 'There was a spoon named http://wordpress.org.', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>.', + ), + 'URL,' => array( + 'text' => 'There was a spoon named http://wordpress.org,', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>,', + ), + 'URL;' => array( + 'text' => 'There was a spoon named http://wordpress.org;', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>;', + ), + 'URL:' => array( + 'text' => 'There was a spoon named http://wordpress.org:', + 'expected' => 'There was a spoon named <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>:', + ), + 'URL)' => array( + 'text' => 'There was a spoon named (http://wordpress.org)', + 'expected' => 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>)', + ), + 'URL)x' => array( + 'text' => 'There was a spoon named (http://wordpress.org)x', + 'expected' => 'There was a spoon named (<a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>)x', + ), - public function test_wrapped_in_angles() { - $before = array( - 'URL wrapped in angle brackets <http://example.com/>', - 'URL wrapped in angle brackets with padding < http://example.com/ >', - 'mailto wrapped in angle brackets <foo@example.com>', - ); - $expected = array( - 'URL wrapped in angle brackets <<a href="http://example.com/" rel="nofollow">http://example.com/</a>>', - 'URL wrapped in angle brackets with padding < <a href="http://example.com/" rel="nofollow">http://example.com/</a> >', - 'mailto wrapped in angle brackets <foo@example.com>', - ); - foreach ( $before as $key => $url ) { - $this->assertSame( $expected[ $key ], make_clickable( $url ) ); - } - } + // Strip trailing without protocol: will not link trailing periods, commas, and (semi-)colons in URLs without protocol (i.e. www.wordpress.org). + 'No protocol www.URL. with content after' => array( + 'text' => 'There was a spoon named www.wordpress.org. Alice!', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>. Alice!', + ), + 'No protocol www.URL, with content after' => array( + 'text' => 'There was a spoon named www.wordpress.org, said Alice.', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>, said Alice.', + ), + 'No protocol www.URL; with content after' => array( + 'text' => 'There was a spoon named www.wordpress.org; said Alice.', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>; said Alice.', + ), + 'No protocol www.URL: with content after' => array( + 'text' => 'There was a spoon named www.wordpress.org: said Alice.', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>: said Alice.', + ), + 'No protocol www.URL) with content after' => array( + 'text' => 'There was a spoon named www.wordpress.org) said Alice.', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>) said Alice.', + ), - public function test_preceded_by_punctuation() { - $before = array( - 'Comma then URL,http://example.com/', - 'Period then URL.http://example.com/', - 'Semi-colon then URL;http://example.com/', - 'Colon then URL:http://example.com/', - 'Exclamation mark then URL!http://example.com/', - 'Question mark then URL?http://example.com/', - ); - $expected = array( - 'Comma then URL,<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - 'Period then URL.<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - 'Semi-colon then URL;<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - 'Colon then URL:<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - 'Exclamation mark then URL!<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - 'Question mark then URL?<a href="http://example.com/" rel="nofollow">http://example.com/</a>', - ); - foreach ( $before as $key => $url ) { - $this->assertSame( $expected[ $key ], make_clickable( $url ) ); - } - } + // Should not link trailing periods, commas, and (semi-)colons in URLs without protocol (i.e. www.wordpress.org). + 'No protocol www.URL' => array( + 'text' => 'www.wordpress.org', + 'expected' => '<a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>', + ), + 'No protocol www.URL.' => array( + 'text' => 'There was a spoon named www.wordpress.org.', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>.', + ), + 'No protocol www.URL,' => array( + 'text' => 'There was a spoon named www.wordpress.org,', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>,', + ), + 'No protocol www.URL;' => array( + 'text' => 'There was a spoon named www.wordpress.org;', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>;', + ), + 'No protocol www.URL:' => array( + 'text' => 'There was a spoon named www.wordpress.org:', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>:', + ), + 'No protocol www.URL)' => array( + 'text' => 'There was a spoon named www.wordpress.org)', + 'expected' => 'There was a spoon named <a href="http://www.wordpress.org" rel="nofollow">http://www.wordpress.org</a>)', + ), - public function test_dont_break_attributes() { - $urls_before = array( - "<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>", - "(<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "http://trunk.domain/testing#something (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "http://trunk.domain/testing#something - (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "<span style='text-align:center; display: block;'><object width='425' height='350'><param name='movie' value='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' /> <param name='allowfullscreen' value='true' /> <param name='wmode' value='opaque' /> <embed src='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' type='application/x-shockwave-flash' allowfullscreen='true' width='425' height='350' wmode='opaque'></embed> </object></span>", - '<a href="http://example.com/example.gif" title="Image from http://example.com">Look at this image!</a>', - ); - $urls_expected = array( - "<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>", - "(<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "<a href=\"http://trunk.domain/testing#something\" rel=\"nofollow\">http://trunk.domain/testing#something</a> (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "<a href=\"http://trunk.domain/testing#something\" rel=\"nofollow\">http://trunk.domain/testing#something</a> - (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", - "<span style='text-align:center; display: block;'><object width='425' height='350'><param name='movie' value='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' /> <param name='allowfullscreen' value='true' /> <param name='wmode' value='opaque' /> <embed src='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' type='application/x-shockwave-flash' allowfullscreen='true' width='425' height='350' wmode='opaque'></embed> </object></span>", - '<a href="http://example.com/example.gif" title="Image from http://example.com">Look at this image!</a>', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } - } + // @ticket 4570 + // Test IRI. + 'IRI in domain' => array( + 'text' => 'http://www.詹姆斯.com/', + 'expected' => '<a href="http://www.詹姆斯.com/" rel="nofollow">http://www.詹姆斯.com/</a>', + ), + 'IRI in path' => array( + 'text' => 'http://bg.wikipedia.org/Баба', + 'expected' => '<a href="http://bg.wikipedia.org/Баба" rel="nofollow">http://bg.wikipedia.org/Баба</a>', + ), + 'IRI in query string' => array( + 'text' => 'http://example.com/?a=баба&b=дядо', + 'expected' => '<a href="http://example.com/?a=баба&b=дядо" rel="nofollow">http://example.com/?a=баба&b=дядо</a>', + ), - /** - * @ticket 23756 - */ - public function test_no_links_inside_pre_or_code() { - $before = array( - '<pre>http://wordpress.org</pre>', - '<code>http://wordpress.org</code>', - '<pre class="foobar" id="foo">http://wordpress.org</pre>', - '<code class="foobar" id="foo">http://wordpress.org</code>', - '<precustomtag>http://wordpress.org</precustomtag>', - '<codecustomtag>http://wordpress.org</codecustomtag>', - 'URL before pre http://wordpress.org<pre>http://wordpress.org</pre>', - 'URL before code http://wordpress.org<code>http://wordpress.org</code>', - 'URL after pre <PRE>http://wordpress.org</PRE>http://wordpress.org', - 'URL after code <code>http://wordpress.org</code>http://wordpress.org', - 'URL before and after pre http://wordpress.org<pre>http://wordpress.org</pre>http://wordpress.org', - 'URL before and after code http://wordpress.org<code>http://wordpress.org</code>http://wordpress.org', - 'code inside pre <pre>http://wordpress.org <code>http://wordpress.org</code> http://wordpress.org</pre>', - ); + // @ticket 10990 + // Test URLS with brackets (within the URL). + 'URL with brackets in path' => array( + 'text' => 'http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)', + 'expected' => '<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>', + ), + '(URL with brackets in path)' => array( + 'text' => '(http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software))', + 'expected' => '(<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>)', + ), + 'URL with brackets in path: word before and after' => array( + 'text' => 'blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software) blah', + 'expected' => 'blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a> blah', + ), + 'URL with brackets in path: trailing ) blah' => array( + 'text' => 'blah (http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah', + 'expected' => 'blah (<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah', + ), + 'URL with brackets in path: within content' => array( + 'text' => 'blah blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software) blah blah', + 'expected' => 'blah blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a> blah blah', + ), + 'URL with brackets in path: trailing ) within content' => array( + 'text' => 'blah blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah blah', + 'expected' => 'blah blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah blah', + ), + '(URL with brackets in path) within content' => array( + 'text' => 'blah blah (http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)) blah blah', + 'expected' => 'blah blah (<a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>) blah blah', + ), + 'URL with brackets in path: trailing .)' => array( + 'text' => 'blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software).) blah blah', + 'expected' => 'blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>.) blah blah', + ), + 'URL with brackets in path: trailing .)moreurl' => array( + 'text' => 'blah blah http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software).)moreurl blah blah', + 'expected' => 'blah blah <a href="http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)" rel="nofollow">http://en.wikipedia.org/wiki/PC_Tools_(Central_Point_Software)</a>.)moreurl blah blah', + ), + 'multiline content with URL with brackets in path' => array( + 'text' => 'In his famous speech “You and Your research” (here: + http://www.cs.virginia.edu/~robins/YouAndYourResearch.html) + Richard Hamming wrote about people getting more done with their doors closed, but', + 'expected' => 'In his famous speech “You and Your research” (here: + <a href="http://www.cs.virginia.edu/~robins/YouAndYourResearch.html" rel="nofollow">http://www.cs.virginia.edu/~robins/YouAndYourResearch.html</a>) + Richard Hamming wrote about people getting more done with their doors closed, but', + ), - $expected = array( - '<pre>http://wordpress.org</pre>', - '<code>http://wordpress.org</code>', - '<pre class="foobar" id="foo">http://wordpress.org</pre>', - '<code class="foobar" id="foo">http://wordpress.org</code>', - '<precustomtag><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a></precustomtag>', - '<codecustomtag><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a></codecustomtag>', - 'URL before pre <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><pre>http://wordpress.org</pre>', - 'URL before code <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><code>http://wordpress.org</code>', - 'URL after pre <PRE>http://wordpress.org</PRE><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', - 'URL after code <code>http://wordpress.org</code><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', - 'URL before and after pre <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><pre>http://wordpress.org</pre><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', - 'URL before and after code <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><code>http://wordpress.org</code><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', - 'code inside pre <pre>http://wordpress.org <code>http://wordpress.org</code> http://wordpress.org</pre>', - ); + // @ticket #62037 + 'URL with brackets in path before the extension' => array( + 'text' => 'http://example-image(2).jpg', + 'expected' => '<a href="http://example-image(2).jpg" rel="nofollow">http://example-image(2).jpg</a>', + ), + 'URL with brackets within path and with a extension' => array( + 'text' => 'http://example-(2)-image.jpg', + 'expected' => '<a href="http://example-(2)-image.jpg" rel="nofollow">http://example-(2)-image.jpg</a>', + ), - foreach ( $before as $key => $url ) { - $this->assertSame( $expected[ $key ], make_clickable( $url ) ); - } - } + // @ticket 11211 + // Test with real comments which were incorrectly linked. + 'real world: example.com text (.org URL)' => array( + 'text' => 'Example: WordPress, test (some text), I love example.com (http://example.org), it is brilliant', + 'expected' => 'Example: WordPress, test (some text), I love example.com (<a href="http://example.org">http://example.org</a>), it is brilliant', + ), + 'real world: example.com text (.com URL)' => array( + 'text' => 'Example: WordPress, test (some text), I love example.com (http://example.com), it is brilliant', + 'expected' => 'Example: WordPress, test (some text), I love example.com (<a href="http://example.com" rel="nofollow">http://example.com</a>), it is brilliant', + ), + 'real world: (URL)...' => array( + 'text' => 'Some text followed by a bracketed link with a trailing ellipsis (http://example.com)...', + 'expected' => 'Some text followed by a bracketed link with a trailing ellipsis (<a href="http://example.com" rel="nofollow">http://example.com</a>)...', + ), + 'real world: (here: URL)' => array( + 'text' => 'In his famous speech “You and Your research” (here: http://www.cs.virginia.edu/~robins/YouAndYourResearch.html) Richard Hamming wrote about people getting more done with their doors closed...', + 'expected' => 'In his famous speech “You and Your research” (here: <a href="http://www.cs.virginia.edu/~robins/YouAndYourResearch.html" rel="nofollow">http://www.cs.virginia.edu/~robins/YouAndYourResearch.html</a>) Richard Hamming wrote about people getting more done with their doors closed...', + ), - /** - * @ticket 16892 - */ - public function test_click_inside_html() { - $urls_before = array( - '<span>http://example.com</span>', - '<p>http://example.com/</p>', - ); - $urls_expected = array( - '<span><a href="http://example.com" rel="nofollow">http://example.com</a></span>', - '<p><a href="http://example.com/" rel="nofollow">http://example.com/</a></p>', - ); - foreach ( $urls_before as $key => $url ) { - $this->assertSame( $urls_expected[ $key ], make_clickable( $url ) ); - } - } + // @ticket 14993 + // Test Twitter hash bang URL. + 'Twitter hash bang URL' => array( + 'text' => 'http://twitter.com/#!/wordpress/status/25907440233', + 'expected' => '<a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a>', + ), + 'Twitter hash bang URL in sentence' => array( + 'text' => 'This is a really good tweet http://twitter.com/#!/wordpress/status/25907440233 !', + 'expected' => 'This is a really good tweet <a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a> !', + ), + 'Twitter hash bang in sentence with trailing !' => array( + 'text' => 'This is a really good tweet http://twitter.com/#!/wordpress/status/25907440233!', + 'expected' => 'This is a really good tweet <a href="http://twitter.com/#!/wordpress/status/25907440233" rel="nofollow">http://twitter.com/#!/wordpress/status/25907440233</a>!', + ), + + // Test URLs wrapped in angled brackets, i.e. < >. + '<URL>' => array( + 'text' => 'URL wrapped in angle brackets <http://example.com/>', + 'expected' => 'URL wrapped in angle brackets <<a href="http://example.com/" rel="nofollow">http://example.com/</a>>', + ), + '< URL >' => array( + 'text' => 'URL wrapped in angle brackets with padding < http://example.com/ >', + 'expected' => 'URL wrapped in angle brackets with padding < <a href="http://example.com/" rel="nofollow">http://example.com/</a> >', + ), + '<email>' => array( + 'text' => 'mailto wrapped in angle brackets <foo@example.com>', + 'expected' => 'mailto wrapped in angle brackets <foo@example.com>', + ), + + // Test URLs preceded by punctuation. + ',URL' => array( + 'text' => 'Comma then URL,http://example.com/', + 'expected' => 'Comma then URL,<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + '.URL' => array( + 'text' => 'Period then URL.http://example.com/', + 'expected' => 'Period then URL.<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + ';URL' => array( + 'text' => 'Semi-colon then URL;http://example.com/', + 'expected' => 'Semi-colon then URL;<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + ':URL' => array( + 'text' => 'Colon then URL:http://example.com/', + 'expected' => 'Colon then URL:<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + '!URL' => array( + 'text' => 'Exclamation mark then URL!http://example.com/', + 'expected' => 'Exclamation mark then URL!<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + '?URL' => array( + 'text' => 'Question mark then URL?http://example.com/', + 'expected' => 'Question mark then URL?<a href="http://example.com/" rel="nofollow">http://example.com/</a>', + ), + + // Test it doesn't break tag attributes. + '<img src=URL with attributes>' => array( + 'text' => "<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>", + 'expected' => "<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>", + ), + '(<img src=URL with attributes>)' => array( + 'text' => "(<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + 'expected' => "(<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + ), + 'URL (<img src=URL with attributes>)' => array( + 'text' => "http://trunk.domain/testing#something (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + 'expected' => "<a href=\"http://trunk.domain/testing#something\" rel=\"nofollow\">http://trunk.domain/testing#something</a> (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + ), + 'multiline URL (<img src=URL with attributes>)' => array( + 'text' => "http://trunk.domain/testing#something + (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + 'expected' => "<a href=\"http://trunk.domain/testing#something\" rel=\"nofollow\">http://trunk.domain/testing#something</a> + (<img src='http://trunk.domain/wp-includes/images/smilies/icon_smile.gif' alt=':)' class='wp-smiley'>)", + ), + '<param value=URL><embed src=URL>' => array( + 'text' => "<span style='text-align:center; display: block;'><object width='425' height='350'><param name='movie' value='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' /> <param name='allowfullscreen' value='true' /> <param name='wmode' value='opaque' /> <embed src='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' type='application/x-shockwave-flash' allowfullscreen='true' width='425' height='350' wmode='opaque'></embed> </object></span>", + 'expected' => "<span style='text-align:center; display: block;'><object width='425' height='350'><param name='movie' value='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' /> <param name='allowfullscreen' value='true' /> <param name='wmode' value='opaque' /> <embed src='https://www.youtube.com/watch?v=72xdCU__XCk&rel=1&fs=1&showsearch=0&showinfo=1&iv_load_policy=1' type='application/x-shockwave-flash' allowfullscreen='true' width='425' height='350' wmode='opaque'></embed> </object></span>", + ), + '<a src=URL title=URL></a>' => array( + 'text' => '<a href="http://example.com/example.gif" title="Image from http://example.com">Look at this image!</a>', + 'expected' => '<a href="http://example.com/example.gif" title="Image from http://example.com">Look at this image!</a>', + ), - public function test_no_links_within_links() { - $in = array( - 'Some text with a link <a href="http://example.com">http://example.com</a>', - // '<a href="http://wordpress.org">This is already a link www.wordpress.org</a>', // Fails in 3.3.1 too. + // Test doesn't add links within <pre> or <code> elements. + 'Does not add link within <pre>' => array( + 'text' => '<pre>http://wordpress.org</pre>', + 'expected' => '<pre>http://wordpress.org</pre>', + ), + 'Does not add link within <code>' => array( + 'text' => '<code>http://wordpress.org</code>', + 'expected' => '<code>http://wordpress.org</code>', + ), + 'Does not add link within <pre with attributes>' => array( + 'text' => '<pre class="foobar" id="foo">http://wordpress.org</pre>', + 'expected' => '<pre class="foobar" id="foo">http://wordpress.org</pre>', + ), + 'Does not add link within <code with attributes>' => array( + 'text' => '<code class="foobar" id="foo">http://wordpress.org</code>', + 'expected' => '<code class="foobar" id="foo">http://wordpress.org</code>', + ), + 'Adds link within <precustomtag>' => array( + 'text' => '<precustomtag>http://wordpress.org</precustomtag>', + 'expected' => '<precustomtag><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a></precustomtag>', + ), + 'Adds link within <codecustomtag>' => array( + 'text' => '<codecustomtag>http://wordpress.org</codecustomtag>', + 'expected' => '<codecustomtag><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a></codecustomtag>', + ), + 'Adds link to URL before <pre>, but does not add link within <pre>' => array( + 'text' => 'URL before pre http://wordpress.org<pre>http://wordpress.org</pre>', + 'expected' => 'URL before pre <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><pre>http://wordpress.org</pre>', + ), + 'Adds link to URL before <code>, but does not add link within <code>' => array( + 'text' => 'URL before code http://wordpress.org<code>http://wordpress.org</code>', + 'expected' => 'URL before code <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><code>http://wordpress.org</code>', + ), + 'Does not add link to <PRE>, but does add link to URL after <PRE>' => array( + 'text' => 'URL after pre <PRE>http://wordpress.org</PRE>http://wordpress.org', + 'expected' => 'URL after pre <PRE>http://wordpress.org</PRE><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', + ), + 'Does not add link within <code>, but does add link to URL after <code>' => array( + 'text' => 'URL after code <code>http://wordpress.org</code>http://wordpress.org', + 'expected' => 'URL after code <code>http://wordpress.org</code><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', + ), + 'Adds link to before and after URLs, but does not add link within <pre>' => array( + 'text' => 'URL before and after pre http://wordpress.org<pre>http://wordpress.org</pre>http://wordpress.org', + 'expected' => 'URL before and after pre <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><pre>http://wordpress.org</pre><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', + ), + 'Adds link to before and after URLs, but does not add link within <code>' => array( + 'text' => 'URL before and after code http://wordpress.org<code>http://wordpress.org</code>http://wordpress.org', + 'expected' => 'URL before and after code <a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a><code>http://wordpress.org</code><a href="http://wordpress.org" rel="nofollow">http://wordpress.org</a>', + ), + 'Does not add links within nested <pre>URL <code>URL</code> </pre>' => array( + 'text' => 'code inside pre <pre>http://wordpress.org <code>http://wordpress.org</code> http://wordpress.org</pre>', + 'expected' => 'code inside pre <pre>http://wordpress.org <code>http://wordpress.org</code> http://wordpress.org</pre>', + ), + + // @ticket 16892 + // Test adds link inside of HTML elements. + '<span>URL</span>' => array( + 'text' => '<span>http://example.com</span>', + 'expected' => '<span><a href="http://example.com" rel="nofollow">http://example.com</a></span>', + ), + '<p>URL</p>' => array( + 'text' => '<p>http://example.com/</p>', + 'expected' => '<p><a href="http://example.com/" rel="nofollow">http://example.com/</a></p>', + ), + + // Test does not add links within the <a> element. + '<a>URL</a>' => array( + 'text' => 'Some text with a link <a href="http://example.com">http://example.com</a>', + 'expected' => 'Some text with a link <a href="http://example.com">http://example.com</a>', + ), + /* + Fails in 3.3.1 too. + '<a>text www.URL</a>' => array( + 'text' => '<a href="http://wordpress.org">This is already a link www.wordpress.org</a>', + 'expected' => '<a href="http://wordpress.org">This is already a link www.wordpress.org</a>', + ), + */ ); - foreach ( $in as $text ) { - $this->assertSame( $text, make_clickable( $text ) ); - } } /** @@ -419,6 +482,7 @@ public function data_script_and_style_tags() { /** * @ticket 48022 + * @ticket 56444 * @dataProvider data_add_rel_ugc_in_comments */ public function test_add_rel_ugc_in_comments( $content, $expected ) { @@ -436,7 +500,11 @@ public function test_add_rel_ugc_in_comments( $content, $expected ) { } public function data_add_rel_ugc_in_comments() { + $home_url_http = set_url_scheme( home_url(), 'http' ); + $home_url_https = set_url_scheme( home_url(), 'https' ); + return array( + // @ticket 48022 array( 'http://wordpress.org', '<a href="http://wordpress.org" rel="nofollow ugc">http://wordpress.org</a>', @@ -445,7 +513,19 @@ public function data_add_rel_ugc_in_comments() { 'www.wordpress.org', '<p><a href="http://www.wordpress.org" rel="nofollow ugc">http://www.wordpress.org</a>', ), + // @ticket 56444 + array( + 'www.example.org', + '<p><a href="http://www.example.org" rel="nofollow ugc">http://www.example.org</a>', + ), + array( + $home_url_http, + '<a href="' . $home_url_http . '" rel="ugc">' . $home_url_http . '</a>', + ), + array( + $home_url_https, + '<a href="' . $home_url_https . '" rel="ugc">' . $home_url_https . '</a>', + ), ); } - } diff --git a/tests/phpunit/tests/formatting/mapDeep.php b/tests/phpunit/tests/formatting/mapDeep.php index 0a291c97697a9..0d46ea852377f 100644 --- a/tests/phpunit/tests/formatting/mapDeep.php +++ b/tests/phpunit/tests/formatting/mapDeep.php @@ -3,6 +3,8 @@ /** * @group formatting * @ticket 22300 + * + * @covers ::map_deep */ class Tests_Formatting_MapDeep extends WP_UnitTestCase { @@ -47,7 +49,7 @@ public function test_map_deep_should_map_each_element_of_array_two_levels_deep() } public function test_map_deep_should_map_each_object_element_of_an_array() { - $this->assertEquals( + $this->assertEqualSets( array( 'var0' => 'ababa', 'var1' => (object) array( @@ -169,5 +171,4 @@ public function test_map_deep_should_map_array_elements_passed_by_reference() { public function append_baba( $value ) { return $value . 'baba'; } - } diff --git a/tests/phpunit/tests/formatting/maybeHashHexColor.php b/tests/phpunit/tests/formatting/maybeHashHexColor.php new file mode 100644 index 0000000000000..88ed38e8e9bc9 --- /dev/null +++ b/tests/phpunit/tests/formatting/maybeHashHexColor.php @@ -0,0 +1,174 @@ +<?php + +/** + * Tests for the sanitize_hex_color function. + * + * @group formatting + * + * @covers ::maybe_hash_hex_color + */ +class Tests_Formatting_MaybeHashHexColor extends WP_UnitTestCase { + + /** + * @ticket 60272 + * + * @dataProvider data_sanitize_hex_color_no_hash + * + * @param string $color Color. + * @param string $expected Expected. + */ + public function test_maybe_hash_hex_color( $color, $expected ) { + $this->assertSame( $expected, maybe_hash_hex_color( $color ) ); + } + + /** + * Data provider for test_maybe_hash_hex_color(). + * + * @return array[] + */ + public function data_sanitize_hex_color_no_hash() { + return array( + '$maybe_alpha = false, 3 digit' => array( + 'color' => '#123', + 'expected' => '#123', + ), + '$maybe_alpha = false, 3 letter' => array( + 'color' => '#abc', + 'expected' => '#abc', + ), + '$maybe_alpha = false, 3 mixed' => array( + 'color' => '#0ab', + 'expected' => '#0ab', + ), + '$maybe_alpha = false, 6 digit' => array( + 'color' => '#123456', + 'expected' => '#123456', + ), + '$maybe_alpha = false, 6 letter' => array( + 'color' => '#abcdef', + 'expected' => '#abcdef', + ), + '$maybe_alpha = false, 6 mixed' => array( + 'color' => '#abc123', + 'expected' => '#abc123', + ), + 'empty string' => array( + 'color' => '', + 'expected' => '', + ), + 'just #' => array( + 'color' => '#', + 'expected' => '#', + ), + 'no hash' => array( + 'color' => '123', + 'expected' => '#123', + ), + 'not a-f' => array( + 'color' => '#hjg', + 'expected' => '#hjg', + ), + 'not upper A-F' => array( + 'color' => '#HJG', + 'expected' => '#HJG', + ), + '$maybe_alpha = false, 3 digit with 1 alpha' => array( + 'color' => '#123f', + 'expected' => '#123f', + ), + '$maybe_alpha = false, 3 letter with 1 alpha' => array( + 'color' => '#abcf', + 'expected' => '#abcf', + ), + '$maybe_alpha = false, 3 mixed with 1 alpha' => array( + 'color' => '#0abf', + 'expected' => '#0abf', + ), + '$maybe_alpha = false, 6 digit with 2 alpha' => array( + 'color' => '#123456ff', + 'expected' => '#123456ff', + ), + '$maybe_alpha = false, 6 letter with 2 alpha' => array( + 'color' => '#abcdefff', + 'expected' => '#abcdefff', + ), + '$maybe_alpha = false, 6 mixed with 2 alpha' => array( + 'color' => '#abc123ff', + 'expected' => '#abc123ff', + ), + // Happy. + '$maybe_alpha = true, 3 digit' => array( + 'color' => '#123', + 'expected' => '#123', + ), + '$maybe_alpha = true, 3 letter' => array( + 'color' => '#abc', + 'expected' => '#abc', + ), + '$maybe_alpha = true, 3 mixed' => array( + 'color' => '0ab', + 'expected' => '#0ab', + ), + '$maybe_alpha = true, 6 digit' => array( + 'color' => '123456', + 'expected' => '#123456', + ), + '$maybe_alpha = true, 6 letter' => array( + 'color' => 'abcdef', + 'expected' => '#abcdef', + ), + '$maybe_alpha = true, 6 mixed' => array( + 'color' => 'abc123', + 'expected' => '#abc123', + ), + '$maybe_alpha = true, 3 digit with 1 alpha' => array( + 'color' => '123f', + 'expected' => '123f', + ), + '$maybe_alpha = true, 3 letter with 1 alpha' => array( + 'color' => 'abcf', + 'expected' => 'abcf', + ), + '$maybe_alpha = true, 3 mixed with 1 alpha' => array( + 'color' => '0abf', + 'expected' => '0abf', + ), + '$maybe_alpha = true, 6 digit with 2 alpha' => array( + 'color' => '123456ff', + 'expected' => '123456ff', + ), + '$maybe_alpha = true, 6 letter with 2 alpha' => array( + 'color' => 'abcdefff', + 'expected' => 'abcdefff', + ), + '$maybe_alpha = true, 6 mixed with 2 alpha' => array( + 'color' => 'abc123ff', + 'expected' => 'abc123ff', + ), + '$maybe_alpha = true, 3 digit with 2 alpha' => array( + 'color' => '123ff', + 'expected' => '123ff', + ), + '$maybe_alpha = true, 3 letter with 2 alpha' => array( + 'color' => 'abcff', + 'expected' => 'abcff', + ), + '$maybe_alpha = true, 3 mixed with 2 alpha' => array( + 'color' => '0abff', + 'expected' => '0abff', + ), + '$maybe_alpha = true, 6 digit with 1 alpha' => array( + 'color' => '123456f', + 'expected' => '123456f', + ), + '$maybe_alpha = true, 6 letter with 1 alpha' => array( + 'color' => 'abcff', + 'expected' => 'abcff', + ), + '$maybe_alpha = true, 6 mixed with 1 alpha' => array( + 'color' => '0abff', + 'expected' => '0abff', + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/normalizeWhitespace.php b/tests/phpunit/tests/formatting/normalizeWhitespace.php index 2e7668698a2b8..b14647b32e6c0 100644 --- a/tests/phpunit/tests/formatting/normalizeWhitespace.php +++ b/tests/phpunit/tests/formatting/normalizeWhitespace.php @@ -1,14 +1,31 @@ <?php /** * @group formatting + * + * @covers ::normalize_whitespace */ class Tests_Formatting_NormalizeWhitespace extends WP_UnitTestCase { + + /** + * Tests the the normalize_whitespace() function. + * + * @dataProvider data_normalize_whitespace + */ + public function test_normalize_whitespace( $input, $expected ) { + $this->assertSame( $expected, normalize_whitespace( $input ) ); + } + /** - * WhitespaceTest Content DataProvider + * Data provider. * - * array( input_txt, converted_output_txt) + * @return array { + * @type array { + * @type string $input Input content. + * @type string $expected Expected output. + * } + * } */ - public function get_input_output() { + public function data_normalize_whitespace() { return array( array( ' ', @@ -40,13 +57,4 @@ public function get_input_output() { ), ); } - - /** - * Validate the normalize_whitespace function - * - * @dataProvider get_input_output - */ - public function test_normalize_whitespace( $in_str, $exp_str ) { - $this->assertSame( $exp_str, normalize_whitespace( $in_str ) ); - } } diff --git a/tests/phpunit/tests/formatting/redirect.php b/tests/phpunit/tests/formatting/redirect.php index e1004493bdf3e..c4b1d682094f4 100644 --- a/tests/phpunit/tests/formatting/redirect.php +++ b/tests/phpunit/tests/formatting/redirect.php @@ -18,7 +18,9 @@ public function home_url() { /** * @ticket 44317 * - * @dataProvider get_bad_status_codes + * @dataProvider data_wp_redirect_bad_status_code + * + * @covers ::wp_redirect * * @param string $location The path or URL to redirect to. * @param int $status HTTP response status code to use. @@ -29,7 +31,7 @@ public function test_wp_redirect_bad_status_code( $location, $status ) { wp_redirect( $location, $status ); } - public function get_bad_status_codes() { + public function data_wp_redirect_bad_status_code() { return array( // Tests for bad arguments. array( '/wp-admin', 404 ), @@ -41,6 +43,9 @@ public function get_bad_status_codes() { ); } + /** + * @covers ::wp_sanitize_redirect + */ public function test_wp_sanitize_redirect() { $this->assertSame( 'http://example.com/watchthelinefeedgo', wp_sanitize_redirect( 'http://example.com/watchthelinefeed%0Ago' ) ); $this->assertSame( 'http://example.com/watchthelinefeedgo', wp_sanitize_redirect( 'http://example.com/watchthelinefeed%0ago' ) ); @@ -59,6 +64,8 @@ public function test_wp_sanitize_redirect() { /** * @ticket 36998 + * + * @covers ::wp_sanitize_redirect */ public function test_wp_sanitize_redirect_should_encode_spaces() { $this->assertSame( 'http://example.com/test%20spaces', wp_sanitize_redirect( 'http://example.com/test%20spaces' ) ); @@ -66,20 +73,18 @@ public function test_wp_sanitize_redirect_should_encode_spaces() { } /** - * @dataProvider valid_url_provider + * @dataProvider data_wp_validate_redirect_valid_url + * + * @covers ::wp_validate_redirect + * + * @param string $url Redirect requested. + * @param string $expected Expected destination. */ public function test_wp_validate_redirect_valid_url( $url, $expected ) { $this->assertSame( $expected, wp_validate_redirect( $url ) ); } - /** - * @dataProvider invalid_url_provider - */ - public function test_wp_validate_redirect_invalid_url( $url ) { - $this->assertEquals( false, wp_validate_redirect( $url, false ) ); - } - - public function valid_url_provider() { + public function data_wp_validate_redirect_valid_url() { return array( array( 'http://example.com', 'http://example.com' ), array( 'http://example.com/', 'http://example.com/' ), @@ -95,10 +100,22 @@ public function valid_url_provider() { ); } - public function invalid_url_provider() { + /** + * @dataProvider data_wp_validate_redirect_invalid_url + * + * @covers ::wp_validate_redirect + * + * @param string $url Redirect requested. + * @param string|false $expected Optional. Expected destination. Default false. + */ + public function test_wp_validate_redirect_invalid_url( $url, $expected = false ) { + $this->assertSame( $expected, wp_validate_redirect( $url, false ) ); + } + + public function data_wp_validate_redirect_invalid_url() { return array( // parse_url() fails. - array( '' ), + array( '', '' ), array( 'http://:' ), // Non-safelisted domain. @@ -165,7 +182,13 @@ public function invalid_url_provider() { /** * @ticket 47980 - * @dataProvider relative_url_provider + * @dataProvider data_wp_validate_redirect_relative_url + * + * @covers ::wp_validate_redirect + * + * @param string $current_uri Current URI (i.e. path and query string only). + * @param string $url Redirect requested. + * @param string $expected Expected destination. */ public function test_wp_validate_redirect_relative_url( $current_uri, $url, $expected ) { // Backup the global. @@ -190,7 +213,7 @@ public function test_wp_validate_redirect_relative_url( $current_uri, $url, $exp } /** - * Data provider for test_wp_validate_redirect_relative_url. + * Data provider for test_wp_validate_redirect_relative_url(). * * @return array[] { * string Current URI (i.e. path and query string only). @@ -198,7 +221,7 @@ public function test_wp_validate_redirect_relative_url( $current_uri, $url, $exp * string Expected destination. * } */ - public function relative_url_provider() { + public function data_wp_validate_redirect_relative_url() { return array( array( '/', diff --git a/tests/phpunit/tests/formatting/removeAccents.php b/tests/phpunit/tests/formatting/removeAccents.php index 19fd50586bd51..184cd9d1d5dd3 100644 --- a/tests/phpunit/tests/formatting/removeAccents.php +++ b/tests/phpunit/tests/formatting/removeAccents.php @@ -2,12 +2,33 @@ /** * @group formatting + * + * @covers ::remove_accents */ class Tests_Formatting_RemoveAccents extends WP_UnitTestCase { + public function test_remove_accents_simple() { $this->assertSame( 'abcdefghijkl', remove_accents( 'abcdefghijkl' ) ); } + /** + * @ticket 24661 + * + * Tests Unicode sequence normalization from NFD (Normalization Form Decomposed) + * to NFC (Normalization Form [Pre]Composed), the encoding used in `remove_accents()`. + * + * For more information on Unicode normalization, see + * https://unicode.org/faq/normalization.html. + * + * @requires extension intl + */ + public function test_remove_accents_latin1_supplement_nfd_encoding() { + $input = 'ªºÀÁÂÃÄÅÆÇÈÉÊËÌÍÎÏÐÑÒÓÔÕÖØÙÚÛÜÝÞßàáâãäåæçèéêëìíîïðñòóôõöøùúûüýþÿ'; + $output = 'aoAAAAAAAECEEEEIIIIDNOOOOOOUUUUYTHsaaaaaaaeceeeeiiiidnoooooouuuuythy'; + + $this->assertSame( $output, remove_accents( $input ), 'remove_accents replaces Latin-1 Supplement with NFD encoding' ); + } + /** * @ticket 9591 */ @@ -80,67 +101,43 @@ public function test_remove_accents_hanyu_pinyin() { $this->assertSame( 'aaeiouuAEIOUU', remove_accents( 'aɑeiouüAEIOUÜ' ) ); } - public function remove_accents_germanic_umlauts_cb() { - return 'de_DE'; - } - /** * @ticket 3782 */ public function test_remove_accents_germanic_umlauts() { - add_filter( 'locale', array( $this, 'remove_accents_germanic_umlauts_cb' ) ); - - $this->assertSame( 'AeOeUeaeoeuess', remove_accents( 'ÄÖÜäöüß' ) ); - - remove_filter( 'locale', array( $this, 'remove_accents_germanic_umlauts_cb' ) ); + $this->assertSame( 'AeOeUeaeoeuess', remove_accents( 'ÄÖÜäöüß', 'de_DE' ) ); } - public function set_locale_to_danish() { - return 'da_DK'; + /** + * @ticket 64821 + */ + public function test_remove_accents_germanic_capital_eszett() { + // U+1E9E LATIN CAPITAL LETTER SHARP S, standardized in German orthography in 2017 (DIN 5008). + $this->assertSame( 'SS', remove_accents( 'ẞ', 'de_DE' ) ); + // Verify it works in context alongside the lowercase variant. + $this->assertSame( 'SSstrasse', remove_accents( 'ẞstraße', 'de_DE' ) ); } /** * @ticket 23907 */ public function test_remove_danish_accents() { - add_filter( 'locale', array( $this, 'set_locale_to_danish' ) ); - - $this->assertSame( 'AeOeAaaeoeaa', remove_accents( 'ÆØÅæøå' ) ); - - remove_filter( 'locale', array( $this, 'set_locale_to_danish' ) ); - } - - public function set_locale_to_catalan() { - return 'ca'; + $this->assertSame( 'AeOeAaaeoeaa', remove_accents( 'ÆØÅæøå', 'da_DK' ) ); } /** * @ticket 37086 */ public function test_remove_catalan_middot() { - add_filter( 'locale', array( $this, 'set_locale_to_catalan' ) ); - - $this->assertSame( 'allallalla', remove_accents( 'al·lallaŀla' ) ); - - remove_filter( 'locale', array( $this, 'set_locale_to_catalan' ) ); - + $this->assertSame( 'allallalla', remove_accents( 'al·lallaŀla', 'ca' ) ); $this->assertSame( 'al·lallalla', remove_accents( 'al·lallaŀla' ) ); } - public function set_locale_to_serbian() { - return 'sr_RS'; - } - /** * @ticket 38078 */ public function test_transcribe_serbian_crossed_d() { - add_filter( 'locale', array( $this, 'set_locale_to_serbian' ) ); - - $this->assertSame( 'DJdj', remove_accents( 'Đđ' ) ); - - remove_filter( 'locale', array( $this, 'set_locale_to_serbian' ) ); - + $this->assertSame( 'DJdj', remove_accents( 'Đđ', 'sr_RS' ) ); $this->assertSame( 'Dd', remove_accents( 'Đđ' ) ); } } diff --git a/tests/phpunit/tests/formatting/sanitizeEmail.php b/tests/phpunit/tests/formatting/sanitizeEmail.php new file mode 100644 index 0000000000000..5490374d0a5e7 --- /dev/null +++ b/tests/phpunit/tests/formatting/sanitizeEmail.php @@ -0,0 +1,105 @@ +<?php +/** + * Tests for the sanitize_email() function. + * + * @group formatting + * @covers ::sanitize_email + */ +class Tests_Formatting_SanitizeEmail extends WP_UnitTestCase { + /** + * This test checks that email addresses are properly sanitized. + * + * @ticket 31992 + * + * @dataProvider data_sanitized_email_pairs + * + * @param string $address The email address to sanitize. + * @param string $expected The expected sanitized email address. + */ + public function test_returns_stripped_email_address( $address, $expected ) { + $sanitized = sanitize_email( $address ); + + if ( $expected === $sanitized ) { + $this->assertSame( + $expected, + $sanitized, + 'Should have produced the known sanitized form of the email.' + ); + } else { + $this->assertSame( + $expected, + self::invalid_utf8_as_ascii( $sanitized ), + 'Should have produced the known sanitized form of the email.' + ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_sanitized_email_pairs() { + return array( + 'shorter than 6 characters' => array( 'a@b', '' ), + 'contains no @' => array( 'ab', '' ), + 'just a TLD' => array( 'abc@com', '' ), + 'plain' => array( 'abc@example.com', 'abc@example.com' ), + 'invalid utf8 subdomain dropped' => array( "abc@sub.\x80.org", 'abc@sub.org' ), + 'all subdomains invalid utf8' => array( "abc@\x80.org", '' ), + ); + } + + /** + * Transforms invalid byte sequences in UTF-8 into representations of + * each byte value, according to the maximal subpart rule. + * + * Example: + * + * // For valid UTF-8 the output is the input. + * 'test' === invalid_utf8_as_ascii( 'test' ); + * + * // Invalid bytes are represented with their hex value. + * 'a(0x80)b' === invalid_utf8_as_ascii( "a\x80b" ); + * + * // Invalid byte sequences form maximal subparts. + * '(0xC2)(0xEF 0xBF)' === invalid_utf8_as_ascii( "\xC2\xEF\xBF" ); + * + * @param string $text + * @return string + */ + private static function invalid_utf8_as_ascii( string $text ): string { + $output = ''; + $at = 0; + $was_at = 0; + $end = strlen( $text ); + $invalid_bytes = 0; + + while ( $at < $end ) { + if ( 0 === _wp_scan_utf8( $text, $at, $invalid_bytes ) && 0 === $invalid_bytes ) { + break; + } + + if ( $at > $was_at ) { + $output .= substr( $text, $was_at, $at - $was_at ); + } + + if ( $invalid_bytes > 0 ) { + $output .= '('; + + for ( $i = 0; $i < $invalid_bytes; $i++ ) { + $space = $i > 0 ? ' ' : ''; + $as_hex = bin2hex( $text[ $at + $i ] ); + $output .= "{$space}0x{$as_hex}"; + } + + $output .= ')'; + } + + $at += $invalid_bytes; + $was_at = $at; + } + + return $output; + } +} diff --git a/tests/phpunit/tests/formatting/sanitizeFileName.php b/tests/phpunit/tests/formatting/sanitizeFileName.php index fbec256219bfa..d0e366f121a5a 100644 --- a/tests/phpunit/tests/formatting/sanitizeFileName.php +++ b/tests/phpunit/tests/formatting/sanitizeFileName.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::sanitize_file_name */ class Tests_Formatting_SanitizeFileName extends WP_UnitTestCase { public function test_munges_extensions() { @@ -33,13 +35,17 @@ public function test_removes_accents() { * Test that spaces are correctly replaced with dashes. * * @ticket 16330 + * @ticket 62995 */ public function test_replaces_spaces() { $urls = array( - 'unencoded space.png' => 'unencoded-space.png', - 'encoded-space.jpg' => 'encoded-space.jpg', - 'plus+space.jpg' => 'plusspace.jpg', - 'multi %20 +space.png' => 'multi-20-space.png', + 'unencoded space.png' => 'unencoded-space.png', + 'encoded-space.jpg' => 'encoded-space.jpg', + 'plus+space.jpg' => 'plusspace.jpg', + 'multi %20 +space.png' => 'multi-20-space.png', + "Screenshot 2025-02-19 at 2.17.33\u{202F}PM.png" => 'Screenshot-2025-02-19-at-2.17.33-PM.png', + "Filename with non-breaking\u{00A0}space.txt" => 'Filename-with-non-breaking-space.txt', + "Filename with thin\u{2009}space.txt" => 'Filename-with-thin-space.txt', ); foreach ( $urls as $test => $expected ) { @@ -93,4 +99,53 @@ public function data_wp_filenames() { array( 'demo' . json_decode( '"\u00a0"' ) . 'bar.png', 'demo-bar.png' ), ); } + + /** + * Tests that sanitize_file_name() replaces consecutive periods + * with a single period. + * + * @ticket 57242 + * + * @dataProvider data_sanitize_file_name_should_replace_consecutive_periods_with_a_single_period + * + * @param string $filename A filename with consecutive periods. + * @param string $expected The expected filename after sanitization. + */ + public function test_sanitize_file_name_should_replace_consecutive_periods_with_a_single_period( $filename, $expected ) { + $this->assertSame( $expected, sanitize_file_name( $filename ) ); + } + + /** + * Data provider for test_sanitize_file_name_should_replace_consecutive_periods_with_a_single_period(). + * + * @return array[] + */ + public function data_sanitize_file_name_should_replace_consecutive_periods_with_a_single_period() { + return array( + 'consecutive periods at the start' => array( + 'filename' => '...filename.png', + 'expected' => 'filename.png', + ), + 'consecutive periods in the middle' => array( + 'filename' => 'file.......name.png', + 'expected' => 'file.name_.png', + ), + 'consecutive periods before the extension' => array( + 'filename' => 'filename....png', + 'expected' => 'filename.png', + ), + 'consecutive periods after the extension' => array( + 'filename' => 'filename.png...', + 'expected' => 'filename.png', + ), + 'consecutive periods at the start, middle, before, after the extension' => array( + 'filename' => '.....file....name...png......', + 'expected' => 'file.name_.png', + ), + 'consecutive periods and no extension' => array( + 'filename' => 'filename...', + 'expected' => 'filename', + ), + ); + } } diff --git a/tests/phpunit/tests/formatting/sanitizeHexColor.php b/tests/phpunit/tests/formatting/sanitizeHexColor.php new file mode 100644 index 0000000000000..77c058be3dc00 --- /dev/null +++ b/tests/phpunit/tests/formatting/sanitizeHexColor.php @@ -0,0 +1,170 @@ +<?php + +/** + * Tests for the sanitize_hex_color() function. + * + * @group formatting + * + * @covers ::sanitize_hex_color + */ +class Tests_Formatting_SanitizeHexColor extends WP_UnitTestCase { + + /** + * @ticket 60270 + * + * @dataProvider data_sanitize_hex_color + * + * @param string $color Color. + * @param string $expected Expected. + */ + public function test_sanitize_hex_color( $color, $expected ) { + $this->assertSame( $expected, sanitize_hex_color( $color ) ); + } + + /** + * Data provider for test_sanitize_hex_color(). + * + * @return array[] + */ + public function data_sanitize_hex_color() { + return array( + '$maybe_alpha = false, 3 digit' => array( + 'color' => '#123', + 'expected' => '#123', + ), + '$maybe_alpha = false, 3 letter' => array( + 'color' => '#abc', + 'expected' => '#abc', + ), + '$maybe_alpha = false, 3 mixed' => array( + 'color' => '#0ab', + 'expected' => '#0ab', + ), + '$maybe_alpha = false, 6 digit' => array( + 'color' => '#123456', + 'expected' => '#123456', + ), + '$maybe_alpha = false, 6 letter' => array( + 'color' => '#abcdef', + 'expected' => '#abcdef', + ), + '$maybe_alpha = false, 6 mixed' => array( + 'color' => '#abc123', + 'expected' => '#abc123', + ), + 'empty string' => array( + 'color' => '', + 'expected' => '', + ), + 'no hash' => array( + 'color' => '123', + 'expected' => null, + ), + 'not a-f' => array( + 'color' => '#hjg', + 'expected' => null, + ), + 'not upper A-F' => array( + 'color' => '#HJG', + 'expected' => null, + ), + '$maybe_alpha = false, 3 digit with 1 alpha' => array( + 'color' => '#123f', + 'expected' => null, + ), + '$maybe_alpha = false, 3 letter with 1 alpha' => array( + 'color' => '#abcf', + 'expected' => null, + ), + '$maybe_alpha = false, 3 mixed with 1 alpha' => array( + 'color' => '#0abf', + 'expected' => null, + ), + '$maybe_alpha = false, 6 digit with 2 alpha' => array( + 'color' => '#123456ff', + 'expected' => null, + ), + '$maybe_alpha = false, 6 letter with 2 alpha' => array( + 'color' => '#abcdefff', + 'expected' => null, + ), + '$maybe_alpha = false, 6 mixed with 2 alpha' => array( + 'color' => '#abc123ff', + 'expected' => null, + ), + // Happy. + '$maybe_alpha = true, 3 digit' => array( + 'color' => '#123', + 'expected' => '#123', + ), + '$maybe_alpha = true, 3 letter' => array( + 'color' => '#abc', + 'expected' => '#abc', + ), + '$maybe_alpha = true, 3 mixed' => array( + 'color' => '#0ab', + 'expected' => '#0ab', + ), + '$maybe_alpha = true, 6 digit' => array( + 'color' => '#123456', + 'expected' => '#123456', + ), + '$maybe_alpha = true, 6 letter' => array( + 'color' => '#abcdef', + 'expected' => '#abcdef', + ), + '$maybe_alpha = true, 6 mixed' => array( + 'color' => '#abc123', + 'expected' => '#abc123', + ), + '$maybe_alpha = true, 3 digit with 1 alpha' => array( + 'color' => '#123f', + 'expected' => null, + ), + '$maybe_alpha = true, 3 letter with 1 alpha' => array( + 'color' => '#abcf', + 'expected' => null, + ), + '$maybe_alpha = true, 3 mixed with 1 alpha' => array( + 'color' => '#0abf', + 'expected' => null, + ), + '$maybe_alpha = true, 6 digit with 2 alpha' => array( + 'color' => '#123456ff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 letter with 2 alpha' => array( + 'color' => '#abcdefff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 mixed with 2 alpha' => array( + 'color' => '#abc123ff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 digit with 2 alpha' => array( + 'color' => '#123ff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 letter with 2 alpha' => array( + 'color' => '#abcff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 mixed with 2 alpha' => array( + 'color' => '#0abff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 digit with 1 alpha' => array( + 'color' => '#123456f', + 'expected' => null, + ), + '$maybe_alpha = true, 6 letter with 1 alpha' => array( + 'color' => '#abcff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 mixed with 1 alpha' => array( + 'color' => '#0abff', + 'expected' => null, + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/sanitizeHexColorNoHash.php b/tests/phpunit/tests/formatting/sanitizeHexColorNoHash.php new file mode 100644 index 0000000000000..4c22006e403f7 --- /dev/null +++ b/tests/phpunit/tests/formatting/sanitizeHexColorNoHash.php @@ -0,0 +1,174 @@ +<?php + +/** + * Tests for the sanitize_hex_color_no_hash() function. + * + * @group formatting + * + * @covers ::sanitize_hex_color_no_hash + */ +class Tests_Formatting_SanitizeHexColorNoHash extends WP_UnitTestCase { + + /** + * @ticket 60271 + * + * @dataProvider data_sanitize_hex_color_no_hash + * + * @param string $color Color. + * @param string $expected Expected. + */ + public function test_sanitize_hex_color_no_hash( $color, $expected ) { + $this->assertSame( $expected, sanitize_hex_color_no_hash( $color ) ); + } + + /** + * Data provider for data_sanitize_hex_color_no_hash(). + * + * @return array[] + */ + public function data_sanitize_hex_color_no_hash() { + return array( + '$maybe_alpha = false, 3 digit' => array( + 'color' => '#123', + 'expected' => '123', + ), + '$maybe_alpha = false, 3 letter' => array( + 'color' => '#abc', + 'expected' => 'abc', + ), + '$maybe_alpha = false, 3 mixed' => array( + 'color' => '#0ab', + 'expected' => '0ab', + ), + '$maybe_alpha = false, 6 digit' => array( + 'color' => '#123456', + 'expected' => '123456', + ), + '$maybe_alpha = false, 6 letter' => array( + 'color' => '#abcdef', + 'expected' => 'abcdef', + ), + '$maybe_alpha = false, 6 mixed' => array( + 'color' => '#abc123', + 'expected' => 'abc123', + ), + 'empty string' => array( + 'color' => '', + 'expected' => '', + ), + 'just #' => array( + 'color' => '#', + 'expected' => '', + ), + 'no hash' => array( + 'color' => '123', + 'expected' => '123', + ), + 'not a-f' => array( + 'color' => '#hjg', + 'expected' => null, + ), + 'not upper A-F' => array( + 'color' => '#HJG', + 'expected' => null, + ), + '$maybe_alpha = false, 3 digit with 1 alpha' => array( + 'color' => '#123f', + 'expected' => null, + ), + '$maybe_alpha = false, 3 letter with 1 alpha' => array( + 'color' => '#abcf', + 'expected' => null, + ), + '$maybe_alpha = false, 3 mixed with 1 alpha' => array( + 'color' => '#0abf', + 'expected' => null, + ), + '$maybe_alpha = false, 6 digit with 2 alpha' => array( + 'color' => '#123456ff', + 'expected' => null, + ), + '$maybe_alpha = false, 6 letter with 2 alpha' => array( + 'color' => '#abcdefff', + 'expected' => null, + ), + '$maybe_alpha = false, 6 mixed with 2 alpha' => array( + 'color' => '#abc123ff', + 'expected' => null, + ), + // Happy. + '$maybe_alpha = true, 3 digit' => array( + 'color' => '#123', + 'expected' => '123', + ), + '$maybe_alpha = true, 3 letter' => array( + 'color' => '#abc', + 'expected' => 'abc', + ), + '$maybe_alpha = true, 3 mixed' => array( + 'color' => '#0ab', + 'expected' => '0ab', + ), + '$maybe_alpha = true, 6 digit' => array( + 'color' => '#123456', + 'expected' => '123456', + ), + '$maybe_alpha = true, 6 letter' => array( + 'color' => '#abcdef', + 'expected' => 'abcdef', + ), + '$maybe_alpha = true, 6 mixed' => array( + 'color' => '#abc123', + 'expected' => 'abc123', + ), + '$maybe_alpha = true, 3 digit with 1 alpha' => array( + 'color' => '#123f', + 'expected' => null, + ), + '$maybe_alpha = true, 3 letter with 1 alpha' => array( + 'color' => '#abcf', + 'expected' => null, + ), + '$maybe_alpha = true, 3 mixed with 1 alpha' => array( + 'color' => '#0abf', + 'expected' => null, + ), + '$maybe_alpha = true, 6 digit with 2 alpha' => array( + 'color' => '#123456ff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 letter with 2 alpha' => array( + 'color' => '#abcdefff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 mixed with 2 alpha' => array( + 'color' => '#abc123ff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 digit with 2 alpha' => array( + 'color' => '#123ff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 letter with 2 alpha' => array( + 'color' => '#abcff', + 'expected' => null, + ), + '$maybe_alpha = true, 3 mixed with 2 alpha' => array( + 'color' => '#0abff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 digit with 1 alpha' => array( + 'color' => '#123456f', + 'expected' => null, + ), + '$maybe_alpha = true, 6 letter with 1 alpha' => array( + 'color' => '#abcff', + 'expected' => null, + ), + '$maybe_alpha = true, 6 mixed with 1 alpha' => array( + 'color' => '#0abff', + 'expected' => null, + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/sanitizeKey.php b/tests/phpunit/tests/formatting/sanitizeKey.php new file mode 100644 index 0000000000000..e39dd328db569 --- /dev/null +++ b/tests/phpunit/tests/formatting/sanitizeKey.php @@ -0,0 +1,141 @@ +<?php + +/** + * @group formatting + * + * @covers ::sanitize_key + */ +class Tests_Formatting_SanitizeKey extends WP_UnitTestCase { + + /** + * @ticket 54160 + * @dataProvider data_sanitize_key + * + * @param string $key The key to sanitize. + * @param string $expected The expected value. + */ + public function test_sanitize_key( $key, $expected ) { + $this->assertSame( $expected, sanitize_key( $key ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_key() { + return array( + 'an empty string key' => array( + 'key' => '', + 'expected' => '', + ), + 'a lowercase key with commas' => array( + 'key' => 'howdy,admin', + 'expected' => 'howdyadmin', + ), + 'a lowercase key with commas' => array( + 'key' => 'HOWDY,ADMIN', + 'expected' => 'howdyadmin', + ), + 'a mixed case key with commas' => array( + 'key' => 'HoWdY,aDmIn', + 'expected' => 'howdyadmin', + ), + 'a key with dashes' => array( + 'key' => 'howdy-admin', + 'expected' => 'howdy-admin', + ), + 'a key with spaces' => array( + 'key' => 'howdy admin', + 'expected' => 'howdyadmin', + ), + 'a key with a HTML entity' => array( + 'key' => 'howdy admin', + 'expected' => 'howdynbspadmin', + ), + 'a key with a unicode character' => array( + 'key' => 'howdy' . chr( 140 ) . 'admin', + 'expected' => 'howdyadmin', + ), + ); + } + + /** + * @ticket 54160 + * @dataProvider data_sanitize_key_nonstring_scalar + * + * @param mixed $key The key to sanitize. + * @param string $expected The expected value. + */ + public function test_sanitize_key_nonstring_scalar( $key, $expected ) { + $this->assertSame( $expected, sanitize_key( $key ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_key_nonstring_scalar() { + return array( + 'integer type' => array( + 'key' => 0, + 'expected' => '0', + ), + 'boolean true' => array( + 'key' => true, + 'expected' => '1', + ), + 'boolean false' => array( + 'key' => false, + 'expected' => '', + ), + 'float type' => array( + 'key' => 0.123, + 'expected' => '0123', + ), + ); + } + + /** + * @ticket 54160 + * @dataProvider data_sanitize_key_with_non_scalars + * + * @param mixed $nonscalar_key A non-scalar data type given as a key. + */ + public function test_sanitize_key_with_non_scalars( $nonscalar_key ) { + add_filter( + 'sanitize_key', + function ( $sanitized_key, $key ) use ( $nonscalar_key ) { + $this->assertEmpty( $sanitized_key, 'Empty string not passed as first filtered argument' ); + $this->assertSame( $nonscalar_key, $key, 'Given unsanitized key not passed as second filtered argument' ); + return $sanitized_key; + }, + 10, + 2 + ); + $this->assertEmpty( sanitize_key( $nonscalar_key ), 'Non-scalar key did not return empty string' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_key_with_non_scalars() { + return array( + 'array type' => array( + 'key' => array( 'key' ), + 'expected' => '', + ), + 'null' => array( + 'key' => null, + 'expected' => '', + ), + 'object' => array( + 'key' => new stdClass(), + 'expected' => '', + ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/sanitizeLocaleName.php b/tests/phpunit/tests/formatting/sanitizeLocaleName.php new file mode 100644 index 0000000000000..cd22acbf2c60a --- /dev/null +++ b/tests/phpunit/tests/formatting/sanitizeLocaleName.php @@ -0,0 +1,49 @@ +<?php + +/** + * @group formatting + * + * @covers ::sanitize_locale_name + */ +class Tests_Formatting_SanitizeLocaleName extends WP_UnitTestCase { + /** + * @dataProvider data_sanitize_locale_name_returns_non_empty_string + */ + public function test_sanitize_locale_name_returns_non_empty_string( $expected, $input ) { + $this->assertSame( $expected, sanitize_locale_name( $input ) ); + } + + public function data_sanitize_locale_name_returns_non_empty_string() { + return array( + // array( expected, input ) + array( 'en_US', 'en_US' ), + array( 'en', 'en' ), + array( 'fr_FR', 'fr_FR' ), + array( 'fr_FR', 'fr_FR' ), + array( 'fr_FR-e2791ba830489d23043be8650a22a22b', 'fr_FR-e2791ba830489d23043be8650a22a22b' ), + array( '-fr_FRmo', '-fr_FR.mo' ), + array( '12324', '$12324' ), + array( '4124FRRa', '/4124$$$%%FRRa' ), + array( 'FR', '<FR' ), + array( 'FR_FR', 'FR_FR' ), + array( '--__', '--__' ), + ); + } + + /** + * @dataProvider data_sanitize_locale_name_returns_empty_string + */ + public function test_sanitize_locale_name_returns_empty_string( $input ) { + $this->assertSame( '', sanitize_locale_name( $input ) ); + } + + public function data_sanitize_locale_name_returns_empty_string() { + return array( + // array( input ) + array( '$<>' ), + array( '/$$$%%\\)' ), + array( '....' ), + array( '@///' ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/sanitizeMimeType.php b/tests/phpunit/tests/formatting/sanitizeMimeType.php index 77c8b472056fa..6d486159a6bcd 100644 --- a/tests/phpunit/tests/formatting/sanitizeMimeType.php +++ b/tests/phpunit/tests/formatting/sanitizeMimeType.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::sanitize_mime_type */ class Tests_Formatting_SanitizeMimeType extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/formatting/sanitizeOrderby.php b/tests/phpunit/tests/formatting/sanitizeOrderby.php index cede00788686c..fec1d417134e5 100644 --- a/tests/phpunit/tests/formatting/sanitizeOrderby.php +++ b/tests/phpunit/tests/formatting/sanitizeOrderby.php @@ -2,17 +2,18 @@ /** * @group sanitize_sql_orderby + * + * @covers ::sanitize_sql_orderby */ class Tests_Formatting_SanitizeOrderby extends WP_UnitTestCase { /** - * @covers ::sanitize_sql_orderby - * @dataProvider valid_orderbys + * @dataProvider data_sanitize_sql_orderby_valid */ - public function test_valid( $orderby ) { + public function test_sanitize_sql_orderby_valid( $orderby ) { $this->assertSame( $orderby, sanitize_sql_orderby( $orderby ) ); } - public function valid_orderbys() { + public function data_sanitize_sql_orderby_valid() { return array( array( '1' ), array( '1 ASC' ), @@ -33,13 +34,12 @@ public function valid_orderbys() { } /** - * @covers ::sanitize_sql_orderby - * @dataProvider invalid_orderbys + * @dataProvider data_sanitize_sql_orderby_invalid */ - public function test_invalid( $orderby ) { + public function test_sanitize_sql_orderby_invalid( $orderby ) { $this->assertFalse( sanitize_sql_orderby( $orderby ) ); } - public function invalid_orderbys() { + public function data_sanitize_sql_orderby_invalid() { return array( array( '' ), array( '1 2' ), diff --git a/tests/phpunit/tests/formatting/sanitizePost.php b/tests/phpunit/tests/formatting/sanitizePost.php index 1e69e703c1694..0366cf77c78d4 100644 --- a/tests/phpunit/tests/formatting/sanitizePost.php +++ b/tests/phpunit/tests/formatting/sanitizePost.php @@ -2,6 +2,9 @@ /** * @group formatting * @group post + * + * @covers ::sanitize_post + * @covers WP_Post::__construct */ class Tests_Formatting_SanitizePost extends WP_UnitTestCase { @@ -26,6 +29,9 @@ public function test_int_fields() { case 'string': $this->assertIsString( $post->$field, "field $field" ); break; + default: + $this->fail( "Type $type is not handled by this test." ); + break; } } } diff --git a/tests/phpunit/tests/formatting/sanitizeTextField.php b/tests/phpunit/tests/formatting/sanitizeTextField.php index 8d5d0ff0dd9d2..579f8e29de74e 100644 --- a/tests/phpunit/tests/formatting/sanitizeTextField.php +++ b/tests/phpunit/tests/formatting/sanitizeTextField.php @@ -2,8 +2,28 @@ /** * @group formatting + * + * @covers ::sanitize_text_field + * @covers ::sanitize_textarea_field */ class Tests_Formatting_SanitizeTextField extends WP_UnitTestCase { + + /** + * @ticket 32257 + * @dataProvider data_sanitize_text_field + */ + public function test_sanitize_text_field( $str, $expected ) { + if ( is_array( $expected ) ) { + $expected_oneline = $expected['oneline']; + $expected_multiline = $expected['multiline']; + } else { + $expected_oneline = $expected; + $expected_multiline = $expected; + } + $this->assertSame( $expected_oneline, sanitize_text_field( $str ) ); + $this->assertSameIgnoreEOL( $expected_multiline, sanitize_textarea_field( $str ) ); + } + public function data_sanitize_text_field() { return array( array( @@ -83,15 +103,15 @@ public function data_sanitize_text_field() { ), array( '%AB%BC%DE', // Just octets. - '', // Emtpy as we strip all the octets out. + '', // Empty as we strip all the octets out. ), array( - 'Invalid octects remain %II', - 'Invalid octects remain %II', + 'Invalid octets remain %II', + 'Invalid octets remain %II', ), array( - 'Nested octects %%%ABABAB %A%A%ABBB', - 'Nested octects', + 'Nested octets %%%ABABAB %A%A%ABBB', + 'Nested octets', ), array( array(), @@ -102,7 +122,7 @@ public function data_sanitize_text_field() { '', ), array( - new WP_Query, + new WP_Query(), '', ), array( @@ -125,19 +145,24 @@ public function data_sanitize_text_field() { } /** - * @ticket 32257 - * @dataProvider data_sanitize_text_field + * @ticket 60357 */ - public function test_sanitize_text_field( $string, $expected ) { - if ( is_array( $expected ) ) { - $expected_oneline = $expected['oneline']; - $expected_multiline = $expected['multiline']; - } else { - $expected_oneline = $expected; - $expected_multiline = $expected; - } - $this->assertSame( $expected_oneline, sanitize_text_field( $string ) ); - $this->assertSameIgnoreEOL( $expected_multiline, sanitize_textarea_field( $string ) ); + public function test_sanitize_text_field_filter() { + $filter = new MockAction(); + add_filter( 'sanitize_text_field', array( $filter, 'filter' ) ); + + $this->assertSame( 'example', sanitize_text_field( 'example' ) ); + $this->assertSame( 1, $filter->get_call_count(), 'The sanitize_text_field filter was not called.' ); + } + + /** + * @ticket 60357 + */ + public function test_sanitize_textarea_field_filter() { + $filter = new MockAction(); + add_filter( 'sanitize_textarea_field', array( $filter, 'filter' ) ); + $this->assertSame( 'example', sanitize_textarea_field( 'example' ) ); + $this->assertSame( 1, $filter->get_call_count(), 'The sanitize_textarea_field filter was not called.' ); } } diff --git a/tests/phpunit/tests/formatting/sanitizeTitle.php b/tests/phpunit/tests/formatting/sanitizeTitle.php index 7ce850f0fc6b7..faecb8c2c90e0 100644 --- a/tests/phpunit/tests/formatting/sanitizeTitle.php +++ b/tests/phpunit/tests/formatting/sanitizeTitle.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::sanitize_title */ class Tests_Formatting_SanitizeTitle extends WP_UnitTestCase { public function test_strips_html() { diff --git a/tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php b/tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php index 797a36f018ded..8a2ee4f9d9fee 100644 --- a/tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php +++ b/tests/phpunit/tests/formatting/sanitizeTitleWithDashes.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::sanitize_title_with_dashes */ class Tests_Formatting_SanitizeTitleWithDashes extends WP_UnitTestCase { public function test_strips_html() { @@ -83,6 +85,20 @@ public function test_replaces_ndash_mdash_entities() { $this->assertSame( 'do-the-dash', sanitize_title_with_dashes( 'Do — the — Dash', '', 'save' ) ); } + /** + * @ticket 64089 + */ + public function test_replaces_non_breaking_hyphen() { + $this->assertSame( 'do-the-dash', sanitize_title_with_dashes( 'Do‑the Dash', '', 'save' ) ); + } + + /** + * @ticket 64089 + */ + public function test_replaces_non_breaking_hyphen_entity() { + $this->assertSame( 'do-the-dash', sanitize_title_with_dashes( 'Do ‑ the Dash', '', 'save' ) ); + } + public function test_replaces_iexcel_iquest() { $this->assertSame( 'just-a-slug', sanitize_title_with_dashes( 'Just ¡a Slug', '', 'save' ) ); $this->assertSame( 'just-a-slug', sanitize_title_with_dashes( 'Just a Slug¿', '', 'save' ) ); @@ -149,6 +165,7 @@ public function test_replaces_acute_accents() { /** * @ticket 47912 + * @ticket 55117 * @dataProvider data_removes_non_visible_characters_without_width * * @param string $title The title to be sanitized. @@ -177,6 +194,7 @@ public function data_removes_non_visible_characters_without_width() { 'only %e2%80%ad' => array( '%e2%80%ad' ), 'only %e2%80%ae' => array( '%e2%80%ae' ), 'only %ef%bb%bf' => array( '%ef%bb%bf' ), + 'only %ef%bf%bc' => array( '%ef%bf%bc' ), // Non-visible characters within the title. 'in middle of title' => array( @@ -200,6 +218,7 @@ public function data_removes_non_visible_characters_without_width() { /** * @ticket 47912 + * @ticket 55117 * @dataProvider data_non_visible_characters_without_width_when_not_save * * @param string $title The title to be sanitized. @@ -228,6 +247,7 @@ public function data_non_visible_characters_without_width_when_not_save() { 'only %e2%80%ad' => array( '%e2%80%ad', '%e2%80%ad' ), 'only %e2%80%ae' => array( '%e2%80%ae', '%e2%80%ae' ), 'only %ef%bb%bf' => array( '%ef%bb%bf', '%ef%bb%bf' ), + 'only %ef%bf%bc' => array( '%ef%bf%bc', '%ef%bf%bc' ), // Non-visible characters within the title. 'in middle of title' => array( diff --git a/tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php b/tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php index 8337a2a6622e3..b560ad5c75530 100644 --- a/tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php +++ b/tests/phpunit/tests/formatting/sanitizeTrackbackUrls.php @@ -2,17 +2,22 @@ /** * @group formatting + * + * @covers ::sanitize_trackback_urls */ class Tests_Formatting_SanitizeTrackbackUrls extends WP_UnitTestCase { /** * @ticket 21624 - * @dataProvider breaks + * @dataProvider data_sanitize_trackback_urls_with_multiple_urls */ - public function test_sanitize_trackback_urls_with_multiple_urls( $break ) { - $this->assertSame( "http://example.com\nhttp://example.org", sanitize_trackback_urls( "http://example.com{$break}http://example.org" ) ); + public function test_sanitize_trackback_urls_with_multiple_urls( $separator ) { + $this->assertSame( + "http://example.com\nhttp://example.org", + sanitize_trackback_urls( "http://example.com{$separator}http://example.org" ) + ); } - public function breaks() { + public function data_sanitize_trackback_urls_with_multiple_urls() { return array( array( "\r\n\t " ), array( "\r" ), diff --git a/tests/phpunit/tests/formatting/sanitizeUser.php b/tests/phpunit/tests/formatting/sanitizeUser.php index e0ae998f5e97d..1f1cadd88424b 100644 --- a/tests/phpunit/tests/formatting/sanitizeUser.php +++ b/tests/phpunit/tests/formatting/sanitizeUser.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::sanitize_user */ class Tests_Formatting_SanitizeUser extends WP_UnitTestCase { public function test_strips_html() { diff --git a/tests/phpunit/tests/formatting/seemsUtf8.php b/tests/phpunit/tests/formatting/seemsUtf8.php deleted file mode 100644 index a5a6157fd283d..0000000000000 --- a/tests/phpunit/tests/formatting/seemsUtf8.php +++ /dev/null @@ -1,44 +0,0 @@ -<?php - -/** - * @group formatting - */ -class Tests_Formatting_SeemsUtf8 extends WP_UnitTestCase { - - /** - * `seems_utf8` returns true for utf-8 strings, false otherwise. - * - * @dataProvider utf8_strings - */ - public function test_returns_true_for_utf8_strings( $utf8_string ) { - // From http://www.i18nguy.com/unicode-example.html - $this->assertTrue( seems_utf8( $utf8_string ) ); - } - - public function utf8_strings() { - $utf8_strings = file( DIR_TESTDATA . '/formatting/utf-8/utf-8.txt' ); - foreach ( $utf8_strings as &$string ) { - $string = (array) trim( $string ); - } - unset( $string ); - return $utf8_strings; - } - - /** - * @dataProvider big5_strings - */ - public function test_returns_false_for_non_utf8_strings( $big5_string ) { - $this->assertFalse( seems_utf8( $big5_string ) ); - } - - public function big5_strings() { - // Get data from formatting/big5.txt. - $big5_strings = file( DIR_TESTDATA . '/formatting/big5.txt' ); - foreach ( $big5_strings as &$string ) { - $string = (array) trim( $string ); - } - unset( $string ); - return $big5_strings; - } -} - diff --git a/tests/phpunit/tests/formatting/slashit.php b/tests/phpunit/tests/formatting/slashit.php index 9c32aa2c649e9..903cf9f65fe90 100644 --- a/tests/phpunit/tests/formatting/slashit.php +++ b/tests/phpunit/tests/formatting/slashit.php @@ -4,18 +4,31 @@ * @group formatting */ class Tests_Formatting_Slashit extends WP_UnitTestCase { + + /** + * @covers ::backslashit + */ public function test_backslashes_middle_numbers() { $this->assertSame( "\\a-!9\\a943\\b\\c", backslashit( 'a-!9a943bc' ) ); } + /** + * @covers ::backslashit + */ public function test_backslashes_alphas() { $this->assertSame( "\\a943\\b\\c", backslashit( 'a943bc' ) ); } + /** + * @covers ::backslashit + */ public function test_double_backslashes_leading_numbers() { $this->assertSame( '\\\\95', backslashit( '95' ) ); } + /** + * @covers ::untrailingslashit + */ public function test_removes_trailing_slashes() { $this->assertSame( 'a', untrailingslashit( 'a/' ) ); $this->assertSame( 'a', untrailingslashit( 'a////' ) ); @@ -23,6 +36,8 @@ public function test_removes_trailing_slashes() { /** * @ticket 22267 + * + * @covers ::untrailingslashit */ public function test_removes_trailing_backslashes() { $this->assertSame( 'a', untrailingslashit( 'a\\' ) ); @@ -31,22 +46,32 @@ public function test_removes_trailing_backslashes() { /** * @ticket 22267 + * + * @covers ::untrailingslashit */ public function test_removes_trailing_mixed_slashes() { $this->assertSame( 'a', untrailingslashit( 'a/\\' ) ); $this->assertSame( 'a', untrailingslashit( 'a\\/\\///\\\\//' ) ); } + /** + * @covers ::trailingslashit + */ public function test_adds_trailing_slash() { $this->assertSame( 'a/', trailingslashit( 'a' ) ); } + /** + * @covers ::trailingslashit + */ public function test_does_not_add_trailing_slash_if_one_exists() { $this->assertSame( 'a/', trailingslashit( 'a/' ) ); } /** * @ticket 22267 + * + * @covers ::trailingslashit */ public function test_converts_trailing_backslash_to_slash_if_one_exists() { $this->assertSame( 'a/', trailingslashit( 'a\\' ) ); diff --git a/tests/phpunit/tests/formatting/stripslashesDeep.php b/tests/phpunit/tests/formatting/stripslashesDeep.php index c5ce91b44b925..ea2c9139545be 100644 --- a/tests/phpunit/tests/formatting/stripslashesDeep.php +++ b/tests/phpunit/tests/formatting/stripslashesDeep.php @@ -3,6 +3,8 @@ /** * @group formatting * @group slashes + * + * @covers ::stripslashes_deep */ class Tests_Formatting_StripslashesDeep extends WP_UnitTestCase { /** @@ -24,7 +26,7 @@ public function test_preserves_original_datatype() { $this->assertSame( $arr, stripslashes_deep( $arr ) ); // Keyed array. $this->assertSame( array_values( $arr ), stripslashes_deep( array_values( $arr ) ) ); // Non-keyed. - $obj = new stdClass; + $obj = new stdClass(); foreach ( $arr as $k => $v ) { $obj->$k = $v; } @@ -39,9 +41,9 @@ public function test_strips_slashes() { $this->assertSame( array( 'a' => $new ), stripslashes_deep( array( 'a' => $old ) ) ); // Keyed array. $this->assertSame( array( $new ), stripslashes_deep( array( $old ) ) ); // Non-keyed. - $obj_old = new stdClass; + $obj_old = new stdClass(); $obj_old->a = $old; - $obj_new = new stdClass; + $obj_new = new stdClass(); $obj_new->a = $new; $this->assertEquals( $obj_new, stripslashes_deep( $obj_old ) ); } diff --git a/tests/phpunit/tests/formatting/urlShorten.php b/tests/phpunit/tests/formatting/urlShorten.php index 5900e358e9762..4909130dbce21 100644 --- a/tests/phpunit/tests/formatting/urlShorten.php +++ b/tests/phpunit/tests/formatting/urlShorten.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::url_shorten */ class Tests_Formatting_UrlShorten extends WP_UnitTestCase { public function test_url_shorten() { diff --git a/tests/phpunit/tests/formatting/urlencodeDeep.php b/tests/phpunit/tests/formatting/urlencodeDeep.php index 27c0c750bbbfe..9436ba09afc16 100644 --- a/tests/phpunit/tests/formatting/urlencodeDeep.php +++ b/tests/phpunit/tests/formatting/urlencodeDeep.php @@ -3,13 +3,27 @@ /** * @group formatting * @ticket 22300 + * + * @covers ::urlencode_deep */ class Tests_Formatting_UrlencodeDeep extends WP_UnitTestCase { /** - * Data Provider + * Tests the urlencode_deep() function pair by pair. + * + * @dataProvider data_urlencode_deep + * + * @param string $input + * @param string $expected + */ + public function test_urlencode_deep_should_encode_individual_value( $input, $expected ) { + $this->assertSame( $expected, urlencode_deep( $input ) ); + } + + /** + * Data provider. */ - public function data_test_values() { + public function data_urlencode_deep() { return array( array( 'qwerty123456', 'qwerty123456' ), array( '|!"£$%&/()=?', '%7C%21%22%C2%A3%24%25%26%2F%28%29%3D%3F' ), @@ -20,27 +34,14 @@ public function data_test_values() { } /** - * Validate the urlencode_deep function pair by pair - * - * @dataProvider data_test_values - * - * @param string $actual - * @param string $expected - */ - public function test_urlencode_deep_should_encode_individual_value( $actual, $expected ) { - $this->assertSame( $expected, urlencode_deep( $actual ) ); - } - - /** - * Test the whole array as input + * Tests the whole array as input. */ public function test_urlencode_deep_should_encode_all_values_in_array() { - $data = $this->data_test_values(); + $data = $this->data_urlencode_deep(); $actual = wp_list_pluck( $data, 0 ); $expected = wp_list_pluck( $data, 1 ); $this->assertSame( $expected, urlencode_deep( $actual ) ); } - } diff --git a/tests/phpunit/tests/formatting/utf8UriEncode.php b/tests/phpunit/tests/formatting/utf8UriEncode.php index 310b906104acb..cb6f513c8edef 100644 --- a/tests/phpunit/tests/formatting/utf8UriEncode.php +++ b/tests/phpunit/tests/formatting/utf8UriEncode.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::utf8_uri_encode */ class Tests_Formatting_Utf8UriEncode extends WP_UnitTestCase { @@ -20,7 +22,7 @@ public function test_percent_encodes_non_reserved_characters( $utf8, $urlencoded */ public function test_output_is_not_longer_than_optional_length_argument( $utf8, $unused_for_this_test ) { $max_length = 30; - $this->assertTrue( strlen( utf8_uri_encode( $utf8, $max_length ) ) <= $max_length ); + $this->assertLessThanOrEqual( $max_length, strlen( utf8_uri_encode( $utf8, $max_length ) ) ); } public function data() { @@ -33,4 +35,3 @@ public function data() { return $data_provided; } } - diff --git a/tests/phpunit/tests/formatting/wpAutop.php b/tests/phpunit/tests/formatting/wpAutop.php index 35567e87429b3..ee4d90645d09c 100644 --- a/tests/phpunit/tests/formatting/wpAutop.php +++ b/tests/phpunit/tests/formatting/wpAutop.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wpautop */ class Tests_Formatting_wpAutop extends WP_UnitTestCase { @@ -20,9 +22,9 @@ public function test_first_post() { <p>Then you can start enjoying the WordPress experience:</p> <ul> <li>Edit your personal information at <a href="%3$s" title="Edit settings like your password, your display name and your contact information">Users › Profile</a></li> -<li>Start publishing at <a href="%4$s" title="Create a new post">Posts › Add New</a> and at <a href="%5$s" title="Create a new page">Pages › Add New</a></li> -<li>Browse and install plugins at <a href="%6$s" title="Browse and install plugins at the official WordPress repository directly from your Dashboard">Plugins › Add New</a></li> -<li>Browse and install themes at <a href="%7$s" title="Browse and install themes at the official WordPress repository directly from your Dashboard">Appearance › Add New Themes</a></li> +<li>Start publishing at <a href="%4$s" title="Create a new post">Posts › Add</a> and at <a href="%5$s" title="Create a new page">Pages › Add</a></li> +<li>Browse and install plugins at <a href="%6$s" title="Browse and install plugins at the official WordPress repository directly from your Dashboard">Plugins › Add</a></li> +<li>Browse and install themes at <a href="%7$s" title="Browse and install themes at the official WordPress repository directly from your Dashboard">Appearance › Add Themes</a></li> <li>Modify and prettify your website’s links at <a href="%8$s" title="For example, select a link structure like: http://example.com/1999/12/post-name">Settings › Permalinks</a></li> <li>Import content from another system or WordPress site at <a href="%9$s" title="WordPress comes with importers for the most common publishing systems">Tools › Import</a></li> <li>Find answers to your questions at the <a href="%10$s" title="The official WordPress documentation, maintained by the WordPress community">WordPress Codex</a></li> @@ -45,9 +47,9 @@ public function test_first_post() { Then you can start enjoying the WordPress experience: <ul> <li>Edit your personal information at <a href="%3$s" title="Edit settings like your password, your display name and your contact information">Users › Profile</a></li> -<li>Start publishing at <a href="%4$s" title="Create a new post">Posts › Add New</a> and at <a href="%5$s" title="Create a new page">Pages › Add New</a></li> -<li>Browse and install plugins at <a href="%6$s" title="Browse and install plugins at the official WordPress repository directly from your Dashboard">Plugins › Add New</a></li> -<li>Browse and install themes at <a href="%7$s" title="Browse and install themes at the official WordPress repository directly from your Dashboard">Appearance › Add New Themes</a></li> +<li>Start publishing at <a href="%4$s" title="Create a new post">Posts › Add</a> and at <a href="%5$s" title="Create a new page">Pages › Add</a></li> +<li>Browse and install plugins at <a href="%6$s" title="Browse and install plugins at the official WordPress repository directly from your Dashboard">Plugins › Add</a></li> +<li>Browse and install themes at <a href="%7$s" title="Browse and install themes at the official WordPress repository directly from your Dashboard">Appearance › Add Themes</a></li> <li>Modify and prettify your website’s links at <a href="%8$s" title="For example, select a link structure like: http://example.com/1999/12/post-name">Settings › Permalinks</a></li> <li>Import content from another system or WordPress site at <a href="%9$s" title="WordPress comes with importers for the most common publishing systems">Tools › Import</a></li> <li>Find answers to your questions at the <a href="%10$s" title="The official WordPress documentation, maintained by the WordPress community">WordPress Codex</a></li> @@ -102,6 +104,63 @@ public function test_skip_input_elements() { $this->assertSame( "<p>$str</p>", trim( wpautop( $str ) ) ); } + /** + * wpautop() Should add <p> around inline "<math>" elements. + * + * @ticket 13340 + */ + public function test_wrap_inline_math_elements() { + $str = '<math><mrow><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup><mo>=</mo><msup><mi>c</mi><mn>2</mn></msup></mrow></math>'; + + $this->assertSame( "<p>$str</p>", trim( wpautop( $str ) ) ); + } + + /** + * wpautop() Should not add <br> inside block "<math>" elements. + * + * @ticket 13340 + */ + public function test_skip_block_math_elements() { + $str = '<math display="block"> + <mtable> + <mtr> + <mtd> + <msup><mrow><mo>(</mo><mi>a</mi><mo>+</mo><mi>b</mi><mo>)</mo></mrow><mn>2</mn></msup> + </mtd> + <mtd> + <mo>=</mo> + </mtd> + <mtd> + <msup><mi>c</mi><mn>2</mn></msup> + <mo>+</mo><mn>4</mn><mo>⋅</mo> + <mo>(</mo><mfrac><mn>1</mn><mn>2</mn></mfrac><mi>a</mi><mi>b</mi><mo>)</mo> + </mtd> + </mtr> + <mtr> + <mtd> + <msup><mi>a</mi><mn>2</mn></msup> + <mo>+</mo><mn>2</mn><mi>a</mi><mi>b</mi><mo>+</mo> + <msup><mi>b</mi><mn>2</mn></msup> + </mtd> + <mtd> + <mo>=</mo> + </mtd> + <mtd> + <msup><mi>c</mi><mn>2</mn></msup> + <mo>+</mo><mn>2</mn><mi>a</mi><mi>b</mi> + </mtd> + </mtr> + <mtr> + <mtd><msup><mi>a</mi><mn>2</mn></msup><mo>+</mo><msup><mi>b</mi><mn>2</mn></msup></mtd> + <mtd><mo>=</mo></mtd> + <mtd><msup><mi>c</mi><mn>2</mn></msup></mtd> + </mtr> + </mtable> +</math>'; + + $this->assertSameIgnoreEOL( "<p>$str</p>", trim( wpautop( $str ) ) ); + } + /** * wpautop() Should not add <p> and <br/> around <source> and <track> * @@ -306,7 +365,6 @@ public function test_that_wpautop_treats_block_level_elements_as_blocks() { 'map', 'area', 'address', - 'math', 'style', 'p', 'h1', @@ -509,7 +567,7 @@ public function test_that_wpautop_skips_line_breaks_after_br() { } /** - * wpautop() should convert multiple line breaks into a paragraph regarless of <br /> format + * wpautop() should convert multiple line breaks into a paragraph regardless of <br /> format * * @ticket 33377 */ @@ -531,7 +589,7 @@ public function test_that_wpautop_adds_a_paragraph_after_multiple_br() { /** * @ticket 4857 */ - public function test_that_text_before_blocks_is_peed() { + public function test_that_text_before_blocks_is_wrapped_in_a_paragraph() { $content = 'a<div>b</div>'; $expected = "<p>a</p>\n<div>b</div>"; @@ -541,9 +599,6 @@ public function test_that_text_before_blocks_is_peed() { /** * wpautop() should not add extra </p> before <figcaption> * - * @covers ::wpautop - * @uses ::trim - * * @ticket 39307 */ public function test_that_wpautop_does_not_add_extra_closing_p_in_figure() { @@ -565,7 +620,7 @@ public function test_that_wpautop_does_not_add_extra_closing_p_in_figure() { /** * @ticket 14674 */ - public function test_the_hr_is_not_peed() { + public function test_the_hr_is_not_wrapped_in_a_paragraph() { $content = 'paragraph1<hr>paragraph2'; $expected = "<p>paragraph1</p>\n<hr>\n<p>paragraph2</p>"; diff --git a/tests/phpunit/tests/formatting/wpBasename.php b/tests/phpunit/tests/formatting/wpBasename.php index 6bb7d5684b9ae..613c23b617563 100644 --- a/tests/phpunit/tests/formatting/wpBasename.php +++ b/tests/phpunit/tests/formatting/wpBasename.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_basename */ class Tests_Formatting_wpBasename extends WP_UnitTestCase { @@ -38,5 +40,4 @@ public function test_wp_basename_windows_utf8_support() { wp_basename( 'C:\test\щипцы.txt' ) ); } - } diff --git a/tests/phpunit/tests/formatting/wpHtmlExcerpt.php b/tests/phpunit/tests/formatting/wpHtmlExcerpt.php index 3aff259343c0b..96cd58226d2dc 100644 --- a/tests/phpunit/tests/formatting/wpHtmlExcerpt.php +++ b/tests/phpunit/tests/formatting/wpHtmlExcerpt.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_html_excerpt */ class Tests_Formatting_wpHtmlExcerpt extends WP_UnitTestCase { public function test_simple() { diff --git a/tests/phpunit/tests/formatting/wpHtmlSplit.php b/tests/phpunit/tests/formatting/wpHtmlSplit.php index befe43abb8e92..750ad3821cc54 100644 --- a/tests/phpunit/tests/formatting/wpHtmlSplit.php +++ b/tests/phpunit/tests/formatting/wpHtmlSplit.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_html_split */ class Tests_Formatting_wpHtmlSplit extends WP_UnitTestCase { @@ -39,6 +41,8 @@ public function data_basic_features() { * Automated performance testing of the main regex. * * @dataProvider data_whole_posts + * + * @covers ::get_html_split_regex */ public function test_pcre_performance( $input ) { $regex = get_html_split_regex(); diff --git a/tests/phpunit/tests/formatting/wpHtmleditPre.php b/tests/phpunit/tests/formatting/wpHtmleditPre.php index 109ea4bdf20b5..272aac2684bf9 100644 --- a/tests/phpunit/tests/formatting/wpHtmleditPre.php +++ b/tests/phpunit/tests/formatting/wpHtmleditPre.php @@ -3,6 +3,8 @@ /** * @group formatting * @expectedDeprecated wp_htmledit_pre + * + * @covers ::wp_htmledit_pre */ class Tests_Formatting_wpHtmleditPre extends WP_UnitTestCase { @@ -11,7 +13,6 @@ public function charset_iso_8859_1() { } /* - * Only fails in PHP 5.4 onwards * @ticket 23688 */ public function test_wp_htmledit_pre_charset_iso_8859_1() { diff --git a/tests/phpunit/tests/formatting/wpIsoDescrambler.php b/tests/phpunit/tests/formatting/wpIsoDescrambler.php index a15cabc4d2ee2..643ebcc836991 100644 --- a/tests/phpunit/tests/formatting/wpIsoDescrambler.php +++ b/tests/phpunit/tests/formatting/wpIsoDescrambler.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_iso_descrambler */ class Tests_Formatting_wpIsoDescrambler extends WP_UnitTestCase { /* diff --git a/tests/phpunit/tests/formatting/wpMakeLinkRelative.php b/tests/phpunit/tests/formatting/wpMakeLinkRelative.php index ee3f78e9a29fa..4c21381a420de 100644 --- a/tests/phpunit/tests/formatting/wpMakeLinkRelative.php +++ b/tests/phpunit/tests/formatting/wpMakeLinkRelative.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_make_link_relative */ class Tests_Formatting_wpMakeLinkRelative extends WP_UnitTestCase { @@ -43,5 +45,4 @@ public function test_wp_make_link_relative_with_no_path() { $relative_link = wp_make_link_relative( $link ); $this->assertSame( '', $relative_link ); } - } diff --git a/tests/phpunit/tests/formatting/wpParseStr.php b/tests/phpunit/tests/formatting/wpParseStr.php index b8c3d063d2423..502715b97bf3b 100644 --- a/tests/phpunit/tests/formatting/wpParseStr.php +++ b/tests/phpunit/tests/formatting/wpParseStr.php @@ -2,6 +2,7 @@ /** * @group formatting + * * @covers ::wp_parse_str */ class Tests_Formatting_wpParseStr extends WP_UnitTestCase { @@ -23,7 +24,7 @@ public function test_wp_parse_str( $input, $expected ) { } /** - * Data Provider. + * Data provider. * * @return array */ diff --git a/tests/phpunit/tests/formatting/wpRelNofollow.php b/tests/phpunit/tests/formatting/wpRelNofollow.php index d10898d7ea21f..83a9c00c23e95 100644 --- a/tests/phpunit/tests/formatting/wpRelNofollow.php +++ b/tests/phpunit/tests/formatting/wpRelNofollow.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_rel_nofollow */ class Tests_Formatting_wpRelNofollow extends WP_UnitTestCase { @@ -9,38 +11,18 @@ class Tests_Formatting_wpRelNofollow extends WP_UnitTestCase { * @ticket 9959 */ public function test_add_no_follow() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - $content = '<p>This is some cool <a href="/">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"/\" rel=\"nofollow\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '<p>This is some cool <a href="/" rel="nofollow">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } /** * @ticket 9959 */ public function test_convert_no_follow() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - $content = '<p>This is some cool <a href="/" rel="weird">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"/\" rel=\"weird nofollow\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '<p>This is some cool <a href="/" rel="weird nofollow">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } /** @@ -48,17 +30,7 @@ public function test_convert_no_follow() { * @dataProvider data_wp_rel_nofollow */ public function test_wp_rel_nofollow( $input, $output, $expect_deprecation = false ) { - if ( true === $expect_deprecation && PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - - $this->assertSame( wp_slash( $output ), wp_rel_nofollow( $input ) ); + $this->assertEqualHTML( $output, stripslashes( wp_rel_nofollow( $input ) ) ); } public function data_wp_rel_nofollow() { @@ -107,18 +79,8 @@ public function data_wp_rel_nofollow() { } public function test_append_no_follow_with_valueless_attribute() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - $content = '<p>This is some cool <a href="demo.com" download rel="hola">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"demo.com\" download rel=\"hola nofollow\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_nofollow( $content ) ); + $expected = '<p>This is some cool <a href="demo.com" download rel="hola nofollow">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_nofollow( $content ) ) ); } } diff --git a/tests/phpunit/tests/formatting/wpRelUgc.php b/tests/phpunit/tests/formatting/wpRelUgc.php index cbd052c71e059..b1109ed745594 100644 --- a/tests/phpunit/tests/formatting/wpRelUgc.php +++ b/tests/phpunit/tests/formatting/wpRelUgc.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_rel_ugc */ class Tests_Formatting_wpRelUgc extends WP_UnitTestCase { @@ -9,38 +11,18 @@ class Tests_Formatting_wpRelUgc extends WP_UnitTestCase { * @ticket 48022 */ public function test_add_ugc() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - $content = '<p>This is some cool <a href="/">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"/\" rel=\"nofollow ugc\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_ugc( $content ) ); + $expected = '<p>This is some cool <a href="/" rel="nofollow ugc">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_ugc( $content ) ) ); } /** * @ticket 48022 */ public function test_convert_ugc() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - $content = '<p>This is some cool <a href="/" rel="weird">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"/\" rel=\"weird nofollow ugc\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_ugc( $content ) ); + $expected = '<p>This is some cool <a href="/" rel="weird nofollow ugc">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_ugc( $content ) ) ); } /** @@ -48,17 +30,7 @@ public function test_convert_ugc() { * @dataProvider data_wp_rel_ugc */ public function test_wp_rel_ugc( $input, $output, $expect_deprecation = false ) { - if ( true === $expect_deprecation && PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } - - $this->assertSame( wp_slash( $output ), wp_rel_ugc( $input ) ); + $this->assertEqualHTML( $output, stripslashes( wp_rel_ugc( $input ) ) ); } public function data_wp_rel_ugc() { @@ -97,28 +69,19 @@ public function data_wp_rel_ugc() { ), array( '<a href="' . $home_url_http . '/some-url">Home URL (http)</a>', - '<a href="' . $home_url_http . '/some-url">Home URL (http)</a>', + '<a href="' . $home_url_http . '/some-url" rel="ugc">Home URL (http)</a>', ), array( '<a href="' . $home_url_https . '/some-url">Home URL (https)</a>', - '<a href="' . $home_url_https . '/some-url">Home URL (https)</a>', + '<a href="' . $home_url_https . '/some-url" rel="ugc">Home URL (https)</a>', ), ); } public function test_append_ugc_with_valueless_attribute() { - if ( PHP_VERSION_ID >= 80100 ) { - /* - * For the time being, ignoring PHP 8.1 "null to non-nullable" deprecations coming in - * via hooked in filter functions until a more structural solution to the - * "missing input validation" conundrum has been architected and implemented. - */ - $this->expectDeprecation(); - $this->expectDeprecationMessageMatches( '`Passing null to parameter \#[0-9]+ \(\$[^\)]+\) of type [^ ]+ is deprecated`' ); - } $content = '<p>This is some cool <a href="demo.com" download rel="hola">Code</a></p>'; - $expected = '<p>This is some cool <a href=\"demo.com\" download rel=\"hola nofollow ugc\">Code</a></p>'; - $this->assertSame( $expected, wp_rel_ugc( $content ) ); + $expected = '<p>This is some cool <a href="demo.com" download rel="hola nofollow ugc">Code</a></p>'; + $this->assertEqualHTML( $expected, stripslashes( wp_rel_ugc( $content ) ) ); } } diff --git a/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php b/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php index 9781dfbc1b5b5..b461e3bfe8e3c 100644 --- a/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php +++ b/tests/phpunit/tests/formatting/wpReplaceInHtmlTags.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_replace_in_html_tags */ class Tests_Formatting_wpReplaceInHtmlTags extends WP_UnitTestCase { /** @@ -34,4 +36,3 @@ public function data_wp_replace_in_html_tags() { ); } } - diff --git a/tests/phpunit/tests/formatting/wpRicheditPre.php b/tests/phpunit/tests/formatting/wpRicheditPre.php index be12ddd55fba9..323eda673c48d 100644 --- a/tests/phpunit/tests/formatting/wpRicheditPre.php +++ b/tests/phpunit/tests/formatting/wpRicheditPre.php @@ -3,6 +3,8 @@ /** * @group formatting * @expectedDeprecated wp_richedit_pre + * + * @covers ::wp_richedit_pre */ class Tests_Formatting_wpRicheditPre extends WP_UnitTestCase { @@ -11,7 +13,6 @@ public function charset_iso_8859_1() { } /* - * Only fails in PHP 5.4 onwards * @ticket 23688 */ public function test_wp_richedit_pre_charset_iso_8859_1() { diff --git a/tests/phpunit/tests/formatting/wpSlash.php b/tests/phpunit/tests/formatting/wpSlash.php index 0431b7602d738..3617b4bfa4ea0 100644 --- a/tests/phpunit/tests/formatting/wpSlash.php +++ b/tests/phpunit/tests/formatting/wpSlash.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_slash */ class Tests_Formatting_wpSlash extends WP_UnitTestCase { @@ -82,7 +84,7 @@ public function test_preserves_original_datatype() { $this->assertSame( $arr, wp_slash( $arr ) ); // Keyed array. $this->assertSame( array_values( $arr ), wp_slash( array_values( $arr ) ) ); // Non-keyed. - $obj = new stdClass; + $obj = new stdClass(); foreach ( $arr as $k => $v ) { $obj->$k = $v; } @@ -100,4 +102,36 @@ public function test_add_even_more_slashes() { $this->assertSame( array( $new ), wp_slash( array( $old ) ) ); // Non-keyed. } + /** + * Tests that addslashes_gpc() returns the same result as wp_slash() for strings. + * + * @ticket 64539 + * @covers ::addslashes_gpc + * @expectedDeprecated addslashes_gpc + */ + public function test_addslashes_gpc_matches_wp_slash_for_strings() { + $input = "String with 'quotes' and \"double quotes\""; + $this->assertSame( wp_slash( $input ), addslashes_gpc( $input ) ); + } + + /** + * Tests that addslashes_gpc() returns the same result as wp_slash() for arrays. + * + * @ticket 64539 + * @covers ::addslashes_gpc + * @expectedDeprecated addslashes_gpc + */ + public function test_addslashes_gpc_matches_wp_slash_for_arrays() { + $input = array( + 'field1' => "Value with 'apostrophe'", + 'field2' => 'Value with "quotes"', + 'field3' => 'user@example.com', + 'nested' => array( + 'key1' => 'Nested value with \\ backslash', + 'key2' => array( 'deeply', 'nested', 'array' ), + ), + ); + + $this->assertSame( wp_slash( $input ), addslashes_gpc( $input ) ); + } } diff --git a/tests/phpunit/tests/formatting/wpSpecialchars.php b/tests/phpunit/tests/formatting/wpSpecialchars.php index 07fc2b1ad0af1..edbf29336cc26 100644 --- a/tests/phpunit/tests/formatting/wpSpecialchars.php +++ b/tests/phpunit/tests/formatting/wpSpecialchars.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::_wp_specialchars */ class Tests_Formatting_wpSpecialchars extends WP_UnitTestCase { public function test_wp_specialchars_basics() { @@ -15,6 +17,8 @@ public function test_wp_specialchars_basics() { public function test_allowed_entity_names() { global $allowedentitynames; + $this->assertNotEmpty( $allowedentitynames ); + // Allowed entities should be unchanged. foreach ( $allowedentitynames as $ent ) { if ( 'apos' === $ent ) { diff --git a/tests/phpunit/tests/formatting/wpStripAllTags.php b/tests/phpunit/tests/formatting/wpStripAllTags.php index 1ecc12bda491f..6afcbed79fc48 100644 --- a/tests/phpunit/tests/formatting/wpStripAllTags.php +++ b/tests/phpunit/tests/formatting/wpStripAllTags.php @@ -3,6 +3,8 @@ * Test wp_strip_all_tags() * * @group formatting + * + * @covers ::wp_strip_all_tags */ class Tests_Formatting_wpStripAllTags extends WP_UnitTestCase { @@ -29,5 +31,75 @@ public function test_wp_strip_all_tags() { $text = "lorem<style>* { display: 'none' }<script>alert( document.cookie )</script></style>ipsum"; $this->assertSame( 'loremipsum', wp_strip_all_tags( $text ) ); } -} + /** + * Tests that `wp_strip_all_tags()` returns an empty string when null is passed. + * + * @ticket 56434 + */ + public function test_wp_strip_all_tags_should_return_empty_string_for_a_null_arg() { + $this->assertSame( '', wp_strip_all_tags( null ) ); + } + + /** + * Tests that `wp_strip_all_tags()` triggers a warning and returns + * an empty string when passed a non-string argument. + * + * @ticket 56434 + * + * @dataProvider data_wp_strip_all_tags_should_return_empty_string_and_trigger_an_error_for_non_string_arg + * + * @param mixed $non_string A non-string value. + */ + public function test_wp_strip_all_tags_should_return_empty_string_and_trigger_an_error_for_non_string_arg( $non_string ) { + $type = gettype( $non_string ); + $this->expectError(); + $this->expectErrorMessage( "Warning: wp_strip_all_tags expects parameter #1 (\$text) to be a string, $type given." ); + $this->assertSame( '', wp_strip_all_tags( $non_string ) ); + } + + /** + * Data provider for test_wp_strip_all_tags_should_return_empty_string_and_trigger_an_error_for_non_string_arg(). + * + * @return array[] + */ + public function data_wp_strip_all_tags_should_return_empty_string_and_trigger_an_error_for_non_string_arg() { + return array( + 'an empty array' => array( 'non_string' => array() ), + 'a non-empty array' => array( 'non_string' => array( 'a string' ) ), + 'an empty object' => array( 'non_string' => new stdClass() ), + 'a non-empty object' => array( 'non_string' => (object) array( 'howdy' => 'admin' ) ), + ); + } + + /** + * Tests that `wp_strip_all_tags()` casts scalar values to string. + * + * @ticket 56434 + * + * @dataProvider data_wp_strip_all_tags_should_cast_scalar_values_to_string + * + * @param mixed $text A scalar value. + */ + public function test_wp_strip_all_tags_should_cast_scalar_values_to_string( $text ) { + $this->assertSame( (string) $text, wp_strip_all_tags( $text ) ); + } + + /** + * Data provider for test_wp_strip_all_tags_should_cast_scalar_values_to_string(). + * + * @return array[] + */ + public function data_wp_strip_all_tags_should_cast_scalar_values_to_string() { + return array( + '(int) 0' => array( 'text' => 0 ), + '(int) 1' => array( 'text' => 1 ), + '(int) -1' => array( 'text' => -1 ), + '(float) 0.0' => array( 'text' => 0.0 ), + '(float) 1.0' => array( 'text' => 1.0 ), + '(float) -1.0' => array( 'text' => -1.0 ), + '(bool) false' => array( 'text' => false ), + '(bool) true' => array( 'text' => true ), + ); + } +} diff --git a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php b/tests/phpunit/tests/formatting/wpTargetedLinkRel.php deleted file mode 100644 index b58f4188b58cb..0000000000000 --- a/tests/phpunit/tests/formatting/wpTargetedLinkRel.php +++ /dev/null @@ -1,138 +0,0 @@ -<?php - -/** - * @group formatting - * @ticket 43187 - */ -class Tests_Formatting_wpTargetedLinkRel extends WP_UnitTestCase { - - public function test_add_to_links_with_target_blank() { - $content = '<p>Links: <a href="/" target="_blank">No rel</a></p>'; - $expected = '<p>Links: <a href="/" target="_blank" rel="noopener">No rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_add_to_links_with_target_foo() { - $content = '<p>Links: <a href="/" target="foo">No rel</a></p>'; - $expected = '<p>Links: <a href="/" target="foo" rel="noopener">No rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_target_as_first_attribute() { - $content = '<p>Links: <a target="_blank" href="#">No rel</a></p>'; - $expected = '<p>Links: <a target="_blank" href="#" rel="noopener">No rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_add_to_existing_rel() { - $content = '<p>Links: <a href="/" rel="existing values" target="_blank">Existing rel</a></p>'; - $expected = '<p>Links: <a href="/" rel="existing values noopener" target="_blank">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_no_duplicate_values_added() { - $content = '<p>Links: <a href="/" rel="existing noopener values" target="_blank">Existing rel</a></p>'; - $expected = '<p>Links: <a href="/" rel="existing noopener values" target="_blank">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_rel_with_single_quote_delimiter() { - $content = '<p>Links: <a href="/" rel=\'existing values\' target="_blank">Existing rel</a></p>'; - $expected = '<p>Links: <a href="/" rel="existing values noopener" target="_blank">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_rel_with_no_delimiter() { - $content = '<p>Links: <a href="/" rel=existing target="_blank">Existing rel</a></p>'; - $expected = '<p>Links: <a href="/" rel="existing noopener" target="_blank">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_rel_value_spaced_and_no_delimiter() { - $content = '<p>Links: <a href="/" rel = existing target="_blank">Existing rel</a></p>'; - $expected = '<p>Links: <a href="/" rel="existing noopener" target="_blank">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_escaped_quotes() { - $content = '<p>Links: <a href=\"/\" rel=\"existing values\" target=\"_blank\">Existing rel</a></p>'; - $expected = '<p>Links: <a href=\"/\" rel=\"existing values noopener\" target=\"_blank\">Existing rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_ignore_links_with_no_target() { - $content = '<p>Links: <a href="/" target="_blank">Change me</a> <a href="/">Do not change me</a></p>'; - $expected = '<p>Links: <a href="/" target="_blank" rel="noopener">Change me</a> <a href="/">Do not change me</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - /** - * Ensure empty rel attributes are not added. - * - * @ticket 45352 - */ - public function test_ignore_if_wp_targeted_link_rel_nulled() { - add_filter( 'wp_targeted_link_rel', '__return_empty_string' ); - $content = '<p>Links: <a href="/" target="_blank">Do not change me</a></p>'; - $expected = '<p>Links: <a href="/" target="_blank">Do not change me</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - /** - * Ensure default content filters are added. - * - * @ticket 45292 - */ - public function test_wp_targeted_link_rel_filters_run() { - $content = '<p>Links: <a href="/" target="_blank">No rel</a></p>'; - $expected = '<p>Links: <a href="/" target="_blank" rel="noopener">No rel</a></p>'; - - $post = $this->factory()->post->create_and_get( - array( - 'post_content' => $content, - ) - ); - - $this->assertSame( $expected, $post->post_content ); - } - - /** - * Ensure JSON format is preserved when relation attribute (rel) is missing. - * - * @ticket 46316 - */ - public function test_wp_targeted_link_rel_should_preserve_json() { - $content = '<p>Links: <a href=\"\/\" target=\"_blank\">No rel<\/a><\/p>'; - $expected = '<p>Links: <a href=\"\/\" target=\"_blank\" rel=\"noopener\">No rel<\/a><\/p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - /** - * Ensure the content of style and script tags are not processed - * - * @ticket 47244 - */ - public function test_wp_targeted_link_rel_skips_style_and_scripts() { - $content = '<style><a href="/" target=a></style><p>Links: <script>console.log("<a href=\'/\' target=a>hi</a>");</script><script>alert(1);</script>here <a href="/" target=_blank>aq</a></p><script>console.log("<a href=\'last\' target=\'_blank\'")</script>'; - $expected = '<style><a href="/" target=a></style><p>Links: <script>console.log("<a href=\'/\' target=a>hi</a>");</script><script>alert(1);</script>here <a href="/" target="_blank" rel="noopener">aq</a></p><script>console.log("<a href=\'last\' target=\'_blank\'")</script>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - /** - * Ensure entirely serialized content is ignored. - * - * @ticket 46402 - */ - public function test_ignore_entirely_serialized_content() { - $content = 'a:1:{s:4:"html";s:52:"<p>Links: <a href="/" target="_blank">No Rel</a></p>";}'; - $expected = 'a:1:{s:4:"html";s:52:"<p>Links: <a href="/" target="_blank">No Rel</a></p>";}'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - - public function test_wp_targeted_link_rel_tab_separated_values_are_split() { - $content = "<p>Links: <a href=\"/\" target=\"_blank\" rel=\"ugc\t\tnoopener\t\">No rel</a></p>"; - $expected = '<p>Links: <a href="/" target="_blank" rel="ugc noopener">No rel</a></p>'; - $this->assertSame( $expected, wp_targeted_link_rel( $content ) ); - } - -} diff --git a/tests/phpunit/tests/formatting/wpTexturize.php b/tests/phpunit/tests/formatting/wpTexturize.php index 8ac9bb378477b..fa81245c83bb9 100644 --- a/tests/phpunit/tests/formatting/wpTexturize.php +++ b/tests/phpunit/tests/formatting/wpTexturize.php @@ -2,8 +2,11 @@ /** * @group formatting + * + * @covers ::wptexturize */ class Tests_Formatting_wpTexturize extends WP_UnitTestCase { + public function test_dashes() { $this->assertSame( 'Hey — boo?', wptexturize( 'Hey -- boo?' ) ); $this->assertSame( '<a href="http://xx--xx">Hey — boo?</a>', wptexturize( '<a href="http://xx--xx">Hey -- boo?</a>' ) ); @@ -29,7 +32,6 @@ public function test_disable() { $invalid_nest = '<pre></code>"baba"</pre>'; $this->assertSame( $invalid_nest, wptexturize( $invalid_nest ) ); - } /** @@ -266,7 +268,7 @@ public function test_spaces_around_quotes_never() { * @dataProvider data_spaces_around_quotes */ public function test_spaces_around_quotes( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_spaces_around_quotes() { @@ -322,7 +324,7 @@ public function data_spaces_around_quotes() { * @dataProvider data_apos_before_digits */ public function test_apos_before_digits( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_apos_before_digits() { @@ -363,7 +365,7 @@ public function data_apos_before_digits() { * @dataProvider data_opening_single_quote */ public function test_opening_single_quote( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_opening_single_quote() { @@ -492,7 +494,7 @@ public function data_opening_single_quote() { * @dataProvider data_double_prime */ public function test_double_prime( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_double_prime() { @@ -525,7 +527,7 @@ public function data_double_prime() { * @dataProvider data_single_prime */ public function test_single_prime( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_single_prime() { @@ -558,7 +560,7 @@ public function data_single_prime() { * @dataProvider data_contractions */ public function test_contractions( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_contractions() { @@ -599,7 +601,7 @@ public function data_contractions() { * @dataProvider data_opening_quote */ public function test_opening_quote( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_opening_quote() { @@ -676,7 +678,7 @@ public function data_opening_quote() { * @dataProvider data_closing_quote */ public function test_closing_quote( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_closing_quote() { @@ -765,7 +767,7 @@ public function data_closing_quote() { * @dataProvider data_closing_single_quote */ public function test_closing_single_quote( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_closing_single_quote() { @@ -855,7 +857,7 @@ public function data_closing_single_quote() { * @dataProvider data_multiplication */ public function test_multiplication( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_multiplication() { @@ -905,7 +907,7 @@ public function data_multiplication() { * @dataProvider data_ampersand */ public function test_ampersand( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_ampersand() { @@ -970,7 +972,7 @@ public function data_ampersand() { * @dataProvider data_cockney */ public function test_cockney( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_cockney() { @@ -1031,7 +1033,7 @@ public function data_cockney() { * @dataProvider data_smart_dashes */ public function test_smart_dashes( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_smart_dashes() { @@ -1084,7 +1086,7 @@ public function data_smart_dashes() { * @dataProvider data_misc_static_replacements */ public function test_misc_static_replacements( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_misc_static_replacements() { @@ -1139,7 +1141,7 @@ public function data_misc_static_replacements() { * @dataProvider data_quoted_numbers */ public function test_quoted_numbers( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_quoted_numbers() { @@ -1190,7 +1192,7 @@ public function data_quoted_numbers() { * @dataProvider data_quotes_and_dashes */ public function test_quotes_and_dashes( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_quotes_and_dashes() { @@ -1253,7 +1255,7 @@ public function data_quotes_and_dashes() { * @dataProvider data_tag_avoidance */ public function test_tag_avoidance( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_tag_avoidance() { @@ -1476,7 +1478,7 @@ public function data_tag_avoidance() { * @dataProvider data_year_abbr */ public function test_year_abbr( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_year_abbr() { @@ -1572,7 +1574,7 @@ public function test_translate( $input, $output ) { remove_filter( 'gettext_with_context', array( $this, 'filter_translate' ), 10, 4 ); wptexturize( 'reset', true ); - return $this->assertSame( $output, $result ); + $this->assertSame( $output, $result ); } public function filter_translate( $translations, $text, $context, $domain ) { @@ -1786,13 +1788,13 @@ public function data_translate() { } /** - * Extra sanity checks for _wptexturize_pushpop_element() + * Extra confidence checks for _wptexturize_pushpop_element() * * @ticket 28483 * @dataProvider data_element_stack */ public function test_element_stack( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_element_stack() { @@ -1928,7 +1930,7 @@ public function data_unregistered_shortcodes() { * @dataProvider data_primes_vs_quotes */ public function test_primes_vs_quotes( $input, $output ) { - return $this->assertSame( $output, wptexturize( $input ) ); + $this->assertSame( $output, wptexturize( $input ) ); } public function data_primes_vs_quotes() { @@ -1997,7 +1999,7 @@ public function test_primes_quotes_translation( $input, $output ) { remove_filter( 'gettext_with_context', array( $this, 'filter_translate2' ), 10, 4 ); wptexturize( 'reset', true ); - return $this->assertSame( $output, $result ); + $this->assertSame( $output, $result ); } public function filter_translate2( $translations, $text, $context, $domain ) { @@ -2081,6 +2083,9 @@ public function data_primes_quotes_translation() { * Automated performance testing of the main regex. * * @dataProvider data_whole_posts + * + * @covers ::_get_wptexturize_split_regex + * @covers ::_get_wptexturize_shortcode_regex */ public function test_pcre_performance( $input ) { global $shortcode_tags; diff --git a/tests/phpunit/tests/formatting/wpTrimExcerpt.php b/tests/phpunit/tests/formatting/wpTrimExcerpt.php index ed5117bb56073..688ba7731f1ce 100644 --- a/tests/phpunit/tests/formatting/wpTrimExcerpt.php +++ b/tests/phpunit/tests/formatting/wpTrimExcerpt.php @@ -2,6 +2,7 @@ /** * @group formatting + * * @covers ::wp_trim_excerpt */ class Tests_Formatting_wpTrimExcerpt extends WP_UnitTestCase { @@ -28,11 +29,12 @@ public function test_secondary_loop_respect_more() { 'post__in' => array( $post2 ), ) ); - if ( $q->have_posts() ) { - while ( $q->have_posts() ) { - $q->the_post(); - $this->assertSame( 'Post 2 Page 1', wp_trim_excerpt() ); - } + + $this->assertTrue( $q->have_posts() ); + + while ( $q->have_posts() ) { + $q->the_post(); + $this->assertSame( 'Post 2 Page 1', wp_trim_excerpt() ); } } @@ -59,11 +61,12 @@ public function test_secondary_loop_respect_nextpage() { 'post__in' => array( $post2 ), ) ); - if ( $q->have_posts() ) { - while ( $q->have_posts() ) { - $q->the_post(); - $this->assertSame( 'Post 2 Page 1', wp_trim_excerpt() ); - } + + $this->assertTrue( $q->have_posts() ); + + while ( $q->have_posts() ) { + $q->the_post(); + $this->assertSame( 'Post 2 Page 1', wp_trim_excerpt() ); } } @@ -91,4 +94,133 @@ public function test_should_generate_excerpt_for_empty_values() { $this->assertSame( 'Post content', wp_trim_excerpt( null, $post ) ); $this->assertSame( 'Post content', wp_trim_excerpt( false, $post ) ); } + + /** + * Tests that `wp_trim_excerpt()` unhooks `wp_filter_content_tags()` from 'the_content' filter. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_unhooks_wp_filter_content_tags() { + $post = self::factory()->post->create(); + + /* + * Record that during 'the_content' filter run by wp_trim_excerpt() the + * wp_filter_content_tags() callback is not used. + */ + $has_filter = true; + add_filter( + 'the_content', + static function ( $content ) use ( &$has_filter ) { + $has_filter = has_filter( 'the_content', 'wp_filter_content_tags' ); + return $content; + } + ); + + wp_trim_excerpt( '', $post ); + + $this->assertFalse( $has_filter, 'wp_filter_content_tags() was not unhooked in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't permanently unhook `wp_filter_content_tags()` from 'the_content' filter. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_should_not_permanently_unhook_wp_filter_content_tags() { + $post = self::factory()->post->create(); + + wp_trim_excerpt( '', $post ); + + $this->assertSame( 12, has_filter( 'the_content', 'wp_filter_content_tags' ), 'wp_filter_content_tags() was not restored in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't restore `wp_filter_content_tags()` if it was previously unhooked. + * + * @ticket 56588 + */ + public function test_wp_trim_excerpt_does_not_restore_wp_filter_content_tags_if_previously_unhooked() { + $post = self::factory()->post->create(); + + // Remove wp_filter_content_tags() from 'the_content' filter generally. + remove_filter( 'the_content', 'wp_filter_content_tags', 12 ); + + wp_trim_excerpt( '', $post ); + + // Assert that the filter callback was not restored after running 'the_content'. + $this->assertFalse( has_filter( 'the_content', 'wp_filter_content_tags' ) ); + } + + /** + * Tests that `wp_trim_excerpt()` does process valid blocks. + * + * @ticket 58682 + */ + public function test_wp_trim_excerpt_check_if_block_renders() { + $post = self::factory()->post->create( + array( + 'post_content' => '<!-- wp:paragraph --> <p>A test paragraph</p> <!-- /wp:paragraph -->', + ) + ); + + $output_text = wp_trim_excerpt( '', $post ); + + $this->assertSame( 'A test paragraph', $output_text, 'wp_trim_excerpt() did not process paragraph block.' ); + } + + /** + * Tests that `wp_trim_excerpt()` unhooks `do_blocks()` from 'the_content' filter. + * + * @ticket 58682 + */ + public function test_wp_trim_excerpt_unhooks_do_blocks() { + $post = self::factory()->post->create(); + + /* + * Record that during 'the_content' filter run by wp_trim_excerpt() the + * do_blocks() callback is not used. + */ + $has_filter = true; + add_filter( + 'the_content', + static function ( $content ) use ( &$has_filter ) { + $has_filter = has_filter( 'the_content', 'do_blocks' ); + return $content; + } + ); + + wp_trim_excerpt( '', $post ); + + $this->assertFalse( $has_filter, 'do_blocks() was not unhooked in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't permanently unhook `do_blocks()` from 'the_content' filter. + * + * @ticket 58682 + */ + public function test_wp_trim_excerpt_should_not_permanently_unhook_do_blocks() { + $post = self::factory()->post->create(); + + wp_trim_excerpt( '', $post ); + + $this->assertSame( 9, has_filter( 'the_content', 'do_blocks' ), 'do_blocks() was not restored in wp_trim_excerpt()' ); + } + + /** + * Tests that `wp_trim_excerpt()` doesn't restore `do_blocks()` if it was previously unhooked. + * + * @ticket 58682 + */ + public function test_wp_trim_excerpt_does_not_restore_do_blocks_if_previously_unhooked() { + $post = self::factory()->post->create(); + + // Remove do_blocks() from 'the_content' filter generally. + remove_filter( 'the_content', 'do_blocks', 9 ); + + wp_trim_excerpt( '', $post ); + + // Assert that the filter callback was not restored after running 'the_content'. + $this->assertFalse( has_filter( 'the_content', 'do_blocks' ) ); + } } diff --git a/tests/phpunit/tests/formatting/wpTrimWords.php b/tests/phpunit/tests/formatting/wpTrimWords.php index 1becf2b5d2093..b918fd1642b4b 100644 --- a/tests/phpunit/tests/formatting/wpTrimWords.php +++ b/tests/phpunit/tests/formatting/wpTrimWords.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::wp_trim_words */ class Tests_Formatting_wpTrimWords extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/formatting/zeroise.php b/tests/phpunit/tests/formatting/zeroise.php index c702fc4eb9f1c..fb8f49897b69a 100644 --- a/tests/phpunit/tests/formatting/zeroise.php +++ b/tests/phpunit/tests/formatting/zeroise.php @@ -2,6 +2,8 @@ /** * @group formatting + * + * @covers ::zeroise */ class Tests_Formatting_Zeroise extends WP_UnitTestCase { public function test_pads_with_leading_zeroes() { diff --git a/tests/phpunit/tests/functions.php b/tests/phpunit/tests/functions.php index bcf852521657a..b6080da780ba8 100644 --- a/tests/phpunit/tests/functions.php +++ b/tests/phpunit/tests/functions.php @@ -1,11 +1,11 @@ <?php /** - * @group functions.php + * @group functions */ class Tests_Functions extends WP_UnitTestCase { public function test_wp_parse_args_object() { - $x = new MockClass; + $x = new MockClass(); $x->_baba = 5; $x->yZ = 'baba'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $x->a = array( 5, 111, 'x' ); @@ -17,7 +17,7 @@ public function test_wp_parse_args_object() { ), wp_parse_args( $x ) ); - $y = new MockClass; + $y = new MockClass(); $this->assertSame( array(), wp_parse_args( $y ) ); } @@ -41,7 +41,7 @@ public function test_wp_parse_args_array() { } public function test_wp_parse_args_defaults() { - $x = new MockClass; + $x = new MockClass(); $x->_baba = 5; $x->yZ = 'baba'; // phpcs:ignore WordPress.NamingConventions.ValidVariableName.UsedPropertyNotSnakeCase $x->a = array( 5, 111, 'x' ); @@ -129,6 +129,68 @@ public function test_path_is_not_absolute() { } } + /** + * Tests path_join(). + * + * @ticket 55897 + * @dataProvider data_path_join + */ + public function test_path_join( $base, $path, $expected ) { + $this->assertSame( $expected, path_join( $base, $path ) ); + } + + /** + * Data provider for test_path_join(). + * + * @return string[][] + */ + public function data_path_join() { + return array( + // Absolute paths. + 'absolute path should return path' => array( + 'base' => 'base', + 'path' => '/path', + 'expected' => '/path', + ), + 'windows path with slashes' => array( + 'base' => 'base', + 'path' => '//path', + 'expected' => '//path', + ), + 'windows path with backslashes' => array( + 'base' => 'base', + 'path' => '\\\\path', + 'expected' => '\\\\path', + ), + // Non-absolute paths. + 'join base and path' => array( + 'base' => 'base', + 'path' => 'path', + 'expected' => 'base/path', + ), + 'strip trailing slashes in base' => array( + 'base' => 'base///', + 'path' => 'path', + 'expected' => 'base/path', + ), + 'empty path' => array( + 'base' => 'base', + 'path' => '', + 'expected' => 'base/', + ), + 'empty base' => array( + 'base' => '', + 'path' => 'path', + 'expected' => '/path', + ), + 'empty path and base' => array( + 'base' => '', + 'path' => '', + 'expected' => '/', + ), + ); + } + /** * @ticket 33265 * @ticket 35996 @@ -160,14 +222,71 @@ public function data_wp_normalize_path() { array( 'php://input', 'php://input' ), array( 'http://example.com//path.ext', 'http://example.com/path.ext' ), array( 'file://c:\\www\\path\\', 'file://C:/www/path/' ), + + // Edge cases. + array( '', '' ), // Empty string should return empty string. + array( 123, '123' ), // Integer should be cast to string. ); } + /** + * Tests that wp_normalize_path() works with objects that have __toString(). + * + * This is important because the function uses a static cache, and the input + * must be cast to string before being used as an array key. + * + * @ticket 64538 + */ + public function test_wp_normalize_path_with_stringable_object() { + $file_info = new SplFileInfo( '/var/www/html\\test' ); + + $this->assertSame( '/var/www/html/test', wp_normalize_path( $file_info ) ); + } + + /** + * Tests that wp_normalize_path() returns consistent results on repeated calls. + * + * The function uses a static cache, so this verifies cache behavior. + * + * @ticket 64538 + */ + public function test_wp_normalize_path_returns_consistent_results() { + $path = 'C:\\www\\path\\'; + + $first_call = wp_normalize_path( $path ); + $second_call = wp_normalize_path( $path ); + $third_call = wp_normalize_path( $path ); + + $this->assertSame( $first_call, $second_call, 'Second call should return same result as first.' ); + $this->assertSame( $second_call, $third_call, 'Third call should return same result as second.' ); + $this->assertSame( 'C:/www/path/', $first_call, 'Normalized path should match expected value.' ); + } + + /** + * Tests that wp_normalize_path() static cache stores results. + * + * @ticket 64538 + */ + public function test_wp_normalize_path_static_cache() { + $path = '/var/www/cache-test\\subdir\\'; + $expected = '/var/www/cache-test/subdir/'; + + $result = wp_normalize_path( $path ); + $this->assertSame( $expected, $result ); + + $reflection = new ReflectionFunction( 'wp_normalize_path' ); + $static_vars = $reflection->getStaticVariables(); + + $this->assertArrayHasKey( 'cache', $static_vars, 'Static cache array should exist.' ); + $this->assertArrayHasKey( $path, $static_vars['cache'], 'Cache should contain the normalized path.' ); + $this->assertSame( $expected, $static_vars['cache'][ $path ], 'Cached value should match the expected normalized path.' ); + } + public function test_wp_unique_filename() { $testdir = DIR_TESTDATA . '/images/'; - // Sanity check. + // Confidence check. $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcdefg.png' ), 'Test non-existing file, file name should be unchanged.' ); // Ensure correct images exist. @@ -196,12 +315,12 @@ public function test_wp_unique_filename() { $this->assertSame( 'abcdefgh.png', wp_unique_filename( $testdir, 'abcdefg"h.png' ), 'File with quote failed' ); // Test crazy name (useful for regression tests). - $this->assertSame( '12af34567890@..^_qwerty-fghjkl-zx.png', wp_unique_filename( $testdir, '12%af34567890#~!@#$..%^&*()|_+qwerty fgh`jkl zx<>?:"{}[]="\'/?.png' ), 'Failed crazy file name' ); + $this->assertSame( '12af34567890@.^_qwerty-fghjkl-zx.png', wp_unique_filename( $testdir, '12%af34567890#~!@#$..%^&*()|_+qwerty fgh`jkl zx<>?:"{}[]="\'/?.png' ), 'Failed crazy file name' ); // Test slashes in names. $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcde\fg.png' ), 'Slash not removed' ); $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcde\\fg.png' ), 'Double slashed not removed' ); - $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcde\\\fg.png' ), 'Tripple slashed not removed' ); + $this->assertSame( 'abcdefg.png', wp_unique_filename( $testdir, 'abcde\\\fg.png' ), 'Triple slashed not removed' ); } /** @@ -311,152 +430,6 @@ public function image_editor_output_format_handler( $formats ) { return $formats; } - /** - * @dataProvider data_is_not_serialized - */ - public function test_maybe_serialize( $value ) { - if ( is_array( $value ) || is_object( $value ) ) { - $expected = serialize( $value ); - } else { - $expected = $value; - } - - $this->assertSame( $expected, maybe_serialize( $value ) ); - } - - /** - * @dataProvider data_is_serialized - */ - public function test_maybe_serialize_with_double_serialization( $value ) { - $expected = serialize( $value ); - - $this->assertSame( $expected, maybe_serialize( $value ) ); - } - - /** - * @dataProvider data_is_serialized - * @dataProvider data_is_not_serialized - */ - public function test_maybe_unserialize( $value, $is_serialized ) { - if ( $is_serialized ) { - $expected = unserialize( trim( $value ) ); - } else { - $expected = $value; - } - - if ( is_object( $expected ) ) { - $this->assertEquals( $expected, maybe_unserialize( $value ) ); - } else { - $this->assertSame( $expected, maybe_unserialize( $value ) ); - } - } - - /** - * @dataProvider data_is_serialized - * @dataProvider data_is_not_serialized - */ - public function test_is_serialized( $value, $expected ) { - $this->assertSame( $expected, is_serialized( $value ) ); - } - - /** - * @dataProvider data_serialize_deserialize_objects - */ - public function test_deserialize_request_utility_filtered_iterator_objects( $value ) { - $serialized = maybe_serialize( $value ); - if ( get_class( $value ) === 'Requests_Utility_FilteredIterator' ) { - $new_value = unserialize( $serialized ); - $property = ( new ReflectionClass( 'Requests_Utility_FilteredIterator' ) )->getProperty( 'callback' ); - $property->setAccessible( true ); - $callback_value = $property->getValue( $new_value ); - $this->assertSame( null, $callback_value ); - } else { - $this->assertSame( $value->count(), unserialize( $serialized )->count() ); - } - } - - public function data_serialize_deserialize_objects() { - return array( - array( new Requests_Utility_FilteredIterator( array( 1 ), 'md5' ) ), - array( new Requests_Utility_FilteredIterator( array( 1, 2 ), 'sha1' ) ), - array( new ArrayIterator( array( 1, 2, 3 ) ) ), - ); - } - - public function data_is_serialized() { - return array( - array( serialize( null ), true ), - array( serialize( true ), true ), - array( serialize( false ), true ), - array( serialize( -25 ), true ), - array( serialize( 25 ), true ), - array( serialize( 1.1 ), true ), - array( serialize( 'this string will be serialized' ), true ), - array( serialize( "a\nb" ), true ), - array( serialize( array() ), true ), - array( serialize( array( 1, 1, 2, 3, 5, 8, 13 ) ), true ), - array( - serialize( - (object) array( - 'test' => true, - '3', - 4, - ) - ), - true, - ), - array( ' s:25:"this string is serialized"; ', true ), - ); - } - - public function data_is_not_serialized() { - return array( - array( null, false ), - array( true, false ), - array( false, false ), - array( -25, false ), - array( 25, false ), - array( 1.1, false ), - array( 'this string will be serialized', false ), - array( "a\nb", false ), - array( array(), false ), - array( array( 1, 1, 2, 3, 5, 8, 13 ), false ), - array( - (object) array( - 'test' => true, - '3', - 4, - ), - false, - ), - array( 'a string', false ), - array( 'garbage:a:0:garbage;', false ), - array( 's:4:test;', false ), - ); - } - - /** - * @ticket 46570 - * @dataProvider data_is_serialized_should_return_true_for_large_floats - */ - public function test_is_serialized_should_return_true_for_large_floats( $value ) { - $this->assertTrue( is_serialized( $value ) ); - } - - public function data_is_serialized_should_return_true_for_large_floats() { - return array( - array( serialize( 1.7976931348623157E+308 ) ), - array( serialize( array( 1.7976931348623157E+308, 1.23e50 ) ) ), - ); - } - - /** - * @ticket 17375 - */ - public function test_no_new_serializable_types() { - $this->assertFalse( is_serialized( 'C:16:"Serialized_Class":6:{a:0:{}}' ) ); - } - /** * @group add_query_arg */ @@ -665,6 +638,62 @@ public function test_add_query_arg_numeric_keys() { $this->assertSame( 'foo=bar&1=2', $url ); } + /** + * Tests that add_query_arg removes the question mark when + * a parameter is set to false. + * + * @dataProvider data_add_query_arg_removes_question_mark + * + * @ticket 44499 + * @group add_query_arg + * + * @covers ::add_query_arg + * + * @param string $url Url to test. + * @param string $expected Expected URL. + */ + public function test_add_query_arg_removes_question_mark( $url, $expected, $key = 'param', $value = false ) { + $this->assertSame( $expected, add_query_arg( $key, $value, $url ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_add_query_arg_removes_question_mark() { + return array( + 'anchor' => array( + 'url' => 'http://example.org?#anchor', + 'expected' => 'http://example.org#anchor', + ), + '/ then anchor' => array( + 'url' => 'http://example.org/?#anchor', + 'expected' => 'http://example.org/#anchor', + ), + 'invalid query param and anchor' => array( + 'url' => 'http://example.org?param=value#anchor', + 'expected' => 'http://example.org#anchor', + ), + '/ then invalid query param and anchor' => array( + 'url' => 'http://example.org/?param=value#anchor', + 'expected' => 'http://example.org/#anchor', + ), + '?#anchor when adding valid key/value args' => array( + 'url' => 'http://example.org?#anchor', + 'expected' => 'http://example.org?foo=bar#anchor', + 'key' => 'foo', + 'value' => 'bar', + ), + '/?#anchor when adding valid key/value args' => array( + 'url' => 'http://example.org/?#anchor', + 'expected' => 'http://example.org/?foo=bar#anchor', + 'key' => 'foo', + 'value' => 'bar', + ), + ); + } + /** * @ticket 21594 */ @@ -754,65 +783,6 @@ public function test_canonical_charset() { update_option( 'blog_charset', $orig_blog_charset ); } - /** - * @ticket 43977 - * @dataProvider data_wp_parse_list - */ - public function test_wp_parse_list( $expected, $actual ) { - $this->assertSame( $expected, array_values( wp_parse_list( $actual ) ) ); - } - - public function data_wp_parse_list() { - return array( - array( array( '1', '2', '3', '4' ), '1,2,3,4' ), - array( array( 'apple', 'banana', 'carrot', 'dog' ), 'apple,banana,carrot,dog' ), - array( array( '1', '2', 'apple', 'banana' ), '1,2,apple,banana' ), - array( array( '1', '2', 'apple', 'banana' ), '1, 2,apple,banana' ), - array( array( '1', '2', 'apple', 'banana' ), '1,2,apple,,banana' ), - array( array( '1', '2', 'apple', 'banana' ), ',1,2,apple,banana' ), - array( array( '1', '2', 'apple', 'banana' ), '1,2,apple,banana,' ), - array( array( '1', '2', 'apple', 'banana' ), '1,2 ,apple,banana' ), - array( array(), '' ), - array( array(), ',' ), - array( array(), ',,' ), - ); - } - - /** - * @dataProvider data_wp_parse_id_list - */ - public function test_wp_parse_id_list( $expected, $actual ) { - $this->assertSame( $expected, array_values( wp_parse_id_list( $actual ) ) ); - } - - public function data_wp_parse_id_list() { - return array( - array( array( 1, 2, 3, 4 ), '1,2,3,4' ), - array( array( 1, 2, 3, 4 ), '1, 2,,3,4' ), - array( array( 1, 2, 3, 4 ), '1,2,2,3,4' ), - array( array( 1, 2, 3, 4 ), array( '1', '2', '3', '4', '3' ) ), - array( array( 1, 2, 3, 4 ), array( 1, '2', 3, '4' ) ), - array( array( 1, 2, 3, 4 ), '-1,2,-3,4' ), - array( array( 1, 2, 3, 4 ), array( -1, 2, '-3', '4' ) ), - ); - } - - /** - * @dataProvider data_wp_parse_slug_list - */ - public function test_wp_parse_slug_list( $expected, $actual ) { - $this->assertSame( $expected, array_values( wp_parse_slug_list( $actual ) ) ); - } - - public function data_wp_parse_slug_list() { - return array( - array( array( 'apple', 'banana', 'carrot', 'dog' ), 'apple,banana,carrot,dog' ), - array( array( 'apple', 'banana', 'carrot', 'dog' ), 'apple, banana,,carrot,dog' ), - array( array( 'apple', 'banana', 'carrot', 'dog' ), 'apple banana carrot dog' ), - array( array( 'apple', 'banana-carrot', 'd-o-g' ), array( 'apple ', 'banana carrot', 'd o g' ) ), - ); - } - /** * @dataProvider data_device_can_upload */ @@ -934,6 +904,10 @@ public function test_wp_extract_urls() { 'ftp://127.0.0.1/', 'http://www.woo.com/video?v=exvUH2qKLTU', 'http://taco.com?burrito=enchilada#guac', + 'http://example.org/?post_type=post&p=4', + 'http://example.org/?post_type=post&p=5', + 'http://example.org/?post_type=post&p=6', + 'http://typo-in-query.org/?foo=bar&baz=missing_semicolon', ); $blob = ' @@ -996,6 +970,12 @@ public function test_wp_extract_urls() { http://www.woo.com/video?v=exvUH2qKLTU http://taco.com?burrito=enchilada#guac + + http://example.org/?post_type=post&p=4 + http://example.org/?post_type=post&p=5 + http://example.org/?post_type=post&p=6 + + http://typo-in-query.org/?foo=bar&baz=missing_semicolon '; $urls = wp_extract_urls( $blob ); @@ -1040,6 +1020,28 @@ public function test_wp_extract_urls() { $this->assertSame( array_slice( $original_urls, 0, 8 ), $urls ); } + /** + * Tests for backward compatibility of `wp_extract_urls` to remove unused semicolons. + * + * @ticket 30580 + * + * @covers ::wp_extract_urls + */ + public function test_wp_extract_urls_remove_semicolon() { + $expected = array( + 'http://typo.com', + 'http://example.org/?post_type=post&p=8', + ); + $actual = wp_extract_urls( + ' + http://typo.com; + http://example.org/?post_type=;p;o;s;t;&p=8; + ' + ); + + $this->assertSame( $expected, $actual ); + } + /** * @ticket 28786 */ @@ -1109,7 +1111,7 @@ public function test_wp_json_encode_array() { * @ticket 28786 */ public function test_wp_json_encode_object() { - $object = new stdClass; + $object = new stdClass(); $object->a = 'b'; $this->assertSame( wp_json_encode( $object ), '{"a":"b"}' ); } @@ -1154,7 +1156,7 @@ public function test_wp_json_file_decode_associative_array() { /** * @ticket 36054 - * @dataProvider datetime_provider + * @dataProvider data_mysql_to_rfc3339 */ public function test_mysql_to_rfc3339( $expected, $actual ) { $date_return = mysql_to_rfc3339( $actual ); @@ -1165,7 +1167,7 @@ public function test_mysql_to_rfc3339( $expected, $actual ) { $this->assertEquals( new DateTime( $expected ), new DateTime( $date_return ), 'The date is not the same after the call method' ); } - public function datetime_provider() { + public function data_mysql_to_rfc3339() { return array( array( '2016-03-15T18:54:46', '15-03-2016 18:54:46' ), array( '2016-03-02T19:13:25', '2016-03-02 19:13:25' ), @@ -1200,6 +1202,8 @@ public function test_wp_get_ext_types() { public function test_wp_ext2type() { $extensions = wp_get_ext_types(); + $this->assertNotEmpty( $extensions ); + foreach ( $extensions as $type => $extension_list ) { foreach ( $extension_list as $extension ) { $this->assertSame( $type, wp_ext2type( $extension ) ); @@ -1330,106 +1334,24 @@ public function test_wp_unique_id() { /** * @ticket 40017 - * @dataProvider wp_get_image_mime + * @dataProvider data_wp_get_image_mime */ public function test_wp_get_image_mime( $file, $expected ) { if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) { $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); } - $this->assertSame( $expected, wp_get_image_mime( $file ) ); - } - - /** - * @ticket 35725 - * @dataProvider data_wp_getimagesize - */ - public function test_wp_getimagesize( $file, $expected ) { - if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) { - $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); - } - - $result = wp_getimagesize( $file ); - - // The getimagesize() function varies in its response, so - // let's restrict comparison to expected keys only. if ( is_array( $expected ) ) { - foreach ( $expected as $k => $v ) { - $this->assertArrayHasKey( $k, $result ); - $this->assertSame( $expected[ $k ], $result[ $k ] ); - } + $this->assertContains( wp_get_image_mime( $file ), $expected ); } else { - $this->assertSame( $expected, $result ); + $this->assertSame( $expected, wp_get_image_mime( $file ) ); } } - /** - * @ticket 39550 - * @dataProvider wp_check_filetype_and_ext_data - * @requires extension fileinfo - */ - public function test_wp_check_filetype_and_ext( $file, $filename, $expected ) { - $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); - } - - /** - * @ticket 39550 - * @group ms-excluded - * @requires extension fileinfo - */ - public function test_wp_check_filetype_and_ext_with_filtered_svg() { - $file = DIR_TESTDATA . '/uploads/video-play.svg'; - $filename = 'video-play.svg'; - - $expected = array( - 'ext' => 'svg', - 'type' => 'image/svg+xml', - 'proper_filename' => false, - ); - - add_filter( 'upload_mimes', array( $this, 'filter_mime_types_svg' ) ); - $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); - - // Cleanup. - remove_filter( 'upload_mimes', array( $this, 'filter_mime_types_svg' ) ); - } - - /** - * @ticket 39550 - * @group ms-excluded - * @requires extension fileinfo - */ - public function test_wp_check_filetype_and_ext_with_filtered_woff() { - $file = DIR_TESTDATA . '/uploads/dashicons.woff'; - $filename = 'dashicons.woff'; - - $expected = array( - 'ext' => 'woff', - 'type' => 'application/font-woff', - 'proper_filename' => false, - ); - - add_filter( 'upload_mimes', array( $this, 'filter_mime_types_woff' ) ); - $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); - - // Cleanup. - remove_filter( 'upload_mimes', array( $this, 'filter_mime_types_woff' ) ); - } - - public function filter_mime_types_svg( $mimes ) { - $mimes['svg'] = 'image/svg+xml'; - return $mimes; - } - - public function filter_mime_types_woff( $mimes ) { - $mimes['woff'] = 'application/font-woff'; - return $mimes; - } - /** * Data provider for test_wp_get_image_mime(). */ - public function wp_get_image_mime() { + public function data_wp_get_image_mime() { $data = array( // Standard JPEG. array( @@ -1476,13 +1398,62 @@ public function wp_get_image_mime() { DIR_TESTDATA . '/uploads/dashicons.woff', false, ), + // Animated AVIF. + array( + DIR_TESTDATA . '/images/avif-animated.avif', + 'image/avif', + ), + // Lossless AVIF. + array( + DIR_TESTDATA . '/images/avif-lossless.avif', + 'image/avif', + ), + // Lossy AVIF. + array( + DIR_TESTDATA . '/images/avif-lossy.avif', + 'image/avif', + ), + // Transparent AVIF. + array( + DIR_TESTDATA . '/images/avif-transparent.avif', + 'image/avif', + ), + // HEIC. + array( + DIR_TESTDATA . '/images/test-image.heic', + // In PHP 8.5, it returns 'image/heif'. Before that, it returns 'image/heic'. + array( 'image/heic', 'image/heif' ), + ), ); return $data; } /** - * Data profider for test_wp_getimagesize(). + * @ticket 35725 + * @dataProvider data_wp_getimagesize + */ + public function test_wp_getimagesize( $file, $expected ) { + if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) { + $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); + } + + $result = wp_getimagesize( $file ); + + // The getimagesize() function varies in its response, so + // let's restrict comparison to expected keys only. + if ( is_array( $expected ) ) { + foreach ( $expected as $k => $v ) { + $this->assertArrayHasKey( $k, $result ); + $this->assertSame( $expected[ $k ], $result[ $k ] ); + } + } else { + $this->assertSame( $expected, $result ); + } + } + + /** + * Data provider for test_wp_getimagesize(). */ public function data_wp_getimagesize() { $data = array( @@ -1579,12 +1550,121 @@ public function data_wp_getimagesize() { DIR_TESTDATA . '/uploads/dashicons.woff', false, ), + // Animated AVIF. + array( + DIR_TESTDATA . '/images/avif-animated.avif', + array( + 150, + 150, + IMAGETYPE_AVIF, + 'width="150" height="150"', + 'mime' => 'image/avif', + ), + ), + // Lossless AVIF. + array( + DIR_TESTDATA . '/images/avif-lossless.avif', + array( + 400, + 400, + IMAGETYPE_AVIF, + 'width="400" height="400"', + 'mime' => 'image/avif', + ), + ), + // Lossy AVIF. + array( + DIR_TESTDATA . '/images/avif-lossy.avif', + array( + 400, + 400, + IMAGETYPE_AVIF, + 'width="400" height="400"', + 'mime' => 'image/avif', + ), + ), + // Transparent AVIF. + array( + DIR_TESTDATA . '/images/avif-transparent.avif', + array( + 128, + 128, + IMAGETYPE_AVIF, + 'width="128" height="128"', + 'mime' => 'image/avif', + ), + ), + // Grid AVIF. + array( + DIR_TESTDATA . '/images/avif-alpha-grid2x1.avif', + array( + 199, + 200, + IMAGETYPE_AVIF, + 'width="199" height="200"', + 'mime' => 'image/avif', + ), + ), ); return $data; } - public function wp_check_filetype_and_ext_data() { + /** + * Tests that wp_getimagesize() correctly handles HEIC image files. + * + * @ticket 53645 + */ + public function test_wp_getimagesize_heic() { + if ( ! is_callable( 'exif_imagetype' ) && ! function_exists( 'getimagesize' ) ) { + $this->markTestSkipped( 'The exif PHP extension is not loaded.' ); + } + + $file = DIR_TESTDATA . '/images/test-image.heic'; + + $editor = wp_get_image_editor( $file ); + if ( is_wp_error( $editor ) || ! $editor->supports_mime_type( 'image/heic' ) ) { + $this->markTestSkipped( 'No HEIC support in the editor engine on this system.' ); + } + + $expected = array( + 1180, + 1180, + IMAGETYPE_HEIF, + 'width="1180" height="1180"', + ); + + // As of PHP 8.5.0, getimagesize() supports HEIF/HEIC files. + if ( PHP_VERSION_ID >= 80500 ) { + $expected = array_merge( + $expected, + array( + 'bits' => 8, + 'channels' => 3, + 'mime' => 'image/heif', + 'width_unit' => 'px', + 'height_unit' => 'px', + ) + ); + } else { + $expected['mime'] = 'image/heic'; + } + + $result = wp_getimagesize( $file ); + $this->assertSame( $expected, $result ); + } + + + /** + * @ticket 39550 + * @dataProvider data_wp_check_filetype_and_ext + * @requires extension fileinfo + */ + public function test_wp_check_filetype_and_ext( $file, $filename, $expected ) { + $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + } + + public function data_wp_check_filetype_and_ext() { $data = array( // Standard image. array( @@ -1693,6 +1773,16 @@ public function wp_check_filetype_and_ext_data() { 'proper_filename' => false, ), ), + // Google Docs file for which finfo_file() returns a duplicate mime type. + array( + DIR_TESTDATA . '/uploads/double-mime-type.docx', + 'double-mime-type.docx', + array( + 'ext' => 'docx', + 'type' => 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + 'proper_filename' => false, + ), + ), // Non-image file with wrong sub-type. array( DIR_TESTDATA . '/uploads/pages-to-word.docx', @@ -1749,11 +1839,76 @@ public function wp_check_filetype_and_ext_data() { return $data; } + /** + * @ticket 39550 + * @group ms-excluded + * @requires extension fileinfo + */ + public function test_wp_check_filetype_and_ext_with_filtered_svg() { + $file = DIR_TESTDATA . '/uploads/video-play.svg'; + $filename = 'video-play.svg'; + + $expected = array( + 'ext' => 'svg', + 'type' => 'image/svg+xml', + 'proper_filename' => false, + ); + + add_filter( + 'upload_mimes', + static function ( $mimes ) { + $mimes['svg'] = 'image/svg+xml'; + return $mimes; + } + ); + + $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + } + + /** + * @ticket 39550 + * @group ms-excluded + * @requires extension fileinfo + */ + public function test_wp_check_filetype_and_ext_with_filtered_woff() { + $file = DIR_TESTDATA . '/uploads/dashicons.woff'; + $filename = 'dashicons.woff'; + + $woff_mime_type = 'application/font-woff'; + + /* + * As of PHP 8.1.12, which includes libmagic/file update to version 5.42, + * the expected mime type for WOFF files is 'font/woff'. + * + * See https://github.com/php/php-src/issues/8805. + */ + if ( PHP_VERSION_ID >= 80112 ) { + $woff_mime_type = 'font/woff'; + } + + $expected = array( + 'ext' => 'woff', + 'type' => $woff_mime_type, + 'proper_filename' => false, + ); + + add_filter( + 'upload_mimes', + static function ( $mimes ) use ( $woff_mime_type ) { + $mimes['woff'] = $woff_mime_type; + return $mimes; + } + ); + + $this->assertSame( $expected, wp_check_filetype_and_ext( $file, $filename ) ); + } + /** * Test file path validation * * @ticket 42016 - * @dataProvider data_test_validate_file() + * @ticket 61488 + * @dataProvider data_validate_file * * @param string $file File path. * @param array $allowed_files List of allowed files. @@ -1767,14 +1922,14 @@ public function test_validate_file( $file, $allowed_files, $expected ) { * Data provider for file validation. * * @return array { - * @type array $0... { + * @type array ...$0 { * @type string $0 File path. * @type array $1 List of allowed files. * @type int $2 Expected result. * } * } */ - public function data_test_validate_file() { + public function data_validate_file() { return array( // Allowed files: @@ -1873,6 +2028,13 @@ public function data_test_validate_file() { 2, ), + // Windows Path with allowed file + array( + 'Apache24\htdocs\wordpress/wp-content/themes/twentyten/style.css', + array( 'Apache24\htdocs\wordpress/wp-content/themes/twentyten/style.css' ), + 0, + ), + // Disallowed files: array( 'foo.ext', @@ -1896,7 +2058,7 @@ public function data_test_validate_file() { /** * Test stream URL validation. * - * @dataProvider data_test_wp_is_stream + * @dataProvider data_wp_is_stream * * @param string $path The resource path or URL. * @param bool $expected Expected result. @@ -1913,13 +2075,13 @@ public function test_wp_is_stream( $path, $expected ) { * Data provider for stream URL validation. * * @return array { - * @type array $0... { + * @type array ...$0 { * @type string $0 The resource path or URL. * @type bool $1 Expected result. * } * } */ - public function data_test_wp_is_stream() { + public function data_wp_is_stream() { return array( // Legitimate stream examples. array( 'http://example.com', true ), @@ -1940,7 +2102,7 @@ public function data_test_wp_is_stream() { * Test human_readable_duration(). * * @ticket 39667 - * @dataProvider data_test_human_readable_duration + * @dataProvider data_human_readable_duration * * @param string $input Duration. * @param string $expected Expected human readable duration. @@ -1950,7 +2112,7 @@ public function test_human_readable_duration( $input, $expected ) { } /** - * Dataprovider for test_duration_format(). + * Data provider for test_duration_format(). * * @return array { * @type array { @@ -1959,7 +2121,7 @@ public function test_human_readable_duration( $input, $expected ) { * } * } */ - public function data_test_human_readable_duration() { + public function data_human_readable_duration() { return array( // Valid ii:ss cases. array( '0:0', '0 minutes, 0 seconds' ), @@ -2016,14 +2178,14 @@ public function data_test_human_readable_duration() { /** * @ticket 49404 - * @dataProvider data_test_wp_is_json_media_type + * @dataProvider data_wp_is_json_media_type */ public function test_wp_is_json_media_type( $input, $expected ) { $this->assertSame( $expected, wp_is_json_media_type( $input ) ); } - public function data_test_wp_is_json_media_type() { + public function data_wp_is_json_media_type() { return array( array( 'application/ld+json', true ), array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', true ), @@ -2041,13 +2203,99 @@ public function data_test_wp_is_json_media_type() { * @ticket 53668 */ public function test_wp_get_default_extension_for_mime_type() { - $this->assertEquals( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' ); + $this->assertSame( 'jpg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpg not returned as default extension for "image/jpeg"' ); $this->assertNotEquals( 'jpeg', wp_get_default_extension_for_mime_type( 'image/jpeg' ), 'jpeg should not be returned as default extension for "image/jpeg"' ); - $this->assertEquals( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' ); + $this->assertSame( 'png', wp_get_default_extension_for_mime_type( 'image/png' ), 'png not returned as default extension for "image/png"' ); $this->assertFalse( wp_get_default_extension_for_mime_type( 'wibble/wobble' ), 'false not returned for unrecognized mime type' ); $this->assertFalse( wp_get_default_extension_for_mime_type( '' ), 'false not returned when empty string as mime type supplied' ); $this->assertFalse( wp_get_default_extension_for_mime_type( ' ' ), 'false not returned when empty string as mime type supplied' ); $this->assertFalse( wp_get_default_extension_for_mime_type( 123 ), 'false not returned when int as mime type supplied' ); $this->assertFalse( wp_get_default_extension_for_mime_type( null ), 'false not returned when null as mime type supplied' ); } + + /** + * @ticket 55505 + * @covers ::wp_recursive_ksort + */ + public function test_wp_recursive_ksort() { + // Create an array to test. + $theme_json = array( + 'version' => 1, + 'settings' => array( + 'typography' => array( + 'fontFamilies' => array( + 'fontFamily' => 'DM Sans, sans-serif', + 'slug' => 'dm-sans', + 'name' => 'DM Sans', + ), + ), + 'color' => array( + 'palette' => array( + array( + 'slug' => 'foreground', + 'color' => '#242321', + 'name' => 'Foreground', + ), + array( + 'slug' => 'background', + 'color' => '#FCFBF8', + 'name' => 'Background', + ), + array( + 'slug' => 'primary', + 'color' => '#71706E', + 'name' => 'Primary', + ), + array( + 'slug' => 'tertiary', + 'color' => '#CFCFCF', + 'name' => 'Tertiary', + ), + ), + ), + ), + ); + + // Sort the array. + wp_recursive_ksort( $theme_json ); + + // Expected result. + $expected_theme_json = array( + 'settings' => array( + 'color' => array( + 'palette' => array( + array( + 'color' => '#242321', + 'name' => 'Foreground', + 'slug' => 'foreground', + ), + array( + 'color' => '#FCFBF8', + 'name' => 'Background', + 'slug' => 'background', + ), + array( + 'color' => '#71706E', + 'name' => 'Primary', + 'slug' => 'primary', + ), + array( + 'color' => '#CFCFCF', + 'name' => 'Tertiary', + 'slug' => 'tertiary', + ), + ), + ), + 'typography' => array( + 'fontFamilies' => array( + 'fontFamily' => 'DM Sans, sans-serif', + 'name' => 'DM Sans', + 'slug' => 'dm-sans', + ), + ), + ), + 'version' => 1, + ); + $this->assertSameSetsWithIndex( $theme_json, $expected_theme_json ); + } } diff --git a/tests/phpunit/tests/functions/absint.php b/tests/phpunit/tests/functions/absint.php new file mode 100644 index 0000000000000..5a9edd6961077 --- /dev/null +++ b/tests/phpunit/tests/functions/absint.php @@ -0,0 +1,81 @@ +<?php + +/** + * Tests for the absint() function. + * + * @group functions + * + * @covers ::absint + */ +class Tests_Functions_Absint extends WP_UnitTestCase { + + /** + * @ticket 60101 + * + * @dataProvider data_absint + */ + public function test_absint( $test_value, $expected_value ) { + $this->assertSame( $expected_value, absint( $test_value ) ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type string $test_value Test value. + * @type string $expected Expected return value. + * } + */ + public function data_absint() { + return array( + '1 int' => array( + 'test_value' => 1, + 'expected_value' => 1, + ), + '1 string' => array( + 'test_value' => '1', + 'expected_value' => 1, + ), + '-1 int' => array( + 'test_value' => -1, + 'expected_value' => 1, + ), + '-1 string' => array( + 'test_value' => '-1', + 'expected_value' => 1, + ), + '9.1 float' => array( + 'test_value' => 9.1, + 'expected_value' => 9, + ), + '9.9 float' => array( + 'test_value' => 9.9, + 'expected_value' => 9, + ), + 'string' => array( + 'test_value' => 'string', + 'expected_value' => 0, + ), + 'string_1' => array( + 'test_value' => 'string_1', + 'expected_value' => 0, + ), + '999_string' => array( + 'test_value' => '999_string', + 'expected_value' => 999, + ), + '99 string with spaces' => array( + 'test_value' => '99 string with spaces', + 'expected_value' => 99, + ), + '99 array' => array( + 'test_value' => array( 99 ), + 'expected_value' => 1, + ), + '99 string array' => array( + 'test_value' => array( '99' ), + 'expected_value' => 1, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/addMagicQuotes.php b/tests/phpunit/tests/functions/addMagicQuotes.php index f42b601e09816..2ad1a4ed98ebc 100644 --- a/tests/phpunit/tests/functions/addMagicQuotes.php +++ b/tests/phpunit/tests/functions/addMagicQuotes.php @@ -2,7 +2,8 @@ /** * @group formatting - * @group functions.php + * @group functions + * * @covers ::add_magic_quotes */ class Tests_Functions_AddMagicQuotes extends WP_UnitTestCase { @@ -20,7 +21,7 @@ public function test_add_magic_quotes( $test_array, $expected ) { } /** - * Data provider for test_add_magic_quotes. + * Data provider for test_add_magic_quotes(). * * @return array[] Test parameters { * @type array $test_array Test value. @@ -61,5 +62,4 @@ public function data_add_magic_quotes() { ), ); } - } diff --git a/tests/phpunit/tests/functions/allowedProtocols.php b/tests/phpunit/tests/functions/allowedProtocols.php index 8484024c86988..4d4ecf5320e14 100644 --- a/tests/phpunit/tests/functions/allowedProtocols.php +++ b/tests/phpunit/tests/functions/allowedProtocols.php @@ -2,7 +2,8 @@ /** * @group formatting - * @group functions.php + * @group functions + * * @covers ::wp_allowed_protocols */ class Tests_Functions_AllowedProtocols extends WP_UnitTestCase { @@ -35,7 +36,11 @@ public function test_allowed_protocols( $protocol, $url ) { } /** + * Data provider. + * * @link http://www.iana.org/assignments/uri-schemes/uri-schemes.xhtml + * + * @return array[] */ public function data_example_urls() { return array( diff --git a/tests/phpunit/tests/functions/anonymization.php b/tests/phpunit/tests/functions/anonymization.php index b0dfafa528a28..c4b979bfe92b2 100644 --- a/tests/phpunit/tests/functions/anonymization.php +++ b/tests/phpunit/tests/functions/anonymization.php @@ -5,21 +5,16 @@ * @package WordPress\UnitTests * * @since 4.9.6 - */ - -/** - * Class Tests_Functions_Anonymization. * - * @since 4.9.6 - * - * @group functions.php + * @group functions * @group privacy + * * @covers ::wp_privacy_anonymize_data */ class Tests_Functions_Anonymization extends WP_UnitTestCase { /** - * Test that wp_privacy_anonymize_ip() properly anonymizes all possible IP address formats. + * Tests that wp_privacy_anonymize_ip() properly anonymizes all possible IP address formats. * * @dataProvider data_wp_privacy_anonymize_ip * @@ -40,14 +35,14 @@ public function test_wp_privacy_anonymize_ip( $raw_ip, $expected_result ) { } /** - * Provide test cases for `test_wp_privacy_anonymize_ip()`. + * Data provider for `test_wp_privacy_anonymize_ip()`. * * @since 4.9.6 Moved from `Test_WP_Community_Events::data_get_unsafe_client_ip_anonymization()`. * * @return array { * @type array { - * @string string $raw_ip Raw IP address. - * @string string $expected_result Expected result. + * @type string $raw_ip Raw IP address. + * @type string $expected_result Expected result. * } * } */ @@ -175,7 +170,7 @@ public function data_wp_privacy_anonymize_ip() { } /** - * Test that wp_privacy_anonymize_ip() properly anonymizes all possible IP address formats. + * Tests that wp_privacy_anonymize_ip() properly anonymizes all possible IP address formats. * * @dataProvider data_wp_privacy_anonymize_ip_with_inet_dependency * @@ -194,14 +189,14 @@ public function test_wp_privacy_anonymize_ip_with_inet_dependency( $raw_ip, $exp } /** - * Provide test cases for `test_wp_privacy_anonymize_ip()`. + * Data provider for `test_wp_privacy_anonymize_ip()`. * * @since 4.9.6 Moved from `Test_WP_Community_Events::data_get_unsafe_client_ip_anonymization()`. * * @return array { * @type array { - * @string string $raw_ip Raw IP address. - * @string string $expected_result Expected result. + * @type string $raw_ip Raw IP address. + * @type string $expected_result Expected result. * } * } */ @@ -261,28 +256,28 @@ public function data_wp_privacy_anonymize_ip_with_inet_dependency() { } /** - * Test email anonymization of `wp_privacy_anonymize_data()`. + * Tests email anonymization of `wp_privacy_anonymize_data()`. */ public function test_anonymize_email() { $this->assertSame( 'deleted@site.invalid', wp_privacy_anonymize_data( 'email', 'bar@example.com' ) ); } /** - * Test url anonymization of `wp_privacy_anonymize_data()`. + * Tests URL anonymization of `wp_privacy_anonymize_data()`. */ public function test_anonymize_url() { $this->assertSame( 'https://site.invalid', wp_privacy_anonymize_data( 'url', 'https://example.com/author/username' ) ); } /** - * Test date anonymization of `wp_privacy_anonymize_data()`. + * Tests date anonymization of `wp_privacy_anonymize_data()`. */ public function test_anonymize_date() { $this->assertSame( '0000-00-00 00:00:00', wp_privacy_anonymize_data( 'date', '2003-12-25 12:34:56' ) ); } /** - * Test text anonymization of `wp_privacy_anonymize_data()`. + * Tests text anonymization of `wp_privacy_anonymize_data()`. */ public function test_anonymize_text() { $text = __( 'Four score and seven years ago' ); @@ -290,7 +285,7 @@ public function test_anonymize_text() { } /** - * Test long text anonymization of `wp_privacy_anonymize_data()`. + * Tests long text anonymization of `wp_privacy_anonymize_data()`. */ public function test_anonymize_long_text() { $text = __( 'Four score and seven years ago' ); @@ -298,7 +293,7 @@ public function test_anonymize_long_text() { } /** - * Test text anonymization when a filter is added. + * Tests text anonymization when a filter is added. * * @ticket 44141 */ @@ -311,7 +306,7 @@ public function test_anonymize_with_filter() { } /** - * Change the anonymized value for URLs. + * Changes the anonymized value for URLs. * * @since 4.9.8 * @@ -326,5 +321,4 @@ public function filter_wp_privacy_anonymize_data( $anonymous, $type, $data ) { } return $anonymous; } - } diff --git a/tests/phpunit/tests/functions/canonicalCharset.php b/tests/phpunit/tests/functions/canonicalCharset.php index 973562c6a993c..b46b6f5ce5b42 100644 --- a/tests/phpunit/tests/functions/canonicalCharset.php +++ b/tests/phpunit/tests/functions/canonicalCharset.php @@ -5,49 +5,59 @@ * * @since 4.8.0 * - * @group functions.php + * @group functions + * * @covers ::_canonical_charset */ class Tests_Functions_CanonicalCharset extends WP_UnitTestCase { - - public function test_utf_8_lower() { - $this->assertSame( 'UTF-8', _canonical_charset( 'utf-8' ) ); - } - - public function test_utf_8_upper() { - $this->assertSame( 'UTF-8', _canonical_charset( 'UTF-8' ) ); - } - - public function test_utf_8_mixxed() { - $this->assertSame( 'UTF-8', _canonical_charset( 'Utf-8' ) ); - } - - public function test_utf_8() { - $this->assertSame( 'UTF-8', _canonical_charset( 'UTF8' ) ); - } - - public function test_iso_lower() { - $this->assertSame( 'ISO-8859-1', _canonical_charset( 'iso-8859-1' ) ); - } - - public function test_iso_upper() { - $this->assertSame( 'ISO-8859-1', _canonical_charset( 'ISO-8859-1' ) ); - } - - public function test_iso_mixxed() { - $this->assertSame( 'ISO-8859-1', _canonical_charset( 'Iso8859-1' ) ); - } - - public function test_iso() { - $this->assertSame( 'ISO-8859-1', _canonical_charset( 'ISO8859-1' ) ); - } - - public function test_random() { - $this->assertSame( 'random', _canonical_charset( 'random' ) ); + /** + * Ensures that charset variants for common encodings normalize to the expected form. + * + * @ticket 61182 + * + * @dataProvider data_charset_normalizations + * + * @param string $given_charset Potential charset provided by user. + * @param string $normalized_charset Expected normalized form of charset. + */ + public function test_properly_normalizes_charset_variants( $given_charset, $normalized_charset ) { + $this->assertSame( + $normalized_charset, + _canonical_charset( $given_charset ), + 'Did not properly transform the provided charset into its normalized form.' + ); } - public function test_empty() { - $this->assertSame( '', _canonical_charset( '' ) ); + /** + * Data provider. + * + * @return array[]. + */ + public static function data_charset_normalizations() { + return array( + // UTF-8 family. + array( 'UTF-8', 'UTF-8' ), + array( 'Utf-8', 'UTF-8' ), + array( 'Utf-8', 'UTF-8' ), + array( 'UTF8', 'UTF-8' ), + + // Almost UTF-8. + array( 'UTF-8*', 'UTF-8*' ), + array( 'UTF.8', 'UTF.8' ), + array( 'UTF88', 'UTF88' ), + array( 'UTF-7', 'UTF-7' ), + array( 'X-UTF-8', 'X-UTF-8' ), + + // ISO-8859-1 family. + array( 'iso-8859-1', 'ISO-8859-1' ), + array( 'ISO-8859-1', 'ISO-8859-1' ), + array( 'Iso-8859-1', 'ISO-8859-1' ), + array( 'ISO8859-1', 'ISO-8859-1' ), + + // Other charset slugs should not be adjusted. + array( 'random', 'random' ), + array( '', '' ), + ); } /** @@ -88,5 +98,4 @@ public function test_update_option_blog_charset() { update_option( 'blog_charset', $orig_blog_charset ); } - } diff --git a/tests/phpunit/tests/functions/cleanDirsizeCache.php b/tests/phpunit/tests/functions/cleanDirsizeCache.php index 27aacda62b9b9..c30517c115277 100644 --- a/tests/phpunit/tests/functions/cleanDirsizeCache.php +++ b/tests/phpunit/tests/functions/cleanDirsizeCache.php @@ -3,17 +3,17 @@ /** * Tests specific to the directory size caching. * - * @group functions.php + * @group functions + * + * @covers ::clean_dirsize_cache */ class Tests_Functions_CleanDirsizeCache extends WP_UnitTestCase { /** - * Test the handling of invalid data passed as the $path parameter. + * Tests the handling of invalid data passed as the $path parameter. * * @ticket 52241 * - * @covers ::clean_dirsize_cache - * * @dataProvider data_clean_dirsize_cache_with_invalid_inputs * * @param mixed $path Path input to use in the test. @@ -53,12 +53,10 @@ public function data_clean_dirsize_cache_with_invalid_inputs() { } /** - * Test the handling of a non-path text string passed as the $path parameter. + * Tests the handling of a non-path text string passed as the $path parameter. * * @ticket 52241 * - * @covers ::clean_dirsize_cache - * * @dataProvider data_clean_dirsize_cache_with_non_path_string * * @param string $path Path input to use in the test. @@ -90,7 +88,7 @@ public function data_clean_dirsize_cache_with_non_path_string() { 'path' => 'string', 'expected_count' => 1, ), - 'non-existant string, but non-path' => array( + 'non-existent string, but non-path' => array( 'path' => 'doesnotexist', 'expected_count' => 2, ), @@ -105,7 +103,7 @@ private function mock_dirsize_cache_with_non_path_string() { } /** - * Test the behaviour of the function when the transient doesn't exist. + * Tests the behavior of the function when the transient doesn't exist. * * @ticket 52241 * @ticket 53635 @@ -121,7 +119,7 @@ public function test_recurse_dirsize_without_transient() { } /** - * Test the behaviour of the function when the transient does exist, but is not an array. + * Tests the behavior of the function when the transient does exist, but is not an array. * * In particular, this tests that no PHP TypeErrors are being thrown. * diff --git a/tests/phpunit/tests/functions/cleanupHeaderComment.php b/tests/phpunit/tests/functions/cleanupHeaderComment.php index 73fcc1ee78c04..95c1b88d20bf1 100644 --- a/tests/phpunit/tests/functions/cleanupHeaderComment.php +++ b/tests/phpunit/tests/functions/cleanupHeaderComment.php @@ -5,25 +5,26 @@ * @ticket 8497 * @ticket 38101 * - * @group functions.php + * @group functions + * * @covers ::_cleanup_header_comment */ class Tests_Functions_CleanupHeaderComment extends WP_UnitTestCase { /** - * Test cleanup header of header comment. + * Tests _cleanup_header_comment(). * * @dataProvider data_cleanup_header_comment * - * @param string $test_string - * @param string $expected + * @param string $test_string Test string. + * @param string $expected Expected return value. */ public function test_cleanup_header_comment( $test_string, $expected ) { $this->assertSameIgnoreEOL( $expected, _cleanup_header_comment( $test_string ) ); } /** - * Data provider for test_cleanup_header_comment. + * Data provider for test_cleanup_header_comment(). * * @return array[] Test parameters { * @type string $test_string Test string. diff --git a/tests/phpunit/tests/functions/deleteOptionFreshSite.php b/tests/phpunit/tests/functions/deleteOptionFreshSite.php new file mode 100644 index 0000000000000..c9f44e293d5fa --- /dev/null +++ b/tests/phpunit/tests/functions/deleteOptionFreshSite.php @@ -0,0 +1,25 @@ +<?php + +/** + * Tests for _delete_option_fresh_site function. + * + * @group functions + * + * @covers ::_delete_option_fresh_site + */ +class Tests_Functions_DeleteOptionFreshSite extends WP_UnitTestCase { + + /** + * @ticket 57191 + */ + public function test_delete_option_fresh_site() { + $current_option = get_option( 'fresh_site' ); + update_option( 'fresh_site', '1' ); + + _delete_option_fresh_site(); + $actual = get_option( 'fresh_site' ); + update_option( 'fresh_site', $current_option ); + + $this->assertSame( '0', $actual ); + } +} diff --git a/tests/phpunit/tests/functions/deprecated.php b/tests/phpunit/tests/functions/deprecated.php deleted file mode 100644 index b1d4a9f6e7bc9..0000000000000 --- a/tests/phpunit/tests/functions/deprecated.php +++ /dev/null @@ -1,185 +0,0 @@ -<?php - -/** - * Test cases for deprecated functions, arguments, and files - * - * @package WordPress - * @subpackage Unit Tests - * - * @since 3.5.0 - * - * @group functions.php - * @group deprecated - */ -class Tests_Functions_Deprecated extends WP_UnitTestCase { - - /** - * List of functions that have been passed through _deprecated_function(). - * - * @var string[] - */ - protected $_deprecated_functions = array(); - - /** - * List of arguments that have been passed through _deprecated_argument(). - * - * @var string[] - */ - protected $_deprecated_arguments = array(); - - /** - * List of files that have been passed through _deprecated_file(). - * - * @var string[] - */ - protected $_deprecated_files = array(); - - /** - * Sets up the test fixture. - */ - public function set_up() { - parent::set_up(); - $this->_deprecated_functions = array(); - $this->_deprecated_arguments = array(); - $this->_deprecated_files = array(); - add_action( 'deprecated_function_run', array( $this, 'deprecated_function' ), 10, 3 ); - add_action( 'deprecated_function_trigger_error', '__return_false' ); - add_action( 'deprecated_argument_run', array( $this, 'deprecated_argument' ), 10, 3 ); - add_action( 'deprecated_argument_trigger_error', '__return_false' ); - add_action( 'deprecated_file_included', array( $this, 'deprecated_file' ), 10, 4 ); - add_action( 'deprecated_file_trigger_error', '__return_false' ); - } - - /** - * Tears down the test fixture. - */ - public function tear_down() { - remove_action( 'deprecated_function_run', array( $this, 'deprecated_function' ), 10, 3 ); - remove_action( 'deprecated_function_trigger_error', '__return_false' ); - remove_action( 'deprecated_argument_run', array( $this, 'deprecated_argument' ), 10, 3 ); - remove_action( 'deprecated_argument_trigger_error', '__return_false' ); - remove_action( 'deprecated_file_included', array( $this, 'deprecated_argument' ), 10, 4 ); - remove_action( 'deprecated_file_trigger_error', '__return_false' ); - parent::tear_down(); - } - - /** - * Catches functions that have passed through _deprecated_function(). - * - * @param string $function - * @param string $replacement - * @param float $version - */ - public function deprecated_function( $function, $replacement, $version ) { - $this->_deprecated_functions[] = array( - 'function' => $function, - 'replacement' => $replacement, - 'version' => $version, - ); - } - - /** - * Catches arguments that have passed through _deprecated_argument(). - * - * @param string $argument - * @param string $message - * @param float $version - */ - public function deprecated_argument( $argument, $message, $version ) { - $this->_deprecated_arguments[] = array( - 'argument' => $argument, - 'message' => $message, - 'version' => $version, - ); - } - - /** - * Catches arguments that have passed through _deprecated_argument(). - * - * @param string $argument - * @param string $message - * @param float $version - */ - public function deprecated_file( $file, $version, $replacement, $message ) { - $this->_deprecated_files[] = array( - 'file' => $file, - 'version' => $version, - 'replacement' => $replacement, - 'message' => $message, - ); - } - - /** - * Checks if something was deprecated. - * - * @param string $type argument|function|file - * @param string $name - * @return array|false - */ - protected function was_deprecated( $type, $name ) { - switch ( $type ) { - case 'argument': - $search = $this->_deprecated_arguments; - $key = 'argument'; - break; - case 'function': - $search = $this->_deprecated_functions; - $key = 'function'; - break; - default: - $search = $this->_deprecated_files; - $key = 'file'; - } - foreach ( $search as $v ) { - if ( $name === $v[ $key ] ) { - return $v; - } - } - return false; - } - - /** - * Tests that wp_save_image_file() has a deprecated argument when passed a GD resource. - * - * @ticket 6821 - * @expectedDeprecated wp_save_image_file - * @requires function imagejpeg - * - * @covers ::wp_save_image_file - */ - public function test_wp_save_image_file_deprecated_with_gd_resource() { - // Call wp_save_image_file(). - require_once ABSPATH . 'wp-admin/includes/image-edit.php'; - $file = wp_tempnam(); - $img = imagecreatefromjpeg( DIR_TESTDATA . '/images/canola.jpg' ); - wp_save_image_file( $file, $img, 'image/jpeg', 1 ); - imagedestroy( $img ); - unlink( $file ); - - // Check if the arg was deprecated. - $check = $this->was_deprecated( 'argument', 'wp_save_image_file' ); - $this->assertNotEmpty( $check ); - } - - /** - * Tests that wp_save_image_file() doesn't have a deprecated argument when passed a WP_Image_Editor. - * - * @ticket 6821 - * @requires function imagejpeg - * - * @covers ::wp_save_image_file - */ - public function test_wp_save_image_file_not_deprecated_with_wp_image_editor() { - // Call wp_save_image_file(). - require_once ABSPATH . 'wp-admin/includes/image-edit.php'; - $file = wp_tempnam(); - $img = wp_get_image_editor( DIR_TESTDATA . '/images/canola.jpg' ); - wp_save_image_file( $file, $img, 'image/jpeg', 1 ); - unset( $img ); - unlink( $file ); - - // Check if the arg was deprecated. - $check = $this->was_deprecated( 'argument', 'wp_save_image_file' ); - $this->assertFalse( $check ); - } -} diff --git a/tests/phpunit/tests/functions/doEnclose.php b/tests/phpunit/tests/functions/doEnclose.php index 5c8a42089fba7..6d36fd373a779 100644 --- a/tests/phpunit/tests/functions/doEnclose.php +++ b/tests/phpunit/tests/functions/doEnclose.php @@ -1,19 +1,15 @@ <?php + /** * Test cases for the `do_enclose()` function. * * @package WordPress\UnitTests * * @since 5.3.0 - */ - -/** - * Tests_Functions_DoEnclose class. * - * @since 5.3.0 - * - * @group functions.php + * @group functions * @group post + * * @covers ::do_enclose */ class Tests_Functions_DoEnclose extends WP_UnitTestCase { @@ -25,15 +21,15 @@ class Tests_Functions_DoEnclose extends WP_UnitTestCase { */ public function set_up() { parent::set_up(); - add_filter( 'pre_http_request', array( $this, 'fake_http_request' ), 10, 3 ); + add_filter( 'pre_http_request', array( $this, 'mock_http_request' ), 10, 3 ); } /** - * Test the function with an explicit content input. + * Tests the function with an explicit content input. * * @since 5.3.0 * - * @dataProvider data_test_do_enclose + * @dataProvider data_do_enclose */ public function test_function_with_explicit_content_input( $content, $expected ) { $post_id = self::factory()->post->create(); @@ -45,11 +41,11 @@ public function test_function_with_explicit_content_input( $content, $expected ) } /** - * Test the function with an implicit content input. + * Tests the function with an implicit content input. * * @since 5.3.0 * - * @dataProvider data_test_do_enclose + * @dataProvider data_do_enclose */ public function test_function_with_implicit_content_input( $content, $expected ) { $post_id = self::factory()->post->create( @@ -65,7 +61,7 @@ public function test_function_with_implicit_content_input( $content, $expected ) } /** - * Dataprovider for `test_function_with_explicit_content_input()` + * Data provider for `test_function_with_explicit_content_input()` * and `test_function_with_implicit_content_input()`. * * @since 5.3.0 @@ -77,7 +73,7 @@ public function test_function_with_implicit_content_input( $content, $expected ) * } * } */ - public function data_test_do_enclose() { + public function data_do_enclose() { return array( 'null' => array( 'content' => null, @@ -149,7 +145,7 @@ public function test_function_should_return_false_when_invalid_post_id() { * @since 5.3.0 */ public function test_function_should_delete_enclosed_link_when_no_longer_in_post_content() { - $data = $this->data_test_do_enclose(); + $data = $this->data_do_enclose(); // Create a post with a single movie link. $post_id = self::factory()->post->create( @@ -183,7 +179,7 @@ public function test_function_should_delete_enclosed_link_when_no_longer_in_post * @since 5.3.0 */ public function test_function_should_support_post_object_input() { - $data = $this->data_test_do_enclose(); + $data = $this->data_do_enclose(); $post_object = self::factory()->post->create_and_get( array( @@ -203,7 +199,7 @@ public function test_function_should_support_post_object_input() { * @since 5.3.0 */ public function test_function_enclosure_links_should_be_filterable() { - $data = $this->data_test_do_enclose(); + $data = $this->data_do_enclose(); $post_id = self::factory()->post->create( array( @@ -250,30 +246,29 @@ protected function get_enclosed_by_post_id( $post_id ) { } /** - * Fake the HTTP request response. + * Mock the HTTP request response. * * @since 5.3.0 * - * @param bool $false False. - * @param array $arguments Request arguments. - * @param string $url Request URL. - * - * @return array Header. + * @param false|array|WP_Error $response A preemptive return value of an HTTP request. Default false. + * @param array $parsed_args HTTP request arguments. + * @param string $url The request URL. + * @return array Response data. */ - public function fake_http_request( $false, $arguments, $url ) { + public function mock_http_request( $response, $parsed_args, $url ) { // Video and audio headers. $fake_headers = array( 'mp4' => array( 'headers' => array( - 'content-length' => 123, - 'content-type' => 'video/mp4', + 'Content-Length' => 123, + 'Content-Type' => 'video/mp4', ), ), 'ogg' => array( 'headers' => array( - 'content-length' => 321, - 'content-type' => 'audio/ogg', + 'Content-Length' => 321, + 'Content-Type' => 'audio/ogg', ), ), ); @@ -290,10 +285,9 @@ public function fake_http_request( $false, $arguments, $url ) { // Fallback header. return array( 'headers' => array( - 'content-length' => 0, - 'content-type' => '', + 'Content-Length' => 0, + 'Content-Type' => '', ), ); } - } diff --git a/tests/phpunit/tests/functions/forceSslAdmin.php b/tests/phpunit/tests/functions/forceSslAdmin.php new file mode 100644 index 0000000000000..e0432c382e959 --- /dev/null +++ b/tests/phpunit/tests/functions/forceSslAdmin.php @@ -0,0 +1,51 @@ +<?php +/** + * Test cases for the `force_ssl_admin()` function. + * + * @since 6.8.0 + * + * @group functions + * + * @covers ::force_ssl_admin + */ +class Tests_Functions_ForceSslAdmin extends WP_UnitTestCase { + + public function set_up() { + parent::set_up(); + // Reset the `$forced` static variable before each test. + force_ssl_admin( false ); + } + + /** + * Tests that force_ssl_admin() returns expected values based on various inputs. + * + * @dataProvider data_force_ssl_admin + * + * @param mixed $input The input value to test. + * @param bool $expected The expected result for subsequent calls. + */ + public function test_force_ssl_admin( $input, $expected ) { + // The first call always returns the previous value. + $this->assertFalse( force_ssl_admin( $input ), 'First call did not return the expected value' ); + + // Call again to check subsequent behavior. + $this->assertSame( $expected, force_ssl_admin( $input ), 'Subsequent call did not return the expected value' ); + } + + /** + * Data provider for testing force_ssl_admin(). + * + * @return array[] + */ + public function data_force_ssl_admin() { + return array( + 'default' => array( null, false ), + 'true' => array( true, true ), + 'false' => array( false, false ), + 'non-empty string' => array( 'some string', true ), + 'empty string' => array( '', false ), + 'integer 1' => array( 1, true ), + 'integer 0' => array( 0, false ), + ); + } +} diff --git a/tests/phpunit/tests/functions/getNonCachedIds.php b/tests/phpunit/tests/functions/getNonCachedIds.php new file mode 100644 index 0000000000000..0f6b93227d710 --- /dev/null +++ b/tests/phpunit/tests/functions/getNonCachedIds.php @@ -0,0 +1,104 @@ +<?php +/** + * Test class for `_get_non_cached_ids()`. + * + * @package WordPress + * + * @group functions + * @group cache + * + * @covers ::_get_non_cached_ids + * @covers ::_validate_cache_id + */ +class Tests_Functions_GetNonCachedIds extends WP_UnitTestCase { + + /** + * @ticket 57593 + */ + public function test_uncached_valid_ids_should_be_unique() { + $object_id = 1; + + $this->assertSame( + array( $object_id ), + _get_non_cached_ids( array( $object_id, $object_id, (string) $object_id ), 'fake-group' ), + 'Duplicate object IDs should be removed.' + ); + } + + /** + * @ticket 57593 + * + * @dataProvider data_valid_ids_should_be_returned_as_integers + * + * @param mixed $object_id The object ID. + */ + public function test_valid_ids_should_be_returned_as_integers( $object_id ) { + $this->assertSame( + array( (int) $object_id ), + _get_non_cached_ids( array( $object_id ), 'fake-group' ), + 'Object IDs should be returned as integers.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_valid_ids_should_be_returned_as_integers() { + return array( + '(int) 1' => array( 1 ), + '(string) 1' => array( '1' ), + ); + } + + /** + * @ticket 57593 + */ + public function test_mix_of_valid_and_invalid_ids_should_return_the_valid_ids_and_throw_a_notice() { + $object_id = 1; + + $this->setExpectedIncorrectUsage( '_get_non_cached_ids' ); + $this->assertSame( + array( $object_id ), + _get_non_cached_ids( array( $object_id, null ), 'fake-group' ), + 'Valid object IDs should be returned.' + ); + } + + /** + * @ticket 57593 + * + * @dataProvider data_invalid_cache_ids_should_throw_a_notice + * + * @param mixed $object_id The object ID. + */ + public function test_invalid_cache_ids_should_throw_a_notice( $object_id ) { + $this->setExpectedIncorrectUsage( '_get_non_cached_ids' ); + $this->assertSame( + array(), + _get_non_cached_ids( array( $object_id ), 'fake-group' ), + 'Invalid object IDs should be dropped.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_invalid_cache_ids_should_throw_a_notice() { + return array( + 'null' => array( null ), + 'false' => array( false ), + 'true' => array( true ), + '(float) 1.0' => array( 1.0 ), + '(string) 5.0' => array( '5.0' ), + 'string' => array( 'johnny cache' ), + 'empty string' => array( '' ), + 'array' => array( array( 1 ) ), + 'empty array' => array( array() ), + 'stdClass' => array( new stdClass() ), + ); + } +} diff --git a/tests/phpunit/tests/functions/getStatusHeaderDesc.php b/tests/phpunit/tests/functions/getStatusHeaderDesc.php index c8a73ea99dbb5..1bcc6b4864669 100644 --- a/tests/phpunit/tests/functions/getStatusHeaderDesc.php +++ b/tests/phpunit/tests/functions/getStatusHeaderDesc.php @@ -5,27 +5,28 @@ * * @since 5.3.0 * - * @group functions.php + * @group functions + * * @covers ::get_status_header_desc */ class Tests_Functions_GetStatusHeaderDesc extends WP_UnitTestCase { /** - * @dataProvider _status_strings + * @dataProvider data_get_status_header_desc * * @param int $code HTTP status code. * @param string $expected Status description. */ public function test_get_status_header_desc( $code, $expected ) { - $this->assertSame( get_status_header_desc( $code ), $expected ); + $this->assertSame( $expected, get_status_header_desc( $code ) ); } /** * Data provider for test_get_status_header_desc(). * - * @return array + * @return array[] */ - public function _status_strings() { + public function data_get_status_header_desc() { return array( array( 200, 'OK' ), array( 301, 'Moved Permanently' ), diff --git a/tests/phpunit/tests/functions/getWeekstartend.php b/tests/phpunit/tests/functions/getWeekstartend.php index 3b162b8401ba1..45d10a92531ac 100644 --- a/tests/phpunit/tests/functions/getWeekstartend.php +++ b/tests/phpunit/tests/functions/getWeekstartend.php @@ -1,7 +1,8 @@ <?php /** - * @group functions.php + * @group functions + * * @covers ::get_weekstartend */ class Tests_Functions_GetWeekstartend extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/isNewDay.php b/tests/phpunit/tests/functions/isNewDay.php index 3201717fe0dde..27c9b5232fe4c 100644 --- a/tests/phpunit/tests/functions/isNewDay.php +++ b/tests/phpunit/tests/functions/isNewDay.php @@ -4,14 +4,15 @@ * * @since 5.2.0 * - * @group functions.php + * @group functions + * * @covers ::is_new_day */ class Tests_Functions_IsNewDate extends WP_UnitTestCase { /** * @ticket 46627 - * @dataProvider _data_is_new_date + * @dataProvider data_is_new_date * * @param string $currentday_string The day of the current post in the loop. * @param string $previousday_string The day of the previous post in the loop. @@ -26,12 +27,16 @@ public function test_is_new_date( $currentday_string, $previousday_string, $expe $this->assertSame( $expected, is_new_day() ); } - public function _data_is_new_date() { + /** + * Data provider. + * + * @return array[] + */ + public function data_is_new_date() { return array( array( '21.05.19', '21.05.19', 0 ), array( '21.05.19', '20.05.19', 1 ), array( '21.05.19', false, 1 ), ); } - } diff --git a/tests/phpunit/tests/functions/isPhpVersionCompatible.php b/tests/phpunit/tests/functions/isPhpVersionCompatible.php new file mode 100644 index 0000000000000..e471a2ea98000 --- /dev/null +++ b/tests/phpunit/tests/functions/isPhpVersionCompatible.php @@ -0,0 +1,90 @@ +<?php + +/** + * Tests the is_php_version_compatible() function. + * + * @group functions + * + * @covers ::is_php_version_compatible + */ +class Tests_Functions_IsPhpVersionCompatible extends WP_UnitTestCase { + /** + * Tests is_php_version_compatible(). + * + * @dataProvider data_is_php_version_compatible + * + * @ticket 54257 + * + * @param mixed $required The minimum required PHP version. + * @param bool $expected The expected result. + */ + public function test_is_php_version_compatible( $required, $expected ) { + $this->assertSame( $expected, is_php_version_compatible( $required ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_is_php_version_compatible() { + $php_version = PHP_VERSION; + + $version_parts = explode( '.', $php_version ); + $lower_version = $version_parts; + $higher_version = $version_parts; + + // Adjust the major version numbers. + --$lower_version[0]; + ++$higher_version[0]; + + $lower_version = implode( '.', $lower_version ); + $higher_version = implode( '.', $higher_version ); + + return array( + // Happy paths. + 'a lower required version' => array( + 'required' => $lower_version, + 'expected' => true, + ), + 'the same version' => array( + 'required' => $php_version, + 'expected' => true, + ), + 'a higher required version' => array( + 'required' => $higher_version, + 'expected' => false, + ), + + // Falsey values. + 'false' => array( + 'required' => false, + 'expected' => true, + ), + 'null' => array( + 'required' => null, + 'expected' => true, + ), + '0 int' => array( + 'required' => 0, + 'expected' => true, + ), + '0.0 float' => array( + 'required' => 0.0, + 'expected' => true, + ), + '0 string' => array( + 'required' => '0', + 'expected' => true, + ), + 'empty string' => array( + 'required' => '', + 'expected' => true, + ), + 'empty array' => array( + 'required' => array(), + 'expected' => true, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/isSerialized.php b/tests/phpunit/tests/functions/isSerialized.php new file mode 100644 index 0000000000000..fdc7e124fb42e --- /dev/null +++ b/tests/phpunit/tests/functions/isSerialized.php @@ -0,0 +1,213 @@ +<?php + +/** + * Tests for `is_serialized()`. + * + * @ticket 53299 + * + * @group functions + * + * @covers ::is_serialized + */ +class Tests_Functions_IsSerialized extends WP_UnitTestCase { + + /** + * @dataProvider data_is_serialized + * @dataProvider data_is_not_serialized + * + * @param mixed $data Data value to test. + * @param bool $expected Expected function result. + */ + public function test_is_serialized( $data, $expected ) { + $this->assertSame( $expected, is_serialized( $data ) ); + } + + /** + * Data provider for `test_is_serialized()`. + * + * @return array[] + */ + public function data_is_serialized() { + return array( + 'serialized empty array' => array( + 'data' => serialize( array() ), + 'expected' => true, + ), + 'serialized non-empty array' => array( + 'data' => serialize( array( 1, 1, 2, 3, 5, 8, 13 ) ), + 'expected' => true, + ), + 'serialized empty object' => array( + 'data' => serialize( new stdClass() ), + 'expected' => true, + ), + 'serialized non-empty object' => array( + 'data' => serialize( + (object) array( + 'test' => true, + '1', + 2, + ) + ), + 'expected' => true, + ), + 'serialized null' => array( + 'data' => serialize( null ), + 'expected' => true, + ), + 'serialized boolean true' => array( + 'data' => serialize( true ), + 'expected' => true, + ), + 'serialized boolean false' => array( + 'data' => serialize( false ), + 'expected' => true, + ), + 'serialized integer -1' => array( + 'data' => serialize( -1 ), + 'expected' => true, + ), + 'serialized integer 1' => array( + 'data' => serialize( -1 ), + 'expected' => true, + ), + 'serialized float 1.1' => array( + 'data' => serialize( 1.1 ), + 'expected' => true, + ), + 'serialized string' => array( + 'data' => serialize( 'this string will be serialized' ), + 'expected' => true, + ), + 'serialized string with line break' => array( + 'data' => serialize( "a\nb" ), + 'expected' => true, + ), + 'serialized string with leading and trailing spaces' => array( + 'data' => ' s:25:"this string is serialized"; ', + 'expected' => true, + ), + 'serialized enum' => array( + 'data' => 'E:7:"Foo:bar";', + 'expected' => true, + ), + ); + } + + /** + * Data provider for `test_is_serialized()`. + * + * @return array[] + */ + public function data_is_not_serialized() { + return array( + 'an empty array' => array( + 'data' => array(), + 'expected' => false, + ), + 'a non-empty array' => array( + 'data' => array( 1, 1, 2, 3, 5, 8, 13 ), + 'expected' => false, + ), + 'an empty object' => array( + 'data' => new stdClass(), + 'expected' => false, + ), + 'a non-empty object' => array( + 'data' => (object) array( + 'test' => true, + '1', + 2, + ), + 'expected' => false, + ), + 'null' => array( + 'data' => null, + 'expected' => false, + ), + 'a boolean true' => array( + 'data' => true, + 'expected' => false, + ), + 'a boolean false' => array( + 'data' => false, + 'expected' => false, + ), + 'an integer -1' => array( + 'data' => -1, + 'expected' => false, + ), + 'an integer 0' => array( + 'data' => 0, + 'expected' => false, + ), + 'an integer 1' => array( + 'data' => 1, + 'expected' => false, + ), + 'a float 0.0' => array( + 'data' => 0.0, + 'expected' => false, + ), + 'a float 1.1' => array( + 'data' => 1.1, + 'expected' => false, + ), + 'a string' => array( + 'data' => 'a string', + 'expected' => false, + ), + 'a string with line break' => array( + 'data' => "a\nb", + 'expected' => false, + ), + 'a string with leading and trailing garbage' => array( + 'data' => 'garbage:a:0:garbage;', + 'expected' => false, + ), + 'a string with missing double quotes' => array( + 'data' => 's:4:test;', + 'expected' => false, + ), + 'a string that is too short' => array( + 'data' => 's:3', + 'expected' => false, + ), + 'not a colon in second position' => array( + 'data' => 's!3:"foo";', + 'expected' => false, + ), + 'no trailing semicolon (strict check)' => array( + 'data' => 's:3:"foo"', + 'expected' => false, + ), + ); + } + + /** + * @ticket 46570 + * @dataProvider data_is_serialized_should_return_true_for_large_floats + */ + public function test_is_serialized_should_return_true_for_large_floats( $value ) { + $this->assertTrue( is_serialized( $value ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_is_serialized_should_return_true_for_large_floats() { + return array( + array( serialize( 1.7976931348623157E+308 ) ), + array( serialize( array( 1.7976931348623157E+308, 1.23e50 ) ) ), + ); + } + + /** + * @ticket 17375 + */ + public function test_no_new_serializable_types() { + $this->assertFalse( is_serialized( 'C:16:"Serialized_Class":6:{a:0:{}}' ) ); + } +} diff --git a/tests/phpunit/tests/functions/isSerializedString.php b/tests/phpunit/tests/functions/isSerializedString.php index 677bf6d591f17..cc0099e96bfc4 100644 --- a/tests/phpunit/tests/functions/isSerializedString.php +++ b/tests/phpunit/tests/functions/isSerializedString.php @@ -5,66 +5,77 @@ * * @ticket 42870 * - * @group functions.php + * @group functions + * * @covers ::is_serialized_string */ class Tests_Functions_IsSerializedString extends WP_UnitTestCase { /** - * Data provider method for testing `is_serialized_string()`. + * @dataProvider data_is_serialized_string * - * @return array + * @param array|object|int|string $data Data value to test. + * @param bool $expected Expected function result. */ - public function _is_serialized_string() { - return array( - - // pass array. - array( array(), false ), - - // pass a class. - array( new stdClass(), false ), - - // Not a string. - array( 0, false ), - - // Too short when trimmed. - array( 's:3 ', false ), - - // Too short. - array( 's:3', false ), - - // No colon in second position. - array( 's!3:"foo";', false ), - - // No trailing semicolon. - array( 's:3:"foo"', false ), - - // Wrong type. - array( 'a:3:"foo";', false ), - - // No closing quote. - array( 'a:3:"foo;', false ), - - // have to use double Quotes. - array( "s:12:'foo';", false ), - - // Wrong number of characters is close enough for is_serialized_string(). - array( 's:12:"foo";', true ), - - // Okay. - array( 's:3:"foo";', true ), - ); + public function test_is_serialized_string( $data, $expected ) { + $this->assertSame( $expected, is_serialized_string( $data ) ); } /** - * Run tests on `is_serialized_string()`. - * - * @dataProvider _is_serialized_string + * Data provider for `test_is_serialized_string()`. * - * @param array|object|int|string $data Data value to test. - * @param bool $expected Expected function result. + * @return array[] */ - public function test_is_serialized_string( $data, $expected ) { - $this->assertSame( $expected, is_serialized_string( $data ) ); + public function data_is_serialized_string() { + return array( + 'an array' => array( + 'data' => array(), + 'expected' => false, + ), + 'an object' => array( + 'data' => new stdClass(), + 'expected' => false, + ), + 'an integer 0' => array( + 'data' => 0, + 'expected' => false, + ), + 'a string that is too short when trimmed' => array( + 'data' => 's:3 ', + 'expected' => false, + ), + 'a string that is too short' => array( + 'data' => 's:3', + 'expected' => false, + ), + 'not a colon in second position' => array( + 'data' => 's!3:"foo";', + 'expected' => false, + ), + 'no trailing semicolon' => array( + 'data' => 's:3:"foo"', + 'expected' => false, + ), + 'wrong type of serialized data' => array( + 'data' => 'a:3:"foo";', + 'expected' => false, + ), + 'no closing quote' => array( + 'data' => 'a:3:"foo;', + 'expected' => false, + ), + 'single quotes instead of double' => array( + 'data' => "s:12:'foo';", + 'expected' => false, + ), + 'wrong number of characters (should not matter)' => array( + 'data' => 's:12:"foo";', + 'expected' => true, + ), + 'valid serialized string' => array( + 'data' => 's:3:"foo";', + 'expected' => true, + ), + ); } } diff --git a/tests/phpunit/tests/functions/isWpVersionCompatible.php b/tests/phpunit/tests/functions/isWpVersionCompatible.php new file mode 100644 index 0000000000000..fba8af3e14fdd --- /dev/null +++ b/tests/phpunit/tests/functions/isWpVersionCompatible.php @@ -0,0 +1,290 @@ +<?php + +/** + * Tests the is_wp_version_compatible() function. + * + * @group functions + * + * @covers ::is_wp_version_compatible + */ +class Tests_Functions_IsWpVersionCompatible extends WP_UnitTestCase { + /** + * The current WordPress version. + * + * @var string + */ + private static $wp_version; + + /** + * Sets the test WordPress version property and global before any tests run. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + self::$wp_version = wp_get_wp_version(); + $GLOBALS['_wp_tests_wp_version'] = self::$wp_version; + } + + /** + * Resets the test WordPress version global after each test runs. + */ + public function tear_down() { + $GLOBALS['_wp_tests_wp_version'] = self::$wp_version; + parent::tear_down(); + } + + /** + * Unsets the test WordPress version global after all tests run. + */ + public static function tear_down_after_class() { + unset( $GLOBALS['_wp_tests_wp_version'] ); + parent::tear_down_after_class(); + } + + /** + * Tests is_wp_version_compatible(). + * + * @dataProvider data_is_wp_version_compatible + * + * @ticket 54257 + * @ticket 61781 + * + * @param mixed $required The minimum required WordPress version. + * @param bool $expected The expected result. + */ + public function test_is_wp_version_compatible( $required, $expected ) { + $this->assertSame( $expected, is_wp_version_compatible( $required ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_is_wp_version_compatible() { + $wp_version = wp_get_wp_version(); + $version_parts = explode( '.', $wp_version ); + $lower_version = $version_parts; + $higher_version = $version_parts; + + // Adjust the major version numbers. + --$lower_version[0]; + ++$higher_version[0]; + + $lower_version = implode( '.', $lower_version ); + $higher_version = implode( '.', $higher_version ); + + return array( + // Happy paths. + 'the same version' => array( + 'required' => $wp_version, + 'expected' => true, + ), + 'a lower required version' => array( + 'required' => $lower_version, + 'expected' => true, + ), + 'a higher required version' => array( + 'required' => $higher_version, + 'expected' => false, + ), + + // Acceptable versions containing '.0'. + 'correct version ending with x.0' => array( + 'required' => '5.0', + 'expected' => true, + ), + 'correct version with x.0.x in middle of version' => array( + 'required' => '5.0.1', + 'expected' => true, + ), + + // Falsey values. + 'false' => array( + 'required' => false, + 'expected' => true, + ), + 'null' => array( + 'required' => null, + 'expected' => true, + ), + '0 int' => array( + 'required' => 0, + 'expected' => true, + ), + '0.0 float' => array( + 'required' => 0.0, + 'expected' => true, + ), + '0 string' => array( + 'required' => '0', + 'expected' => true, + ), + 'empty string' => array( + 'required' => '', + 'expected' => true, + ), + 'empty array' => array( + 'required' => array(), + 'expected' => true, + ), + ); + } + + /** + * Tests that is_wp_version_compatible() gracefully handles incorrect version numbering. + * + * @dataProvider data_is_wp_version_compatible_should_gracefully_handle_trailing_point_zero_version_numbers + * + * @ticket 59448 + * @ticket 61781 + * + * @param mixed $required The minimum required WordPress version. + * @param string $wp The value for the $wp_version global variable. + * @param bool $expected The expected result. + */ + public function test_is_wp_version_compatible_should_gracefully_handle_trailing_point_zero_version_numbers( $required, $wp, $expected ) { + $GLOBALS['_wp_tests_wp_version'] = $wp; + $this->assertSame( $expected, is_wp_version_compatible( $required ), 'The expected result was not returned.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_is_wp_version_compatible_should_gracefully_handle_trailing_point_zero_version_numbers() { + return array( + 'an incorrect trailing .0 and the same version' => array( + 'required' => '5.2.0', + 'wp' => '5.2', + 'expected' => true, + ), + 'an incorrect trailing .0 and the same x.0 version' => array( + 'required' => '5.0.0', + 'wp' => '5.0', + 'expected' => true, + ), + 'an incorrect trailing .0 and space and same x.0 version' => array( + 'required' => '5.0.0 ', + 'wp' => '5.0', + 'expected' => true, + ), + 'incorrect preceding and trailing spaces trailing .0' => array( + 'required' => ' 5.0.0 ', + 'wp' => '5.0', + 'expected' => true, + ), + 'an incorrect trailing .0 on x.0.x version' => array( + 'required' => '5.0.1.0', + 'wp' => '5.0.1', + 'expected' => true, + ), + 'an incorrect trailing .0 and an earlier version' => array( + 'required' => '5.0.0', + 'wp' => '4.0', + 'expected' => false, + ), + 'an incorrect trailing .0 and an earlier x.0 version' => array( + 'required' => '5.0.0', + 'wp' => '4.0', + 'expected' => false, + ), + 'an incorrect trailing .0 and a later version' => array( + 'required' => '5.0.0', + 'wp' => '6.0', + 'expected' => true, + ), + 'an incorrect trailing .0 and a later x.0 version' => array( + 'required' => '5.0.0', + 'wp' => '6.0', + 'expected' => true, + ), + ); + } + + /** + * Tests is_wp_version_compatible() with development versions. + * + * @dataProvider data_is_wp_version_compatible_with_development_versions + * + * @ticket 54257 + * @ticket 61781 + * + * @param string $required The minimum required WordPress version. + * @param string $wp The value for the $wp_version global variable. + * @param bool $expected The expected result. + */ + public function test_is_wp_version_compatible_with_development_versions( $required, $wp, $expected ) { + $GLOBALS['_wp_tests_wp_version'] = $wp; + $this->assertSame( $expected, is_wp_version_compatible( $required ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_is_wp_version_compatible_with_development_versions() { + // For consistent results, remove possible suffixes. + list( $version ) = explode( '-', wp_get_wp_version() ); + + $version_parts = explode( '.', $version ); + $lower_version = $version_parts; + $higher_version = $version_parts; + + // Adjust the major version numbers. + --$lower_version[0]; + ++$higher_version[0]; + + $lower_version = implode( '.', $lower_version ); + $higher_version = implode( '.', $higher_version ); + + return array( + 'a lower required version and an alpha wordpress version' => array( + 'required' => $lower_version, + 'wp' => $version . '-alpha-12341-src', + 'expected' => true, + ), + 'a lower required version and a beta wordpress version' => array( + 'required' => $lower_version, + 'wp' => $version . '-beta1', + 'expected' => true, + ), + 'a lower required version and a release candidate wordpress version' => array( + 'required' => $lower_version, + 'wp' => $version . '-RC1', + 'expected' => true, + ), + 'the same required version and an alpha wordpress version' => array( + 'required' => $version, + 'wp' => $version . '-alpha-12341-src', + 'expected' => true, + ), + 'the same required version and a beta wordpress version' => array( + 'required' => $version, + 'wp' => $version . '-beta1', + 'expected' => true, + ), + 'the same required version and a release candidate wordpress version' => array( + 'required' => $version, + 'wp' => $version . '-RC1', + 'expected' => true, + ), + 'a higher required version and an alpha wordpress version' => array( + 'required' => $higher_version, + 'wp' => $version . '-alpha-12341-src', + 'expected' => false, + ), + 'a higher required version and a beta wordpress version' => array( + 'required' => $higher_version, + 'wp' => $version . '-beta1', + 'expected' => false, + ), + 'a higher required version and a release candidate wordpress version' => array( + 'required' => $higher_version, + 'wp' => $version . '-RC1', + 'expected' => false, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/listFiles.php b/tests/phpunit/tests/functions/listFiles.php index a70a05894010d..a7e7346956dc6 100644 --- a/tests/phpunit/tests/functions/listFiles.php +++ b/tests/phpunit/tests/functions/listFiles.php @@ -3,7 +3,8 @@ /** * Test list_files(). * - * @group functions.php + * @group functions + * * @covers ::list_files */ class Tests_Functions_ListFiles extends WP_UnitTestCase { @@ -19,4 +20,63 @@ public function test_list_files_can_exclude_files() { $admin_files = list_files( ABSPATH . 'wp-admin/', 100, array( 'index.php' ) ); $this->assertNotContains( ABSPATH . 'wp-admin/index.php', $admin_files ); } + + /** + * Tests that list_files() optionally includes hidden files. + * + * @ticket 53659 + * + * @dataProvider data_list_files_should_optionally_include_hidden_files + * + * @param string $filename The name of the hidden file. + * @param bool $include_hidden Whether to include hidden ("." prefixed) files. + * @param string[] $exclusions List of folders and files to skip. + * @param bool $expected Whether the file should be included in the results. + */ + public function test_list_files_should_optionally_include_hidden_files( $filename, $include_hidden, $exclusions, $expected ) { + $test_dir = get_temp_dir() . 'test-list-files/'; + $hidden_file = $test_dir . $filename; + + mkdir( $test_dir ); + touch( $hidden_file ); + + $actual = list_files( $test_dir, 100, $exclusions, $include_hidden ); + + unlink( $hidden_file ); + rmdir( $test_dir ); + + if ( $expected ) { + $this->assertContains( $hidden_file, $actual, 'The file was not included.' ); + } else { + $this->assertNotContains( $hidden_file, $actual, 'The file was included.' ); + } + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_list_files_should_optionally_include_hidden_files() { + return array( + '$include_hidden = false and no exclusions' => array( + 'filename' => '.hidden_file', + 'include_hidden' => false, + 'exclusions' => array(), + 'expected' => false, + ), + '$include_hidden = true and no exclusions' => array( + 'filename' => '.hidden_file', + 'include_hidden' => true, + 'exclusions' => array(), + 'expected' => true, + ), + '$include_hidden = true and an excluded filename' => array( + 'filename' => '.hidden_file', + 'include_hidden' => true, + 'exclusions' => array( '.hidden_file' ), + 'expected' => false, + ), + ); + } } diff --git a/tests/phpunit/tests/functions/maybeSerialize.php b/tests/phpunit/tests/functions/maybeSerialize.php new file mode 100644 index 0000000000000..c7ee7179b5984 --- /dev/null +++ b/tests/phpunit/tests/functions/maybeSerialize.php @@ -0,0 +1,249 @@ +<?php + +/** + * Tests for `maybe_serialize()` and `maybe_unserialize()`. + * + * @group functions + * + * @covers ::maybe_serialize + * @covers ::maybe_unserialize + */ +class Tests_Functions_MaybeSerialize extends WP_UnitTestCase { + + /** + * @dataProvider data_is_not_serialized + */ + public function test_maybe_serialize( $value ) { + if ( is_array( $value ) || is_object( $value ) ) { + $expected = serialize( $value ); + } else { + $expected = $value; + } + + $this->assertSame( $expected, maybe_serialize( $value ) ); + } + + /** + * @dataProvider data_is_serialized + */ + public function test_maybe_serialize_with_double_serialization( $value ) { + $expected = serialize( $value ); + + $this->assertSame( $expected, maybe_serialize( $value ) ); + } + + /** + * @dataProvider data_is_serialized + * @dataProvider data_is_not_serialized + */ + public function test_maybe_unserialize( $value, $is_serialized ) { + if ( $is_serialized ) { + $expected = unserialize( trim( $value ) ); + } else { + $expected = $value; + } + + if ( is_object( $expected ) ) { + $this->assertEquals( $expected, maybe_unserialize( $value ) ); + } else { + $this->assertSame( $expected, maybe_unserialize( $value ) ); + } + } + + /** + * Data provider for `test_maybe_unserialize()`. + * + * @return array[] + */ + public function data_is_serialized() { + return array( + 'serialized empty array' => array( + 'data' => serialize( array() ), + 'expected' => true, + ), + 'serialized non-empty array' => array( + 'data' => serialize( array( 1, 1, 2, 3, 5, 8, 13 ) ), + 'expected' => true, + ), + 'serialized empty object' => array( + 'data' => serialize( new stdClass() ), + 'expected' => true, + ), + 'serialized non-empty object' => array( + 'data' => serialize( + (object) array( + 'test' => true, + '1', + 2, + ) + ), + 'expected' => true, + ), + 'serialized null' => array( + 'data' => serialize( null ), + 'expected' => true, + ), + 'serialized boolean true' => array( + 'data' => serialize( true ), + 'expected' => true, + ), + 'serialized boolean false' => array( + 'data' => serialize( false ), + 'expected' => true, + ), + 'serialized integer -1' => array( + 'data' => serialize( -1 ), + 'expected' => true, + ), + 'serialized integer 1' => array( + 'data' => serialize( -1 ), + 'expected' => true, + ), + 'serialized float 1.1' => array( + 'data' => serialize( 1.1 ), + 'expected' => true, + ), + 'serialized string' => array( + 'data' => serialize( 'this string will be serialized' ), + 'expected' => true, + ), + 'serialized string with line break' => array( + 'data' => serialize( "a\nb" ), + 'expected' => true, + ), + 'serialized string with leading and trailing spaces' => array( + 'data' => ' s:25:"this string is serialized"; ', + 'expected' => true, + ), + ); + } + + /** + * Data provider for `test_maybe_serialize()`. + * + * @return array[] + */ + public function data_is_not_serialized() { + return array( + 'an empty array' => array( + 'data' => array(), + 'expected' => false, + ), + 'a non-empty array' => array( + 'data' => array( 1, 1, 2, 3, 5, 8, 13 ), + 'expected' => false, + ), + 'an empty object' => array( + 'data' => new stdClass(), + 'expected' => false, + ), + 'a non-empty object' => array( + 'data' => (object) array( + 'test' => true, + '1', + 2, + ), + 'expected' => false, + ), + 'null' => array( + 'data' => null, + 'expected' => false, + ), + 'a boolean true' => array( + 'data' => true, + 'expected' => false, + ), + 'a boolean false' => array( + 'data' => false, + 'expected' => false, + ), + 'an integer -1' => array( + 'data' => -1, + 'expected' => false, + ), + 'an integer 0' => array( + 'data' => 0, + 'expected' => false, + ), + 'an integer 1' => array( + 'data' => 1, + 'expected' => false, + ), + 'a float 0.0' => array( + 'data' => 0.0, + 'expected' => false, + ), + 'a float 1.1' => array( + 'data' => 1.1, + 'expected' => false, + ), + 'a string' => array( + 'data' => 'a string', + 'expected' => false, + ), + 'a string with line break' => array( + 'data' => "a\nb", + 'expected' => false, + ), + 'a string with leading and trailing garbage' => array( + 'data' => 'garbage:a:0:garbage;', + 'expected' => false, + ), + 'a string with missing double quotes' => array( + 'data' => 's:4:test;', + 'expected' => false, + ), + 'a string that is too short' => array( + 'data' => 's:3', + 'expected' => false, + ), + 'not a colon in second position' => array( + 'data' => 's!3:"foo";', + 'expected' => false, + ), + 'no trailing semicolon (strict check)' => array( + 'data' => 's:3:"foo"', + 'expected' => false, + ), + ); + } + + /** + * @dataProvider data_serialize_deserialize_objects + */ + public function test_deserialize_request_utility_filtered_iterator_objects( $value ) { + $serialized = maybe_serialize( $value ); + + if ( get_class( $value ) === 'WpOrg\Requests\Utility\FilteredIterator' ) { + $new_value = unserialize( $serialized ); + $property = ( new ReflectionClass( 'WpOrg\Requests\Utility\FilteredIterator' ) )->getProperty( 'callback' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $callback_value = $property->getValue( $new_value ); + + $this->assertSame( null, $callback_value ); + } else { + $this->assertSame( $value->count(), unserialize( $serialized )->count() ); + } + } + + /** + * Data provider for test_deserialize_request_utility_filtered_iterator_objects(). + * + * @return array[] + */ + public function data_serialize_deserialize_objects() { + return array( + 'filtered iterator using md5' => array( + new WpOrg\Requests\Utility\FilteredIterator( array( 1 ), 'md5' ), + ), + 'filtered iterator using sha1' => array( + new WpOrg\Requests\Utility\FilteredIterator( array( 1, 2 ), 'sha1' ), + ), + 'array iterator' => array( + new ArrayIterator( array( 1, 2, 3 ) ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/mceSetDirection.php b/tests/phpunit/tests/functions/mceSetDirection.php new file mode 100644 index 0000000000000..485dd06dcc71a --- /dev/null +++ b/tests/phpunit/tests/functions/mceSetDirection.php @@ -0,0 +1,42 @@ +<?php + +/** + * Tests for the _mce_set_direction() function. + * + * @group functions + * + * @covers ::_mce_set_direction + */ +class Tests_Functions_MceSetDirection extends WP_UnitTestCase { + + /** + * @ticket 60219 + */ + public function test__mce_set_direction() { + global $wp_locale; + + $mce_init = array( + 'directionality' => 'ltr', + 'rtl_ui' => false, + 'plugins' => 'plugins', + 'toolbar1' => 'toolbar1', + ); + + $expected = array( + 'directionality' => 'rtl', + 'rtl_ui' => true, + 'plugins' => 'plugins,directionality', + 'toolbar1' => 'toolbar1,ltr', + ); + + $actual = _mce_set_direction( $mce_init ); + $this->assertSameSets( $mce_init, $actual, 'An unexpected LTR result was returned.' ); + + $orig_text_dir = $wp_locale->text_direction; + $wp_locale->text_direction = 'rtl'; + $actual = _mce_set_direction( $mce_init ); + $wp_locale->text_direction = $orig_text_dir; + + $this->assertSameSets( $expected, $actual, 'An unexpected RTL result was returned.' ); + } +} diff --git a/tests/phpunit/tests/functions/numberFormatI18n.php b/tests/phpunit/tests/functions/numberFormatI18n.php index c57405887ddf2..fbddcacf7ba5f 100644 --- a/tests/phpunit/tests/functions/numberFormatI18n.php +++ b/tests/phpunit/tests/functions/numberFormatI18n.php @@ -3,8 +3,9 @@ /** * Tests for number_format_i18n() * - * @group functions.php + * @group functions * @group i18n + * * @covers ::number_format_i18n */ class Tests_Functions_NumberFormatI18n extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/pluginBasename.php b/tests/phpunit/tests/functions/pluginBasename.php index d28cf914cff7f..ac41348c4cd6e 100644 --- a/tests/phpunit/tests/functions/pluginBasename.php +++ b/tests/phpunit/tests/functions/pluginBasename.php @@ -3,8 +3,9 @@ /** * Tests for plugin_basename() * - * @group functions.php + * @group functions * @group plugins + * * @covers ::plugin_basename */ class Tests_Functions_PluginBasename extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/referer.php b/tests/phpunit/tests/functions/referer.php index b1d113afcf5c9..7d0efb31a1697 100644 --- a/tests/phpunit/tests/functions/referer.php +++ b/tests/phpunit/tests/functions/referer.php @@ -3,7 +3,8 @@ /** * Test wp_get_referer(). * - * @group functions.php + * @group functions + * * @covers ::wp_get_referer * @covers ::wp_get_raw_referer */ @@ -30,7 +31,9 @@ public function _fake_subfolder_install() { } public function filter_allowed_redirect_hosts( $hosts ) { - $hosts[] = 'another.' . WP_TESTS_DOMAIN; + // Make sure we're only using the hostname and not anything else that might be in the WP_TESTS_DOMAIN. + $parsed = parse_url( 'http://' . WP_TESTS_DOMAIN ); + $hosts[] = 'another.' . $parsed['host']; return $hosts; } @@ -156,4 +159,12 @@ public function test_raw_referer_both() { $_REQUEST['_wp_http_referer'] = addslashes( 'http://foo.bar/baz' ); $this->assertSame( 'http://foo.bar/baz', wp_get_raw_referer() ); } + + /** + * @ticket 57670 + */ + public function test_raw_referer_is_false_on_invalid_request_parameter() { + $_REQUEST['_wp_http_referer'] = array( 'demo' ); + $this->assertFalse( wp_get_raw_referer() ); + } } diff --git a/tests/phpunit/tests/functions/removeQueryArg.php b/tests/phpunit/tests/functions/removeQueryArg.php index b8130abe8dedd..43252fc11c4e8 100644 --- a/tests/phpunit/tests/functions/removeQueryArg.php +++ b/tests/phpunit/tests/functions/removeQueryArg.php @@ -1,13 +1,14 @@ <?php /** - * @group functions.php + * @group functions + * * @covers ::remove_query_arg */ class Tests_Functions_RemoveQueryArg extends WP_UnitTestCase { /** - * @dataProvider remove_query_arg_provider + * @dataProvider data_remove_query_arg */ public function test_remove_query_arg( $keys_to_remove, $url, $expected ) { $actual = remove_query_arg( $keys_to_remove, $url ); @@ -16,7 +17,12 @@ public function test_remove_query_arg( $keys_to_remove, $url, $expected ) { $this->assertSame( $expected, $actual ); } - public function remove_query_arg_provider() { + /** + * Data provider. + * + * @return array[] + */ + public function data_remove_query_arg() { return array( array( 'foo', 'edit.php?foo=test1&baz=test1', 'edit.php?baz=test1' ), array( array( 'foo' ), 'edit.php?foo=test2&baz=test2', 'edit.php?baz=test2' ), diff --git a/tests/phpunit/tests/functions/sizeFormat.php b/tests/phpunit/tests/functions/sizeFormat.php index 281db07f5d7f0..77134188634fe 100644 --- a/tests/phpunit/tests/functions/sizeFormat.php +++ b/tests/phpunit/tests/functions/sizeFormat.php @@ -3,47 +3,82 @@ /** * Tests for size_format() * + * @ticket 22405 * @ticket 36635 + * @ticket 40875 + * + * @group functions * - * @group functions.php * @covers ::size_format */ class Tests_Functions_SizeFormat extends WP_UnitTestCase { - public function _data_size_format() { + /** + * Data provider. + * + * @return array[] + */ + public function data_size_format() { return array( + // Invalid values. array( array(), 0, false ), array( 'baba', 0, false ), array( '', 0, false ), array( '-1', 0, false ), array( -1, 0, false ), + // Bytes. array( 0, 0, '0 B' ), array( 1, 0, '1 B' ), array( 1023, 0, '1,023 B' ), + // Kilobytes. array( KB_IN_BYTES, 0, '1 KB' ), array( KB_IN_BYTES, 2, '1.00 KB' ), array( 2.5 * KB_IN_BYTES, 0, '3 KB' ), array( 2.5 * KB_IN_BYTES, 2, '2.50 KB' ), array( 10 * KB_IN_BYTES, 0, '10 KB' ), + // Megabytes. array( (string) 1024 * KB_IN_BYTES, 2, '1.00 MB' ), array( MB_IN_BYTES, 0, '1 MB' ), array( 2.5 * MB_IN_BYTES, 0, '3 MB' ), array( 2.5 * MB_IN_BYTES, 2, '2.50 MB' ), + // Gigabytes. array( (string) 1024 * MB_IN_BYTES, 2, '1.00 GB' ), array( GB_IN_BYTES, 0, '1 GB' ), array( 2.5 * GB_IN_BYTES, 0, '3 GB' ), array( 2.5 * GB_IN_BYTES, 2, '2.50 GB' ), + // Terabytes. array( (string) 1024 * GB_IN_BYTES, 2, '1.00 TB' ), array( TB_IN_BYTES, 0, '1 TB' ), array( 2.5 * TB_IN_BYTES, 0, '3 TB' ), array( 2.5 * TB_IN_BYTES, 2, '2.50 TB' ), + // Petabytes. + array( (string) 1024 * TB_IN_BYTES, 2, '1.00 PB' ), + array( PB_IN_BYTES, 0, '1 PB' ), + array( 2.5 * PB_IN_BYTES, 0, '3 PB' ), + array( 2.5 * PB_IN_BYTES, 2, '2.50 PB' ), + // Exabytes. + array( (string) 1024 * PB_IN_BYTES, 2, '1.00 EB' ), + array( EB_IN_BYTES, 0, '1 EB' ), + array( 2.5 * EB_IN_BYTES, 0, '3 EB' ), + array( 2.5 * EB_IN_BYTES, 2, '2.50 EB' ), + // Zettabytes. + array( (string) 1024 * EB_IN_BYTES, 2, '1.00 ZB' ), + array( ZB_IN_BYTES, 0, '1 ZB' ), + array( 2.5 * ZB_IN_BYTES, 0, '3 ZB' ), + array( 2.5 * ZB_IN_BYTES, 2, '2.50 ZB' ), + // Yottabytes. + array( (string) 1024 * ZB_IN_BYTES, 2, '1.00 YB' ), + array( YB_IN_BYTES, 0, '1 YB' ), + array( 2.5 * YB_IN_BYTES, 0, '3 YB' ), + array( 2.5 * YB_IN_BYTES, 2, '2.50 YB' ), + // Edge values. array( TB_IN_BYTES + ( TB_IN_BYTES / 2 ) + MB_IN_BYTES, 1, '1.5 TB' ), array( TB_IN_BYTES - MB_IN_BYTES - KB_IN_BYTES, 3, '1,023.999 GB' ), ); } /** - * @dataProvider _data_size_format + * @dataProvider data_size_format * * @param $bytes * @param $decimals diff --git a/tests/phpunit/tests/functions/underscoreReturn.php b/tests/phpunit/tests/functions/underscoreReturn.php index 4973c2d328238..86c6ca1e45aec 100644 --- a/tests/phpunit/tests/functions/underscoreReturn.php +++ b/tests/phpunit/tests/functions/underscoreReturn.php @@ -4,7 +4,7 @@ * * @since 5.1.0 * - * @group functions.php + * @group functions */ class Tests_Functions_UnderscoreReturn extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/wp.php b/tests/phpunit/tests/functions/wp.php index 04df43b3489d5..984e754fc9645 100644 --- a/tests/phpunit/tests/functions/wp.php +++ b/tests/phpunit/tests/functions/wp.php @@ -1,8 +1,9 @@ <?php /** - * @group functions.php + * @group functions * @group query + * * @covers ::wp */ class Tests_Functions_WP extends WP_UnitTestCase { @@ -16,5 +17,4 @@ public function test_wp_sets_global_vars() { $this->assertInstanceOf( 'WP_Query', $wp_query ); $this->assertInstanceOf( 'WP_Query', $wp_the_query ); } - } diff --git a/tests/phpunit/tests/functions/wpAdminNotice.php b/tests/phpunit/tests/functions/wpAdminNotice.php new file mode 100644 index 0000000000000..e2b694d726cf1 --- /dev/null +++ b/tests/phpunit/tests/functions/wpAdminNotice.php @@ -0,0 +1,326 @@ +<?php + +/** + * Tests for `wp_admin_notice()`. + * + * @group functions + * + * @covers ::wp_admin_notice + */ +class Tests_Functions_WpAdminNotice extends WP_UnitTestCase { + + /** + * Tests that `wp_admin_notice()` outputs the expected admin notice markup. + * + * @ticket 57791 + * + * @dataProvider data_should_output_admin_notice + * + * @param string $message The message to output. + * @param array $args Arguments for the admin notice. + * @param string $expected The expected admin notice markup. + */ + public function test_should_output_admin_notice( $message, $args, $expected ) { + ob_start(); + wp_admin_notice( $message, $args ); + $actual = ob_get_clean(); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_output_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', + ), + 'a notice without paragraph wrapping' => array( + 'message' => '<span>A notice without paragraph wrapping.</span>', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '"><script>alert("Howdy,admin!");</script>', + ), + 'expected' => '<div class="notice notice-">alert("Howdy,admin!");"><p>A notice with an unsafe type.</p></div>', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice', + ), + 'expected' => '<div id="">alert( "Howdy, admin!" ); <div class="notice"><p>A notice with an unsafe ID.</p></div>', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ), + ), + 'expected' => '<div class="notice ">alert( "Howdy, admin!" ); <div class="notice"><p>A notice with unsafe additional classes.</p></div>', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>', + ), + 'an ID that is not a string' => array( + 'message' => 'A notice with an ID that is not a string.', + 'args' => array( + 'id' => array( 'message' ), + ), + 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>', + ), + 'dismissible as a truthy value rather than (bool) true' => array( + 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', + 'args' => array( + 'dismissible' => 1, + ), + 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', + ), + 'additional classes that are not an array' => array( + 'message' => 'A notice with additional classes that are not an array.', + 'args' => array( + 'additional_classes' => 'class-1 class-2 class-3', + ), + 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>', + ), + 'additional attribute with a value' => array( + 'message' => 'A notice with an additional attribute with a value.', + 'args' => array( + 'attributes' => array( 'aria-live' => 'assertive' ), + ), + 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', + ), + 'additional hidden attribute' => array( + 'message' => 'A notice with the hidden attribute.', + 'args' => array( + 'attributes' => array( 'hidden' => true ), + ), + 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>', + ), + 'additional attribute no associative keys' => array( + 'message' => 'A notice with a boolean attribute without an associative key.', + 'args' => array( + 'attributes' => array( 'hidden' ), + ), + 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', + ), + 'additional attribute with role' => array( + 'message' => 'A notice with an additional attribute role.', + 'args' => array( + 'attributes' => array( 'role' => 'alert' ), + ), + 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', + ), + 'multiple additional attributes' => array( + 'message' => 'A notice with multiple additional attributes.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'data-test' => -1, + ), + ), + 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', + ), + 'data attribute with unsafe value' => array( + 'message' => 'A notice with an additional attribute with an unsafe value.', + 'args' => array( + 'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ), + ), + 'expected' => '<div class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', + ), + 'additional invalid attribute' => array( + 'message' => 'A notice with an additional attribute that is invalid.', + 'args' => array( + 'attributes' => array( 'not-valid' => 'not-valid' ), + ), + 'expected' => '<div class="notice"><p>A notice with an additional attribute that is invalid.</p></div>', + ), + 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( + 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'disabled' => 'disabled', + 'data-name' => 'my-name', + 'data-id' => 1, + 'hidden', + ), + ), + 'expected' => '<div class="notice" role="alert" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', + ), + 'paragraph wrapping as a falsy value rather than (bool) false' => array( + 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', + 'args' => array( + 'paragraph_wrap' => 0, + ), + 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', + ), + ); + } + + /** + * Tests that `_doing_it_wrong()` is thrown when a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + ob_start(); + wp_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ); + $actual = ob_get_clean(); + + $this->assertSame( + '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', + $actual + ); + } + + /** + * Tests that `wp_admin_notice()` fires the 'wp_admin_notice' action. + * + * @ticket 57791 + */ + public function test_should_fire_wp_admin_notice_action() { + $action = new MockAction(); + add_action( 'wp_admin_notice', array( $action, 'action' ) ); + + ob_start(); + wp_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + ob_end_clean(); + + $this->assertSame( 1, $action->get_call_count() ); + } +} diff --git a/tests/phpunit/tests/functions/wpArrayGet.php b/tests/phpunit/tests/functions/wpArrayGet.php index 8fdf23bcf96f7..1035a5f200f4a 100644 --- a/tests/phpunit/tests/functions/wpArrayGet.php +++ b/tests/phpunit/tests/functions/wpArrayGet.php @@ -5,13 +5,14 @@ * * @since 5.6.0 * - * @group functions.php + * @group functions + * * @covers ::_wp_array_get */ class Tests_Functions_wpArrayGet extends WP_UnitTestCase { /** - * Test _wp_array_get() with invalid parameters. + * Tests _wp_array_get() with invalid parameters. * * @ticket 51720 */ @@ -57,7 +58,7 @@ public function test_wp_array_get_invalid_parameters() { } /** - * Test _wp_array_get() with non-subtree paths. + * Tests _wp_array_get() with non-subtree paths. * * @ticket 51720 */ @@ -111,7 +112,7 @@ public function test_wp_array_get_simple_non_subtree() { } /** - * Test _wp_array_get() with subtrees. + * Tests _wp_array_get() with subtrees. * * @ticket 51720 */ @@ -160,7 +161,7 @@ public function test_wp_array_get_subtree() { } /** - * Test _wp_array_get() with zero strings. + * Tests _wp_array_get() with zero strings. * * @ticket 51720 */ @@ -211,7 +212,7 @@ public function test_wp_array_get_handle_zeros() { } /** - * Test _wp_array_get() with null values. + * Tests _wp_array_get() with null values. * * @ticket 51720 */ @@ -237,23 +238,10 @@ public function test_wp_array_get_null() { ), true ); - - $this->assertSame( - _wp_array_get( - array( - 'key' => array( - null => 4, - ), - ), - array( 'key', null ), - true - ), - 4 - ); } /** - * Test _wp_array_get() with empty paths. + * Tests _wp_array_get() with empty paths. * * @ticket 51720 */ diff --git a/tests/phpunit/tests/functions/wpArraySet.php b/tests/phpunit/tests/functions/wpArraySet.php index 8a468fabea69c..159565bf8ea99 100644 --- a/tests/phpunit/tests/functions/wpArraySet.php +++ b/tests/phpunit/tests/functions/wpArraySet.php @@ -5,13 +5,14 @@ * * @since 5.8.0 * - * @group functions.php + * @group functions + * * @covers ::_wp_array_set */ class Tests_Functions_wpArraySet extends WP_UnitTestCase { /** - * Test _wp_array_set() with invalid parameters. + * Tests _wp_array_set() with invalid parameters. * * @ticket 53175 */ @@ -53,7 +54,7 @@ public function test_wp_array_set_invalid_parameters() { } /** - * Test _wp_array_set() with simple non-subtree path. + * Tests _wp_array_set() with simple non-subtree path. * * @ticket 53175 */ @@ -84,7 +85,7 @@ public function test_wp_array_set_simple_non_subtree() { } /** - * Test _wp_array_set() with subtree paths. + * Tests _wp_array_set() with subtree paths. * * @ticket 53175 */ diff --git a/tests/phpunit/tests/functions/wpArraySliceAssoc.php b/tests/phpunit/tests/functions/wpArraySliceAssoc.php index f7ce7293a258f..9e3cb65e8114b 100644 --- a/tests/phpunit/tests/functions/wpArraySliceAssoc.php +++ b/tests/phpunit/tests/functions/wpArraySliceAssoc.php @@ -5,13 +5,14 @@ * * @since 5.3.0 * - * @group functions.php + * @group functions + * * @covers ::wp_array_slice_assoc */ class Tests_Functions_wpArraySliceAssoc extends WP_UnitTestCase { /** - * Test wp_array_slice_assoc(). + * Tests wp_array_slice_assoc(). * * @dataProvider data_wp_array_slice_assoc_arrays * @@ -22,13 +23,13 @@ class Tests_Functions_wpArraySliceAssoc extends WP_UnitTestCase { * @param array $expected The expected result. */ public function test_wp_array_slice_assoc( $target_array, $keys, $expected ) { - $this->assertSame( wp_array_slice_assoc( $target_array, $keys ), $expected ); + $this->assertSame( $expected, wp_array_slice_assoc( $target_array, $keys ) ); } /** - * Test data for wp_array_slice_assoc(). + * Data provider for wp_array_slice_assoc(). * - * @return array + * @return array[] */ public function data_wp_array_slice_assoc_arrays() { return array( diff --git a/tests/phpunit/tests/functions/wpAuthCheck.php b/tests/phpunit/tests/functions/wpAuthCheck.php index 70c1780faa537..639634b2b7098 100644 --- a/tests/phpunit/tests/functions/wpAuthCheck.php +++ b/tests/phpunit/tests/functions/wpAuthCheck.php @@ -3,7 +3,8 @@ /** * Tests for the behavior of `wp_auth_check()` * - * @group functions.php + * @group functions + * * @covers ::is_user_logged_in * @covers ::wp_auth_check */ diff --git a/tests/phpunit/tests/functions/wpCacheGetMultipleSalted.php b/tests/phpunit/tests/functions/wpCacheGetMultipleSalted.php new file mode 100644 index 0000000000000..196488b616a8b --- /dev/null +++ b/tests/phpunit/tests/functions/wpCacheGetMultipleSalted.php @@ -0,0 +1,117 @@ +<?php + +/** + * Tests for the behavior of `wp_cache_get_multiple_salted()` + * + * @group functions + * @group cache + * + * @covers ::wp_cache_get_multiple_salted + */ +class Tests_Functions_wpCacheGetMultipleSalted extends WP_UnitTestCase { + + /** + * Test that wp_cache_get_multiple_salted returns the cached data. + * + * @ticket 59592 + */ + public function test_wp_cache_get_multiple_salted_return_data() { + $last_changed = wp_cache_get_last_changed( 'query_data' ); + $cache_value = array( + 'salt' => $last_changed, + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), + ); + wp_cache_set( 'cache_key', $cache_value, 'query_data' ); + + $result = wp_cache_get_multiple_salted( array( 'cache_key' ), 'query_data', $last_changed ); + + $this->assertSameSets( $cache_value['data'], $result['cache_key'] ); + } + + /** + * Test that wp_cache_get_multiple_salted returns the cached data with a salt. + * + * @ticket 59592 + */ + public function test_wp_cache_get_multiple_salted_return_data_array_salt() { + $last_changed = array( + wp_cache_get_last_changed( 'query_data_1' ), + wp_cache_get_last_changed( 'query_data_2' ), + ); + $last_changed_string = implode( ':', $last_changed ); + $cache_value = array( + 'salt' => $last_changed_string, + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), + ); + wp_cache_set( 'cache_key', $cache_value, 'query_data' ); + + $result = wp_cache_get_multiple_salted( array( 'cache_key' ), 'query_data', $last_changed ); + + $this->assertSameSets( $cache_value['data'], $result['cache_key'] ); + } + + /** + * Test that wp_cache_get_multiple_salted returns an array of false values when no data is cached. + * + * @ticket 59592 + */ + public function test_wp_cache_get_multiple_salted_return_false() { + wp_cache_set( 'cache_key', false, 'query_data' ); + wp_cache_set( 'another_key', null, 'query_data' ); + + $last_changed = wp_cache_get_last_changed( 'query_data' ); + + $result = wp_cache_get_multiple_salted( array( 'cache_key', 'another_key' ), 'query_data', $last_changed ); + + $this->assertSameSets( + array( + 'cache_key' => false, + 'another_key' => false, + ), + $result + ); + } + + /** + * Test that wp_cache_get_multiple_salted returns the cached data for multiple keys. + * + * @ticket 59592 + */ + public function test_wp_cache_get_multiple_salted_with_some_false() { + $last_changed = wp_cache_get_last_changed( 'query_data' ); + wp_cache_set( + 'cache_key', + array( + 'salt' => $last_changed, + 'data' => array( 123 ), + ), + 'query_data' + ); + wp_cache_set( + 'another_key', + array( + 'salt' => '123', + 'data' => array(), + ), + 'query_data' + ); + + $last_changed = wp_cache_get_last_changed( 'query_data' ); + + $result = wp_cache_get_multiple_salted( array( 'cache_key', 'another_key' ), 'query_data', $last_changed ); + + $this->assertSameSets( + array( + 'cache_key' => array( 123 ), + 'another_key' => false, + ), + $result + ); + } +} diff --git a/tests/phpunit/tests/functions/wpCacheGetSalted.php b/tests/phpunit/tests/functions/wpCacheGetSalted.php new file mode 100644 index 0000000000000..d54a68274eee6 --- /dev/null +++ b/tests/phpunit/tests/functions/wpCacheGetSalted.php @@ -0,0 +1,95 @@ +<?php + +/** + * Tests for the behavior of `wp_cache_get_salted()` + * + * @group functions + * @group cache + * + * @covers ::wp_cache_get_salted + */ +class Tests_Functions_wpCacheGetSalted extends WP_UnitTestCase { + + /** + * Test that wp_cache_get_salted returns the cached data. + * + * @ticket 59592 + */ + public function test_wp_cache_get_salted_return_data() { + $last_changed = wp_cache_get_last_changed( 'query_data' ); + $cache_value = array( + 'salt' => $last_changed, + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), + ); + wp_cache_set( 'cache_key', $cache_value, 'query_data' ); + + $result = wp_cache_get_salted( 'cache_key', 'query_data', $last_changed ); + + $this->assertSameSets( $cache_value['data'], $result ); + } + + /** + * Test that wp_cache_get_salted returns the cached data with a salt. + * + * @ticket 59592 + */ + public function test_wp_cache_get_salted_return_data_array_salt() { + $last_changed = array( + wp_cache_get_last_changed( 'query_data_1' ), + wp_cache_get_last_changed( 'query_data_2' ), + ); + $last_changed_string = implode( ':', $last_changed ); + $cache_value = array( + 'salt' => $last_changed_string, + 'data' => array( + 'key1' => 'value1', + 'key2' => 'value2', + ), + ); + wp_cache_set( 'cache_key', $cache_value, 'query_data' ); + + $result = wp_cache_get_salted( 'cache_key', 'query_data', $last_changed ); + + $this->assertSameSets( $cache_value['data'], $result ); + } + + /** + * Test that wp_cache_get_salted returns false when no data is cached. + * + * @dataProvider data_wp_cache_get_salted_return_false + * + * @ticket 59592 + */ + public function test_wp_cache_get_salted_return_false( $cache_value ) { + wp_cache_set( 'cache_key', $cache_value, 'query_data' ); + $last_changed = wp_cache_get_last_changed( 'query_data' ); + $this->assertFalse( wp_cache_get_salted( 'cache_key', 'query_data', $last_changed ) ); + } + + /** + * Data provider for test_wp_cache_get_salted_return_false. + * + * @return array[] Data provider. + */ + public function data_wp_cache_get_salted_return_false() { + return array( + array( false ), + array( null ), + array( '' ), + array( 0 ), + array( array() ), + array( new StdClass() ), + array( array( 'salt' => '123' ) ), + array( + array( + 'salt' => '123', + 'data' => array(), + ), + ), + array( array( 'data' => array() ) ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpCacheSetLastChanged.php b/tests/phpunit/tests/functions/wpCacheSetLastChanged.php new file mode 100644 index 0000000000000..f40980bcc3010 --- /dev/null +++ b/tests/phpunit/tests/functions/wpCacheSetLastChanged.php @@ -0,0 +1,37 @@ +<?php + +/** + * Tests for the wp_cache_set_last_changed() function. + * + * @group functions + * @group cache + * + * @covers ::wp_cache_set_last_changed + */ +class Tests_Functions_wpCacheSetLastChanged extends WP_UnitTestCase { + + /** + * Check the cache key last_changed is set for the specified group. + * + * @ticket 59737 + */ + public function test_wp_cache_set_last_changed() { + $group = 'group_name'; + + $this->assertSame( wp_cache_set_last_changed( $group ), wp_cache_get( 'last_changed', $group ) ); + } + + /** + * Check the action is called. + * + * @ticket 59737 + */ + public function test_wp_cache_set_last_changed_action_is_called() { + $a1 = new MockAction(); + add_action( 'wp_cache_set_last_changed', array( $a1, 'action' ) ); + + wp_cache_set_last_changed( 'group_name' ); + + $this->assertSame( 1, $a1->get_call_count() ); + } +} diff --git a/tests/phpunit/tests/functions/wpCacheSetMultipleSalted.php b/tests/phpunit/tests/functions/wpCacheSetMultipleSalted.php new file mode 100644 index 0000000000000..be48428a70096 --- /dev/null +++ b/tests/phpunit/tests/functions/wpCacheSetMultipleSalted.php @@ -0,0 +1,75 @@ +<?php + +/** + * Tests for the behavior of `wp_cache_set_multiple_salted()` + * + * @group functions + * @group cache + * + * @covers ::wp_cache_set_salted + */ +class Tests_Functions_wpCacheSetMultipleSalted extends WP_UnitTestCase { + /** + * Test that wp_cache_set_multiple_salted sets multiple query data correctly. + * + * @ticket 59592 + */ + public function test_wp_cache_set_multiple_salted() { + $cache_group = 'query_data'; + $last_changed = wp_cache_get_last_changed( 'query_data' ); + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + + wp_cache_set_multiple_salted( $data, $cache_group, $last_changed ); + $cache_values = wp_cache_get_multiple( array( 'key1', 'key2' ), $cache_group ); + $expected_cache_values = array( + 'key1' => array( + + 'data' => 'value1', + 'salt' => $last_changed, + ), + 'key2' => array( + + 'data' => 'value2', + 'salt' => $last_changed, + ), + ); + $this->assertSameSets( $expected_cache_values, $cache_values ); + } + + /** + * Test that wp_cache_set_multiple_salted sets multiple query data with a salt. + * + * @ticket 59592 + */ + public function test_wp_cache_set_multiple_salted_array() { + $cache_group = 'query_data'; + $last_changed = array( + wp_cache_get_last_changed( 'query_data_1' ), + wp_cache_get_last_changed( 'query_data_2' ), + ); + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + + wp_cache_set_multiple_salted( $data, $cache_group, $last_changed ); + $cache_values = wp_cache_get_multiple( array( 'key1', 'key2' ), $cache_group ); + $last_changed_string = implode( ':', $last_changed ); + $expected_cache_values = array( + 'key1' => array( + + 'data' => 'value1', + 'salt' => $last_changed_string, + ), + 'key2' => array( + + 'data' => 'value2', + 'salt' => $last_changed_string, + ), + ); + $this->assertSameSets( $expected_cache_values, $cache_values ); + } +} diff --git a/tests/phpunit/tests/functions/wpCacheSetSalted.php b/tests/phpunit/tests/functions/wpCacheSetSalted.php new file mode 100644 index 0000000000000..33cac4aadcb94 --- /dev/null +++ b/tests/phpunit/tests/functions/wpCacheSetSalted.php @@ -0,0 +1,57 @@ +<?php + +/** + * @group functions + * + * @covers ::wp_cache_set_salted + */ +class Tests_Functions_wpCacheSetSalted extends WP_UnitTestCase { + + /** + * Test that wp_cache_set_salted sets the data correctly. + * + * @ticket 59592 + */ + public function test_wp_cache_set_salted() { + $cache_key = 'cache_key'; + $cache_group = 'query_data'; + $last_changed = wp_cache_get_last_changed( 'query_data' ); + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + + wp_cache_set_salted( $cache_key, $data, $cache_group, $last_changed ); + + $cached_data = wp_cache_get( $cache_key, 'query_data' ); + + $this->assertSame( $data, $cached_data['data'], 'The data key should contain the cached data.' ); + $this->assertSame( $last_changed, $cached_data['salt'], 'The last changed key should contain the last change time stamp' ); + } + + /** + * Test that wp_cache_set_salted sets the data with a salt. + * + * @ticket 59592 + */ + public function test_wp_cache_set_salted_array_salt() { + $cache_key = 'cache_key'; + $cache_group = 'query_data'; + $last_changed = array( + wp_cache_get_last_changed( 'query_data_1' ), + wp_cache_get_last_changed( 'query_data_2' ), + ); + $data = array( + 'key1' => 'value1', + 'key2' => 'value2', + ); + + wp_cache_set_salted( $cache_key, $data, $cache_group, $last_changed ); + + $cached_data = wp_cache_get( $cache_key, 'query_data' ); + + $last_changed_string = implode( ':', $last_changed ); + $this->assertSame( $data, $cached_data['data'], 'The data key should contain the cached data.' ); + $this->assertSame( $last_changed_string, $cached_data['salt'], 'The last changed key should contain the last change time stamp' ); + } +} diff --git a/tests/phpunit/tests/functions/wpCheckAlternateFileNames.php b/tests/phpunit/tests/functions/wpCheckAlternateFileNames.php new file mode 100644 index 0000000000000..47a99244d726a --- /dev/null +++ b/tests/phpunit/tests/functions/wpCheckAlternateFileNames.php @@ -0,0 +1,69 @@ +<?php + +/** + * @group functions + * + * @covers ::_wp_check_alternate_file_names + */ +class Tests_Functions_WpCheckAlternateFileNames extends WP_UnitTestCase { + + /** + * @dataProvider data_wp_check_alternate_file_names + * + * @ticket 55199 + * + * @param array $filenames Array of filenames to check. + * @param string $dir The directory to check. + * @param array $files An array of existing files in the directory. + * @param bool $expected Expected result. + */ + public function test_wp_check_alternate_file_names( $filenames, $dir, $files, $expected ) { + $this->assertSame( $expected, _wp_check_alternate_file_names( $filenames, $dir, $files ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_check_alternate_file_names() { + return array( + 'an existing file' => array( + 'filenames' => array( 'canola.jpg' ), + 'dir' => DIR_TESTDATA . '/images/', + 'files' => array(), + 'expected' => true, + ), + 'multiple existing files' => array( + 'filenames' => array( 'canola.jpg', 'codeispoetry.png' ), + 'dir' => DIR_TESTDATA . '/images/', + 'files' => array(), + 'expected' => true, + ), + 'a non-existent file and an existing file' => array( + 'filenames' => array( 'an-image.jpg', 'codeispoetry.png' ), + 'dir' => DIR_TESTDATA . '/images/', + 'files' => array(), + 'expected' => true, + ), + 'a non-existent file and an existing image sub-size file' => array( + 'filenames' => array( 'one-blue-pixel.png' ), + 'dir' => DIR_TESTDATA . '/images/', + 'files' => array( 'one-blue-pixel-100x100.png' ), + 'expected' => true, + ), + 'a non-existent file and no other existing files' => array( + 'filenames' => array( 'filename.php' ), + 'dir' => DIR_TESTDATA . '/images/', + 'files' => array(), + 'expected' => false, + ), + 'multiple non-existent files and no existing image sub-size files' => array( + 'filenames' => array( 'canola.jpg', 'codeispoetry.png' ), + 'dir' => DIR_TESTDATA . '/functions/', + 'files' => array( 'an-image-100x100.jpg', 'another-image-100x100.png' ), + 'expected' => false, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpCheckFiletype.php b/tests/phpunit/tests/functions/wpCheckFiletype.php new file mode 100644 index 0000000000000..37d7afdd01696 --- /dev/null +++ b/tests/phpunit/tests/functions/wpCheckFiletype.php @@ -0,0 +1,110 @@ +<?php + +/** + * Tests for wp_check_filetype(). + * + * @group functions + * @group upload + * + * @covers ::wp_check_filetype + */ +class Tests_Functions_WpCheckFiletype extends WP_UnitTestCase { + + /** + * Tests that wp_check_filetype() returns the correct extension and MIME type. + * + * @ticket 57151 + * + * @dataProvider data_wp_check_filetype + * + * @param string $filename The filename to check. + * @param array|null $mimes An array of MIME types, or null. + * @param array $expected An array containing the expected extension and MIME type. + */ + public function test_wp_check_filetype( $filename, $mimes, $expected ) { + $this->assertSame( $expected, wp_check_filetype( $filename, $mimes ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_check_filetype() { + return array( + '.jpg filename and default allowed' => array( + 'filename' => 'canola.jpg', + 'mimes' => null, + 'expected' => array( + 'ext' => 'jpg', + 'type' => 'image/jpeg', + ), + ), + '.jpg filename and jpg|jpeg|jpe' => array( + 'filename' => 'canola.jpg', + 'mimes' => array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => 'jpg', + 'type' => 'image/jpeg', + ), + ), + '.jpeg filename and jpg|jpeg|jpe' => array( + 'filename' => 'canola.jpeg', + 'mimes' => array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => 'jpeg', + 'type' => 'image/jpeg', + ), + ), + '.jpe filename and jpg|jpeg|jpe' => array( + 'filename' => 'canola.jpe', + 'mimes' => array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => 'jpe', + 'type' => 'image/jpeg', + ), + ), + 'uppercase filename and jpg|jpeg|jpe' => array( + 'filename' => 'canola.JPG', + 'mimes' => array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => 'JPG', + 'type' => 'image/jpeg', + ), + ), + '.XXX filename and no matching MIME type' => array( + 'filename' => 'canola.XXX', + 'mimes' => array( + 'jpg|jpeg|jpe' => 'image/jpeg', + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => false, + 'type' => false, + ), + ), + '.jpg filename but only gif allowed' => array( + 'filename' => 'canola.jpg', + 'mimes' => array( + 'gif' => 'image/gif', + ), + 'expected' => array( + 'ext' => false, + 'type' => false, + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpDeleteFile.php b/tests/phpunit/tests/functions/wpDeleteFile.php new file mode 100644 index 0000000000000..33de72a9ed1d4 --- /dev/null +++ b/tests/phpunit/tests/functions/wpDeleteFile.php @@ -0,0 +1,38 @@ +<?php + +/** + * Tests for wp_delete_file(). + * + * @group functions + * + * @covers ::wp_delete_file + */ +class Tests_Functions_WpDeleteFile extends WP_UnitTestCase { + + /** + * @ticket 61590 + */ + public function test_wp_delete_file() { + $file = wp_tempnam( 'a_file_that_exists.txt' ); + + $this->assertTrue( wp_delete_file( $file ), 'File deletion failed.' ); + $this->assertFileDoesNotExist( $file, 'The file was not deleted.' ); + } + + /** + * @ticket 61590 + */ + public function test_wp_delete_file_with_empty_path() { + $this->assertFalse( wp_delete_file( '' ) ); + } + + /** + * @ticket 61590 + */ + public function test_wp_delete_file_with_file_that_does_not_exist() { + $file = DIR_TESTDATA . '/a_file_that_does_not_exist.txt'; + + $this->assertFileDoesNotExist( $file, "$file already existed as a file before testing." ); + $this->assertFalse( wp_delete_file( $file ), 'Attempting to delete a non-existent file should return false.' ); + } +} diff --git a/tests/phpunit/tests/functions/wpFilesize.php b/tests/phpunit/tests/functions/wpFilesize.php new file mode 100644 index 0000000000000..4ed6de5676bd2 --- /dev/null +++ b/tests/phpunit/tests/functions/wpFilesize.php @@ -0,0 +1,54 @@ +<?php + +/** + * Tests for the wp_filesize() function. + * + * @group functions + * + * @covers ::wp_filesize + */ +class Tests_Functions_wpFilesize extends WP_UnitTestCase { + + /** + * @ticket 49412 + */ + public function test_wp_filesize() { + $file = DIR_TESTDATA . '/images/test-image-upside-down.jpg'; + + $this->assertSame( filesize( $file ), wp_filesize( $file ) ); + } + + /** + * @ticket 49412 + */ + public function test_wp_filesize_filters() { + $file = DIR_TESTDATA . '/images/test-image-upside-down.jpg'; + + add_filter( + 'wp_filesize', + static function () { + return 999; + } + ); + + $this->assertSame( 999, wp_filesize( $file ) ); + + add_filter( + 'pre_wp_filesize', + static function () { + return 111; + } + ); + + $this->assertSame( 111, wp_filesize( $file ) ); + } + + /** + * @ticket 49412 + */ + public function test_wp_filesize_with_nonexistent_file() { + $file = 'nonexistent/file.jpg'; + + $this->assertSame( 0, wp_filesize( $file ) ); + } +} diff --git a/tests/phpunit/tests/functions/wpFilterObjectList.php b/tests/phpunit/tests/functions/wpFilterObjectList.php index b0c9edc4eda47..21b633e352e38 100644 --- a/tests/phpunit/tests/functions/wpFilterObjectList.php +++ b/tests/phpunit/tests/functions/wpFilterObjectList.php @@ -3,7 +3,7 @@ /** * Test wp_filter_object_list(). * - * @group functions.php + * @group functions * @covers ::wp_filter_object_list */ class Tests_Functions_wpFilterObjectList extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/wpFuzzyNumberMatch.php b/tests/phpunit/tests/functions/wpFuzzyNumberMatch.php new file mode 100644 index 0000000000000..1935edefefea4 --- /dev/null +++ b/tests/phpunit/tests/functions/wpFuzzyNumberMatch.php @@ -0,0 +1,112 @@ +<?php + +/** + * Test wp_fuzzy_number_match(). + * + * @group functions + * + * @covers ::wp_fuzzy_number_match + */ +class Tests_Functions_wpFuzzyNumberMatch extends WP_UnitTestCase { + + /** + * @dataProvider data_wp_fuzzy_number_match + * + * @ticket 54239 + * + * @param int|float $expected The expected value. + * @param int|float $actual The actual number. + * @param int|float $precision The allowed variation. + * @param bool $result Whether the numbers match within the specified precision. + */ + public function test_wp_fuzzy_number_match( $expected, $actual, $precision, $result ) { + $this->assertSame( $result, wp_fuzzy_number_match( $expected, $actual, $precision ) ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type int|float $expected The expected value. + * @type int|float $actual The actual number. + * @type int|float $precision The allowed variation. + * @type bool $result Whether the numbers match within the specified precision. + * } + */ + public function data_wp_fuzzy_number_match() { + return array( + 'expected 1 int, actual 1 int' => array( + 'expected' => 1, + 'actual' => 1, + 'precision' => 1, + 'result' => true, + ), + 'expected 1 int, actual 2 int' => array( + 'expected' => 1, + 'actual' => 2, + 'precision' => 1, + 'result' => true, + ), + 'expected 1 int, actual 3 int' => array( + 'expected' => 1, + 'actual' => 3, + 'precision' => 1, + 'result' => false, + ), + 'expected 1 int, actual 1 string' => array( + 'expected' => 1, + 'actual' => '1', + 'precision' => 1, + 'result' => true, + ), + 'expected 1 int, actual 11 int, precision 10' => array( + 'expected' => 1, + 'actual' => 11, + 'precision' => 10, + 'result' => true, + ), + 'expected 1 int, actual 12 int, precision 10' => array( + 'expected' => 1, + 'actual' => 12, + 'precision' => 10, + 'result' => false, + ), + 'expected 1.234 float, actual 1 int' => array( + 'expected' => 1.234, + 'actual' => 1, + 'precision' => 1, + 'result' => true, + ), + 'expected 2.234 float, actual 2 int' => array( + 'expected' => 1.234, + 'actual' => 2, + 'precision' => 1, + 'result' => true, + ), + 'expected 1 int, actual 2.0001 float' => array( + 'expected' => 1, + 'actual' => 2.0001, + 'precision' => 1, + 'result' => false, + ), + 'expected 1 int, actual 3.23 float' => array( + 'expected' => 1, + 'actual' => 3.234, + 'precision' => 1, + 'result' => false, + ), + 'expected 1.2e1 float (12), actual 1.3e1 float (13)' => array( + 'expected' => 1.2e1, + 'actual' => 1.3e1, + 'precision' => 1, + 'result' => true, + ), + 'expected 1.2e3 float (1200), actual 1.2e3 float, precision 1000' => array( + 'expected' => 1.2e3, + 'actual' => 1.2e3, + 'precision' => 1000, + 'result' => true, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpGetAdminNotice.php b/tests/phpunit/tests/functions/wpGetAdminNotice.php new file mode 100644 index 0000000000000..41b24a122be88 --- /dev/null +++ b/tests/phpunit/tests/functions/wpGetAdminNotice.php @@ -0,0 +1,326 @@ +<?php + +/** + * Tests for `wp_get_admin_notice()`. + * + * @group functions + * + * @covers ::wp_get_admin_notice + */ +class Tests_Functions_WpGetAdminNotice extends WP_UnitTestCase { + + /** + * Tests that `wp_get_admin_notice()` returns the expected admin notice markup. + * + * @ticket 57791 + * + * @dataProvider data_should_return_admin_notice + * + * @param string $message The message. + * @param array $args Arguments for the admin notice. + * @param string $expected The expected admin notice markup. + */ + public function test_should_return_admin_notice( $message, $args, $expected ) { + $this->assertSame( $expected, wp_get_admin_notice( $message, $args ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_return_admin_notice() { + return array( + 'defaults' => array( + 'message' => 'A notice with defaults.', + 'args' => array(), + 'expected' => '<div class="notice"><p>A notice with defaults.</p></div>', + ), + 'an empty message (used for templates)' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + ), + 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"><p></p></div>', + ), + 'an empty message (used for templates) without paragraph wrapping' => array( + 'message' => '', + 'args' => array( + 'type' => 'error', + 'dismissible' => true, + 'id' => 'message', + 'additional_classes' => array( 'inline', 'hidden' ), + 'paragraph_wrap' => false, + ), + 'expected' => '<div id="message" class="notice notice-error is-dismissible inline hidden"></div>', + ), + 'an "error" notice' => array( + 'message' => 'An "error" notice.', + 'args' => array( + 'type' => 'error', + ), + 'expected' => '<div class="notice notice-error"><p>An "error" notice.</p></div>', + ), + 'a "success" notice' => array( + 'message' => 'A "success" notice.', + 'args' => array( + 'type' => 'success', + ), + 'expected' => '<div class="notice notice-success"><p>A "success" notice.</p></div>', + ), + 'a "warning" notice' => array( + 'message' => 'A "warning" notice.', + 'args' => array( + 'type' => 'warning', + ), + 'expected' => '<div class="notice notice-warning"><p>A "warning" notice.</p></div>', + ), + 'an "info" notice' => array( + 'message' => 'An "info" notice.', + 'args' => array( + 'type' => 'info', + ), + 'expected' => '<div class="notice notice-info"><p>An "info" notice.</p></div>', + ), + 'a type that already starts with "notice-"' => array( + 'message' => 'A type that already starts with "notice-".', + 'args' => array( + 'type' => 'notice-info', + ), + 'expected' => '<div class="notice notice-notice-info"><p>A type that already starts with "notice-".</p></div>', + ), + 'a dismissible notice' => array( + 'message' => 'A dismissible notice.', + 'args' => array( + 'dismissible' => true, + ), + 'expected' => '<div class="notice is-dismissible"><p>A dismissible notice.</p></div>', + ), + 'no type and an ID' => array( + 'message' => 'A notice with an ID.', + 'args' => array( + 'id' => 'message', + ), + 'expected' => '<div id="message" class="notice"><p>A notice with an ID.</p></div>', + ), + 'a type and an ID' => array( + 'message' => 'A warning notice with an ID.', + 'args' => array( + 'type' => 'warning', + 'id' => 'message', + ), + 'expected' => '<div id="message" class="notice notice-warning"><p>A warning notice with an ID.</p></div>', + ), + 'no type and additional classes' => array( + 'message' => 'A notice with additional classes.', + 'args' => array( + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice error notice-alt"><p>A notice with additional classes.</p></div>', + ), + 'a type and additional classes' => array( + 'message' => 'A warning notice with additional classes.', + 'args' => array( + 'type' => 'warning', + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice notice-warning error notice-alt"><p>A warning notice with additional classes.</p></div>', + ), + 'a dismissible notice with a type and additional classes' => array( + 'message' => 'A dismissible warning notice with a type and additional classes.', + 'args' => array( + 'type' => 'warning', + 'dismissible' => true, + 'additional_classes' => array( 'error', 'notice-alt' ), + ), + 'expected' => '<div class="notice notice-warning is-dismissible error notice-alt"><p>A dismissible warning notice with a type and additional classes.</p></div>', + ), + 'a notice without paragraph wrapping' => array( + 'message' => '<span>A notice without paragraph wrapping.</span>', + 'args' => array( + 'paragraph_wrap' => false, + ), + 'expected' => '<div class="notice"><span>A notice without paragraph wrapping.</span></div>', + ), + 'an unsafe type' => array( + 'message' => 'A notice with an unsafe type.', + 'args' => array( + 'type' => '"><script>alert("Howdy,admin!");</script>', + ), + 'expected' => '<div class="notice notice-"><script>alert("Howdy,admin!");</script>"><p>A notice with an unsafe type.</p></div>', + ), + 'an unsafe ID' => array( + 'message' => 'A notice with an unsafe ID.', + 'args' => array( + 'id' => '"><script>alert( "Howdy, admin!" );</script> <div class="notice', + ), + 'expected' => '<div id=""><script>alert( "Howdy, admin!" );</script> <div class="notice" class="notice"><p>A notice with an unsafe ID.</p></div>', + ), + 'unsafe additional classes' => array( + 'message' => 'A notice with unsafe additional classes.', + 'args' => array( + 'additional_classes' => array( '"><script>alert( "Howdy, admin!" );</script> <div class="notice' ), + ), + 'expected' => '<div class="notice "><script>alert( "Howdy, admin!" );</script> <div class="notice"><p>A notice with unsafe additional classes.</p></div>', + ), + 'a type that is not a string' => array( + 'message' => 'A notice with a type that is not a string.', + 'args' => array( + 'type' => array(), + ), + 'expected' => '<div class="notice"><p>A notice with a type that is not a string.</p></div>', + ), + 'a type with only empty space' => array( + 'message' => 'A notice with a type with only empty space.', + 'args' => array( + 'type' => " \t\r\n", + ), + 'expected' => '<div class="notice"><p>A notice with a type with only empty space.</p></div>', + ), + 'an ID that is not a string' => array( + 'message' => 'A notice with an ID that is not a string.', + 'args' => array( + 'id' => array( 'message' ), + ), + 'expected' => '<div class="notice"><p>A notice with an ID that is not a string.</p></div>', + ), + 'an ID with only empty space' => array( + 'message' => 'A notice with an ID with only empty space.', + 'args' => array( + 'id' => " \t\r\n", + ), + 'expected' => '<div class="notice"><p>A notice with an ID with only empty space.</p></div>', + ), + 'dismissible as a truthy value rather than (bool) true' => array( + 'message' => 'A notice with dismissible as a truthy value rather than (bool) true.', + 'args' => array( + 'dismissible' => 1, + ), + 'expected' => '<div class="notice"><p>A notice with dismissible as a truthy value rather than (bool) true.</p></div>', + ), + 'additional classes that are not an array' => array( + 'message' => 'A notice with additional classes that are not an array.', + 'args' => array( + 'additional_classes' => 'class-1 class-2 class-3', + ), + 'expected' => '<div class="notice"><p>A notice with additional classes that are not an array.</p></div>', + ), + 'additional attribute with a value' => array( + 'message' => 'A notice with an additional attribute with a value.', + 'args' => array( + 'attributes' => array( 'aria-live' => 'assertive' ), + ), + 'expected' => '<div class="notice" aria-live="assertive"><p>A notice with an additional attribute with a value.</p></div>', + ), + 'additional hidden attribute' => array( + 'message' => 'A notice with the hidden attribute.', + 'args' => array( + 'attributes' => array( 'hidden' => true ), + ), + 'expected' => '<div class="notice" hidden><p>A notice with the hidden attribute.</p></div>', + ), + 'additional attribute no associative keys' => array( + 'message' => 'A notice with a boolean attribute without an associative key.', + 'args' => array( + 'attributes' => array( 'hidden' ), + ), + 'expected' => '<div class="notice" hidden><p>A notice with a boolean attribute without an associative key.</p></div>', + ), + 'additional attribute with role' => array( + 'message' => 'A notice with an additional attribute role.', + 'args' => array( + 'attributes' => array( 'role' => 'alert' ), + ), + 'expected' => '<div class="notice" role="alert"><p>A notice with an additional attribute role.</p></div>', + ), + 'multiple additional attributes' => array( + 'message' => 'A notice with multiple additional attributes.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'data-test' => -1, + ), + ), + 'expected' => '<div class="notice" role="alert" data-test="-1"><p>A notice with multiple additional attributes.</p></div>', + ), + 'data attribute with unsafe value' => array( + 'message' => 'A notice with an additional attribute with an unsafe value.', + 'args' => array( + 'attributes' => array( 'data-unsafe' => '<script>alert( "Howdy, admin!" );</script>' ), + ), + 'expected' => '<div class="notice" data-unsafe="<script>alert( "Howdy, admin!" );</script>"><p>A notice with an additional attribute with an unsafe value.</p></div>', + ), + 'multiple attributes with "role", invalid, data-*, numeric, and boolean' => array( + 'message' => 'A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.', + 'args' => array( + 'attributes' => array( + 'role' => 'alert', + 'disabled' => 'disabled', + 'data-name' => 'my-name', + 'data-id' => 1, + 'hidden', + ), + ), + 'expected' => '<div class="notice" role="alert" disabled="disabled" data-name="my-name" data-id="1" hidden><p>A notice with multiple attributes with "role", invalid, "data-*", numeric, and boolean.</p></div>', + ), + 'paragraph wrapping as a falsy value rather than (bool) false' => array( + 'message' => 'A notice with paragraph wrapping as a falsy value rather than (bool) false.', + 'args' => array( + 'paragraph_wrap' => 0, + ), + 'expected' => '<div class="notice"><p>A notice with paragraph wrapping as a falsy value rather than (bool) false.</p></div>', + ), + ); + } + + /** + * Tests that `wp_get_admin_notice()` throws a `_doing_it_wrong()` when + * a 'type' containing spaces is passed. + * + * @ticket 57791 + * + * @expectedIncorrectUsage wp_get_admin_notice + */ + public function test_should_throw_doing_it_wrong_with_a_type_containing_spaces() { + $this->assertSame( + '<div class="notice notice-first second third fourth"><p>A type containing spaces.</p></div>', + wp_get_admin_notice( + 'A type containing spaces.', + array( 'type' => 'first second third fourth' ) + ) + ); + } + + /** + * Tests that `wp_get_admin_notice()` applies filters. + * + * @ticket 57791 + * + * @dataProvider data_should_apply_filters + * + * @param string $hook_name The name of the filter hook. + */ + public function test_should_apply_filters( $hook_name ) { + $filter = new MockAction(); + add_filter( $hook_name, array( $filter, 'filter' ) ); + + wp_get_admin_notice( 'A notice.', array( 'type' => 'success' ) ); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_apply_filters() { + return array( + 'wp_admin_notice_args' => array( 'hook_name' => 'wp_admin_notice_args' ), + 'wp_admin_notice_markup' => array( 'hook_name' => 'wp_admin_notice_markup' ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpGetArchives.php b/tests/phpunit/tests/functions/wpGetArchives.php index 6d0eaded289fb..99967e9be3211 100644 --- a/tests/phpunit/tests/functions/wpGetArchives.php +++ b/tests/phpunit/tests/functions/wpGetArchives.php @@ -1,7 +1,8 @@ <?php /** - * @group functions.php + * @group functions + * * @covers ::wp_get_archives */ class Tests_Functions_wpGetArchives extends WP_UnitTestCase { @@ -203,4 +204,39 @@ public function test_wp_get_archives_post_type() { ); $this->assertSame( $expected, trim( $archives ) ); } + + /** + * @ticket 64304 + */ + public function test_wp_get_archives_args_filter() { + // Test that the filter can modify the limit argument. + add_filter( + 'wp_get_archives_args', + static function ( $args ) { + $args['limit'] = 3; + return $args; + } + ); + + $ids = array_slice( array_reverse( self::$post_ids ), 0, 3 ); + + $expected = join( + '', + array_map( + static function ( $id ) { + return sprintf( "\t<li><a href='%s'>%s</a></li>\n", get_permalink( $id ), get_the_title( $id ) ); + }, + $ids + ) + ); + $archives = wp_get_archives( + array( + 'echo' => false, + 'type' => 'postbypost', + 'limit' => 5, // This should be overridden by the filter to 3. + ) + ); + + $this->assertEqualHTML( $expected, $archives ); + } } diff --git a/tests/phpunit/tests/functions/wpGetMimeTypes.php b/tests/phpunit/tests/functions/wpGetMimeTypes.php index 2bef07644f4cb..8bcd0aed8a9dc 100644 --- a/tests/phpunit/tests/functions/wpGetMimeTypes.php +++ b/tests/phpunit/tests/functions/wpGetMimeTypes.php @@ -3,7 +3,8 @@ /** * Test wp_get_mime_types(). * - * @group functions.php + * @group functions + * * @covers ::wp_get_mime_types */ class Tests_Functions_wpGetMimeTypes extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/functions/wpGetWpVersion.php b/tests/phpunit/tests/functions/wpGetWpVersion.php new file mode 100644 index 0000000000000..d10d946e3baea --- /dev/null +++ b/tests/phpunit/tests/functions/wpGetWpVersion.php @@ -0,0 +1,34 @@ +<?php + +/** + * Tests for wp_get_wp_version(). + * + * @group functions + * + * @covers ::wp_get_wp_version + */ +class Tests_Functions_WpGetWpVersion extends WP_UnitTestCase { + + /** + * Tests that the WordPress version is returned. + * + * @ticket 61627 + */ + public function test_should_return_wp_version() { + $this->assertSame( $GLOBALS['wp_version'], wp_get_wp_version() ); + } + + /** + * Tests that changes to the `$wp_version` global are ignored. + * + * @ticket 61627 + */ + public function test_should_ignore_changes_to_wp_version_global() { + $original_wp_version = $GLOBALS['wp_version']; + $GLOBALS['wp_version'] = 'modified_wp_version'; + $actual = wp_get_wp_version(); + $GLOBALS['wp_version'] = $original_wp_version; + + $this->assertSame( $original_wp_version, $actual ); + } +} diff --git a/tests/phpunit/tests/functions/wpGuessUrl.php b/tests/phpunit/tests/functions/wpGuessUrl.php new file mode 100644 index 0000000000000..88347c7f17524 --- /dev/null +++ b/tests/phpunit/tests/functions/wpGuessUrl.php @@ -0,0 +1,39 @@ +<?php + +/** + * Test wp_guess_url(). + * + * @group functions + * + * @covers ::wp_guess_url + */ +class Tests_Functions_wpGuessUrl extends WP_UnitTestCase { + + /** + * @ticket 36827 + * + * @dataProvider data_wp_guess_url_should_return_site_url + * + * @param string $url The URL to navigate to, relative to `site_url()`. + */ + public function test_wp_guess_url_should_return_site_url( $url ) { + $siteurl = site_url(); + $this->go_to( site_url( $url ) ); + $this->assertSame( $siteurl, wp_guess_url() ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_guess_url_should_return_site_url() { + return array( + 'no trailing slash' => array( 'url' => 'wp-admin' ), + 'trailing slash' => array( 'url' => 'wp-admin/' ), + 'trailing slash, query var' => array( 'url' => 'wp-admin/?foo=bar' ), + 'file extension, no trailing slash' => array( 'url' => 'wp-login.php' ), + 'file extension, query var, no trailing slash' => array( 'url' => 'wp-login.php?foo=bar' ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpHash.php b/tests/phpunit/tests/functions/wpHash.php new file mode 100644 index 0000000000000..63744b188dace --- /dev/null +++ b/tests/phpunit/tests/functions/wpHash.php @@ -0,0 +1,39 @@ +<?php + +/** + * Tests for the behavior of `wp_hash()` + * + * @group functions + * + * @covers ::wp_hash + */ +class Tests_Functions_wpHash extends WP_UnitTestCase { + + /** + * @dataProvider data_wp_hash_uses_specified_algorithm + * + * @ticket 62005 + */ + public function test_wp_hash_uses_specified_algorithm( string $algo, int $expected_length ) { + $hash = wp_hash( 'data', 'auth', $algo ); + + $this->assertSame( $expected_length, strlen( $hash ) ); + } + + public function data_wp_hash_uses_specified_algorithm() { + return array( + array( 'md5', 32 ), + array( 'sha1', 40 ), + array( 'sha256', 64 ), + ); + } + + /** + * @ticket 62005 + */ + public function test_wp_hash_throws_exception_on_invalid_algorithm() { + $this->expectException( 'InvalidArgumentException' ); + + wp_hash( 'data', 'auth', 'invalid' ); + } +} diff --git a/tests/phpunit/tests/functions/wpIsNumericArray.php b/tests/phpunit/tests/functions/wpIsNumericArray.php new file mode 100644 index 0000000000000..4eeab0af81f2a --- /dev/null +++ b/tests/phpunit/tests/functions/wpIsNumericArray.php @@ -0,0 +1,71 @@ +<?php + +/** + * @group functions + * + * @covers ::wp_is_numeric_array + */ +class Tests_Functions_wpIsNumericArray extends WP_UnitTestCase { + + /** + * @dataProvider data_wp_is_numeric_array + * + * @ticket 53971 + * + * @param mixed $input Input to test. + * @param array $expected Expected result. + */ + public function test_wp_is_numeric_array( $input, $expected ) { + $this->assertSame( $expected, wp_is_numeric_array( $input ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_is_numeric_array() { + return array( + 'no index' => array( + 'test_array' => array( 'www', 'eee' ), + 'expected' => true, + ), + 'text index' => array( + 'test_array' => array( 'www' => 'eee' ), + 'expected' => false, + ), + 'numeric index' => array( + 'test_array' => array( 99 => 'eee' ), + 'expected' => true, + ), + '- numeric index' => array( + 'test_array' => array( -11 => 'eee' ), + 'expected' => true, + ), + 'numeric string index' => array( + 'test_array' => array( '11' => 'eee' ), + 'expected' => true, + ), + 'nested number index' => array( + 'test_array' => array( + 'next' => array( + 11 => 'vvv', + ), + ), + 'expected' => false, + ), + 'nested string index' => array( + 'test_array' => array( + '11' => array( + 'eee' => 'vvv', + ), + ), + 'expected' => true, + ), + 'not an array' => array( + 'test_array' => null, + 'expected' => false, + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpListFilter.php b/tests/phpunit/tests/functions/wpListFilter.php index 45ea4f9601f6e..d9fca8cf5c494 100644 --- a/tests/phpunit/tests/functions/wpListFilter.php +++ b/tests/phpunit/tests/functions/wpListFilter.php @@ -3,25 +3,31 @@ /** * Test wp_list_filter(). * - * @group functions.php + * @group functions + * * @covers ::wp_list_filter */ class Tests_Functions_wpListFilter extends WP_UnitTestCase { /** - * @dataProvider data_test_wp_list_filter + * @dataProvider data_wp_list_filter * - * @param array $list An array of objects to filter. - * @param array $args An array of key => value arguments to match - * against each object. - * @param string $operator The logical operation to perform. - * @param array $expected Expected result. + * @param array $input_list An array of objects to filter. + * @param array $args An array of key => value arguments to match + * against each object. + * @param string $operator The logical operation to perform. + * @param array $expected Expected result. */ - public function test_wp_list_filter( $list, $args, $operator, $expected ) { - $this->assertEqualSetsWithIndex( $expected, wp_list_filter( $list, $args, $operator ) ); + public function test_wp_list_filter( $input_list, $args, $operator, $expected ) { + $this->assertEqualSetsWithIndex( $expected, wp_list_filter( $input_list, $args, $operator ) ); } - public function data_test_wp_list_filter() { + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_list_filter() { return array( 'string instead of array' => array( 'foo', @@ -210,6 +216,20 @@ public function data_test_wp_list_filter() { ), ), ), + 'string to int comparison' => array( + array( + (object) array( + 'foo' => '1', + ), + ), + array( 'foo' => 1 ), + 'AND', + array( + 0 => (object) array( + 'foo' => '1', + ), + ), + ), ); } } diff --git a/tests/phpunit/tests/functions/wpListPluck.php b/tests/phpunit/tests/functions/wpListPluck.php index 10c78ed9bd3ff..8313116294519 100644 --- a/tests/phpunit/tests/functions/wpListPluck.php +++ b/tests/phpunit/tests/functions/wpListPluck.php @@ -3,7 +3,8 @@ /** * Test wp_list_pluck(). * - * @group functions.php + * @group functions + * * @covers ::wp_list_pluck */ class Tests_Functions_wpListPluck extends WP_UnitTestCase { @@ -11,7 +12,18 @@ class Tests_Functions_wpListPluck extends WP_UnitTestCase { public $array_list = array(); public function set_up() { - parent::set_up(); + /* + * This method deliberately does not call parent::set_up(). Why? + * + * The call stack for WP_UnitTestCase_Base::set_up() includes a call to + * WP_List_Util::pluck(), which creates an inaccurate coverage report + * for this method. + * + * To ensure that deprecation and incorrect usage notices continue to be + * detectable, this method uses WP_UnitTestCase_Base::expectDeprecated(). + */ + $this->expectDeprecated(); + $this->array_list['foo'] = array( 'name' => 'foo', 'id' => 'f', @@ -193,18 +205,23 @@ public function test_wp_list_pluck_containing_references_keys() { } /** - * @dataProvider data_test_wp_list_pluck + * @dataProvider data_wp_list_pluck * - * @param array $list List of objects or arrays. - * @param int|string $field Field from the object to place instead of the entire object - * @param int|string $index_key Field from the object to use as keys for the new array. - * @param array $expected Expected result. + * @param array $input_list List of objects or arrays. + * @param int|string $field Field from the object to place instead of the entire object + * @param int|string $index_key Field from the object to use as keys for the new array. + * @param array $expected Expected result. */ - public function test_wp_list_pluck( $list, $field, $index_key, $expected ) { - $this->assertSameSetsWithIndex( $expected, wp_list_pluck( $list, $field, $index_key ) ); + public function test_wp_list_pluck( $input_list, $field, $index_key, $expected ) { + $this->assertSameSetsWithIndex( $expected, wp_list_pluck( $input_list, $field, $index_key ) ); } - public function data_test_wp_list_pluck() { + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_list_pluck() { return array( 'arrays' => array( array( diff --git a/tests/phpunit/tests/functions/wpListSort.php b/tests/phpunit/tests/functions/wpListSort.php index 7159af4937687..8bcf8e54414ee 100644 --- a/tests/phpunit/tests/functions/wpListSort.php +++ b/tests/phpunit/tests/functions/wpListSort.php @@ -3,23 +3,29 @@ /** * Test wp_list_sort(). * - * @group functions.php + * @group functions + * * @covers ::wp_list_sort */ class Tests_Functions_wpListSort extends WP_UnitTestCase { /** - * @dataProvider data_test_wp_list_sort + * @dataProvider data_wp_list_sort * * @param string|array $orderby Either the field name to order by or an array - * of multiple orderby fields as $orderby => $order. + * of multiple orderby fields as `$orderby => $order`. * @param string $order Either 'ASC' or 'DESC'. */ - public function test_wp_list_sort( $list, $orderby, $order, $expected ) { - $this->assertSame( $expected, wp_list_sort( $list, $orderby, $order ) ); + public function test_wp_list_sort( $input_list, $orderby, $order, $expected ) { + $this->assertSame( $expected, wp_list_sort( $input_list, $orderby, $order ) ); } - public function data_test_wp_list_sort() { + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_list_sort() { return array( 'single orderby ascending' => array( array( @@ -334,17 +340,22 @@ public function data_test_wp_list_sort() { } /** - * @dataProvider data_test_wp_list_sort_preserve_keys + * @dataProvider data_wp_list_sort_preserve_keys * * @param string|array $orderby Either the field name to order by or an array - * of multiple orderby fields as $orderby => $order. + * of multiple orderby fields as `$orderby => $order`. * @param string $order Either 'ASC' or 'DESC'. */ - public function test_wp_list_sort_preserve_keys( $list, $orderby, $order, $expected ) { - $this->assertSame( $expected, wp_list_sort( $list, $orderby, $order, true ) ); + public function test_wp_list_sort_preserve_keys( $input_list, $orderby, $order, $expected ) { + $this->assertSame( $expected, wp_list_sort( $input_list, $orderby, $order, true ) ); } - public function data_test_wp_list_sort_preserve_keys() { + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_list_sort_preserve_keys() { return array( 'single orderby ascending' => array( array( diff --git a/tests/phpunit/tests/functions/wpListUtil.php b/tests/phpunit/tests/functions/wpListUtil.php index 45cad6627aa38..fd068387fd64c 100644 --- a/tests/phpunit/tests/functions/wpListUtil.php +++ b/tests/phpunit/tests/functions/wpListUtil.php @@ -3,7 +3,7 @@ /** * Test WP_List_Util class. * - * @group functions.php + * @group functions */ class Tests_Functions_wpListUtil extends WP_UnitTestCase { @@ -52,4 +52,1113 @@ public function test_wp_list_util_get_output() { $this->assertEqualSets( $expected, $actual ); $this->assertEqualSets( $expected, $util->get_output() ); } + + /** + * @ticket 55300 + * + * @dataProvider data_wp_list_util_pluck + * + * @covers WP_List_Util::pluck + * @covers ::wp_list_pluck + * + * @param array $target_array The array to create the list from. + * @param string $target_key The key to pluck. + * @param array $expected The expected array. + * @param string $index_key Optional. Field from the element to use as keys for the new array. + * Default null. + */ + public function test_wp_list_util_pluck( $target_array, $target_key, $expected, $index_key = null ) { + $util = new WP_List_Util( $target_array ); + $actual = $util->pluck( $target_key, $index_key ); + + $this->assertEqualSetsWithIndex( + $expected, + $actual, + 'The plucked value did not match the expected value.' + ); + + $this->assertEqualSetsWithIndex( + $expected, + $util->get_output(), + '::get_output() did not return the expected value.' + ); + } + + /** + * Data provider for test_wp_list_util_pluck(). + * + * @return array[] + */ + public function data_wp_list_util_pluck() { + return array( + 'simple' => array( + 'target_array' => array( + 0 => array( 'foo' => 'bar' ), + ), + 'target_key' => 'foo', + 'expected' => array( 'bar' ), + ), + 'simple_object' => array( + 'target_array' => array( + 0 => (object) array( 'foo' => 'bar' ), + ), + 'target_key' => 'foo', + 'expected' => array( 'bar' ), + ), + ); + } + + /** + * Tests that wp_list_pluck() throws _doing_it_wrong() with invalid input. + * + * @ticket 56650 + * + * @dataProvider data_wp_list_pluck_should_throw_doing_it_wrong_with_invalid_input + * + * @covers WP_List_Util::pluck + * @covers ::wp_list_pluck + * + * @expectedIncorrectUsage WP_List_Util::pluck + * + * @param array $input An invalid input array. + */ + public function test_wp_list_pluck_should_throw_doing_it_wrong_with_invalid_input( $input ) { + $this->assertSame( array(), wp_list_pluck( $input, 'a_field' ) ); + } + + /** + * Tests that wp_list_pluck() throws _doing_it_wrong() with an index key and invalid input. + * + * @ticket 56650 + * + * @dataProvider data_wp_list_pluck_should_throw_doing_it_wrong_with_invalid_input + * + * @covers WP_List_Util::pluck + * @covers ::wp_list_pluck + * + * @expectedIncorrectUsage WP_List_Util::pluck + * + * @param array $input An invalid input array. + */ + public function test_wp_list_pluck_should_throw_doing_it_wrong_with_index_key_and_invalid_input( $input ) { + $this->assertSame( array(), wp_list_pluck( $input, 'a_field', 'an_index_key' ) ); + } + + /** + * Data provider that provides invalid input arrays. + * + * @return array[] + */ + public function data_wp_list_pluck_should_throw_doing_it_wrong_with_invalid_input() { + return array( + 'int[] 0' => array( array( 0 ) ), + 'int[] 1' => array( array( 1 ) ), + 'int[] -1' => array( array( -1 ) ), + 'float[] 0.0' => array( array( 0.0 ) ), + 'float[] 1.0' => array( array( 1.0 ) ), + 'float[] -1.0' => array( array( -1.0 ) ), + 'string[] and empty string' => array( array( '' ) ), + 'string[] and "0"' => array( array( '0' ) ), + 'string[] and "1"' => array( array( '1' ) ), + 'string[] and "-1"' => array( array( '-1' ) ), + 'array and null' => array( array( null ) ), + 'array and false' => array( array( false ) ), + 'array and true' => array( array( true ) ), + ); + } + + /** + * @ticket 55300 + * + * @covers WP_List_Util::sort + * @covers ::wp_list_sort + */ + public function test_wp_list_util_sort_simple() { + $expected = array( + 1 => 'one', + 2 => 'two', + 3 => 'three', + 4 => 'four', + ); + $target_array = array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ); + + $util = new WP_List_Util( $target_array ); + $actual = $util->sort(); + + $this->assertEqualSets( + $expected, + $actual, + 'The sorted value did not match the expected value.' + ); + + $this->assertEqualSets( + $expected, + $util->get_output(), + '::get_output() did not return the expected value.' + ); + } + + /** + * @ticket 55300 + * + * @dataProvider data_wp_list_util_sort_string_arrays + * @dataProvider data_wp_list_util_sort_int_arrays + * @dataProvider data_wp_list_util_sort_arrays_of_arrays + * @dataProvider data_wp_list_util_sort_object_arrays + * @dataProvider data_wp_list_util_sort_non_existent_orderby_fields + * + * @covers WP_List_Util::sort + * @covers ::wp_list_sort + * + * @param array $expected The expected array. + * @param array $target_array The array to create a list from. + * @param array $orderby Optional. Either the field name to order by or an array + * of multiple orderby fields as `$orderby => $order`. + * Default empty array. + * @param string $order Optional. Either 'ASC' or 'DESC'. Only used if `$orderby` + * is a string. Default 'ASC'. + * @param bool $preserve_keys Optional. Whether to preserve keys. Default false. + */ + public function test_wp_list_util_sort( $expected, $target_array, $orderby = array(), $order = 'ASC', $preserve_keys = false ) { + $util = new WP_List_Util( $target_array ); + $actual = $util->sort( $orderby, $order, $preserve_keys ); + + $this->assertEqualSetsWithIndex( + $expected, + $actual, + 'The sorted value did not match the expected value.' + ); + + $this->assertEqualSetsWithIndex( + $expected, + $util->get_output(), + '::get_output() did not return the expected value.' + ); + } + + /** + * Data provider that provides string arrays to test_wp_list_util_sort(). + * + * @return array[] + */ + public function data_wp_list_util_sort_string_arrays() { + return array( + 'string[], no keys, no ordering' => array( + 'expected' => array( 'four', 'two', 'three', 'one' ), + 'target_array' => array( 'four', 'two', 'three', 'one' ), + ), + 'string[], int keys, no ordering' => array( + 'expected' => array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ), + 'target_array' => array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ), + ), + 'string[], int keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ), + 'target_array' => array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'string[], string keys, no ordering' => array( + 'expected' => array( + 'four' => 'four', + 'two' => 'two', + 'three' => 'three', + 'one' => 'one', + ), + 'target_array' => array( + 'four' => 'four', + 'two' => 'two', + 'three' => 'three', + 'one' => 'one', + ), + ), + 'string[], string keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => 'four', + 'two' => 'two', + 'three' => 'three', + 'one' => 'one', + ), + 'target_array' => array( + 'four' => 'four', + 'two' => 'two', + 'three' => 'three', + 'one' => 'one', + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + ); + } + + /** + * Data provider that provides int arrays for test_wp_list_util_sort(). + * + * @return array[] + */ + public function data_wp_list_util_sort_int_arrays() { + return array( + 'int[], no keys, no ordering' => array( + 'expected' => array( 4, 2, 3, 1 ), + 'target_array' => array( 4, 2, 3, 1 ), + ), + 'int[], int keys, no ordering' => array( + 'expected' => array( + 4 => 4, + 2 => 2, + 3 => 3, + 1 => 1, + ), + 'target_array' => array( + 4 => 4, + 2 => 2, + 3 => 3, + 1 => 1, + ), + ), + 'int[], int keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 4 => 4, + 2 => 2, + 3 => 3, + 1 => 1, + ), + 'target_array' => array( + 4 => 4, + 2 => 2, + 3 => 3, + 1 => 1, + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'int[], string keys, no ordering' => array( + 'expected' => array( + 'four' => 4, + 'two' => 2, + 'three' => 3, + 'one' => 1, + ), + 'target_array' => array( + 'four' => 4, + 'two' => 2, + 'three' => 3, + 'one' => 1, + ), + ), + 'int[], string keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => 4, + 'two' => 2, + 'three' => 3, + 'one' => 1, + ), + 'target_array' => array( + 'four' => 4, + 'two' => 2, + 'three' => 3, + 'one' => 1, + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + ); + } + + /** + * Data provider that provides arrays of arrays for test_wp_list_util_sort(). + * + * @return array[] + */ + public function data_wp_list_util_sort_arrays_of_arrays() { + return array( + 'array[], no keys, no ordering' => array( + 'expected' => array( + array( 'four' ), + array( 'two' ), + array( 'three' ), + array( 'one' ), + ), + 'target_array' => array( + array( 'four' ), + array( 'two' ), + array( 'three' ), + array( 'one' ), + ), + ), + 'array[], int keys, no ordering' => array( + 'expected' => array( + 4 => array( 'four' ), + 2 => array( 'two' ), + 3 => array( 'three' ), + 1 => array( 'one' ), + ), + 'target_array' => array( + 4 => array( 'four' ), + 2 => array( 'two' ), + 3 => array( 'three' ), + 1 => array( 'one' ), + ), + ), + 'array[], int keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 4 => array( 'value' => 'four' ), + 2 => array( 'value' => 'two' ), + 3 => array( 'value' => 'three' ), + 1 => array( 'value' => 'one' ), + ), + 'target_array' => array( + 4 => array( 'value' => 'four' ), + 2 => array( 'value' => 'two' ), + 3 => array( 'value' => 'three' ), + 1 => array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'array[], int keys, $orderby an existing field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + array( + 'id' => 1, + 'value' => 'one', + ), + array( + 'id' => 2, + 'value' => 'two', + ), + array( + 'id' => 3, + 'value' => 'three', + ), + array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 4 => array( + 'id' => 4, + 'value' => 'four', + ), + 2 => array( + 'id' => 2, + 'value' => 'two', + ), + 3 => array( + 'id' => 3, + 'value' => 'three', + ), + 1 => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'array[], int keys, $orderby an existing field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 3 => array( + 'id' => 4, + 'value' => 'four', + ), + 2 => array( + 'id' => 3, + 'value' => 'three', + ), + 1 => array( + 'id' => 2, + 'value' => 'two', + ), + 0 => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'target_array' => array( + array( + 'id' => 1, + 'value' => 'one', + ), + array( + 'id' => 2, + 'value' => 'two', + ), + array( + 'id' => 3, + 'value' => 'three', + ), + array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'array[], string keys, no ordering' => array( + 'expected' => array( + 'four' => array( 'value' => 'four' ), + 'two' => array( 'value' => 'two' ), + 'three' => array( 'value' => 'three' ), + 'one' => array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => array( 'value' => 'four' ), + 'two' => array( 'value' => 'two' ), + 'three' => array( 'value' => 'three' ), + 'one' => array( 'value' => 'one' ), + ), + ), + 'array[], string keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => array( 'value' => 'four' ), + 'two' => array( 'value' => 'two' ), + 'three' => array( 'value' => 'three' ), + 'one' => array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => array( 'value' => 'four' ), + 'two' => array( 'value' => 'two' ), + 'three' => array( 'value' => 'three' ), + 'one' => array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'array[], string keys, $orderby an existing field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + array( + 'id' => 1, + 'value' => 'one', + ), + array( + 'id' => 2, + 'value' => 'two', + ), + array( + 'id' => 3, + 'value' => 'three', + ), + array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'array[], string keys, $orderby an existing field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'target_array' => array( + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'array[], string keys, $orderby an existing field, $order = asc (lowercase) and $preserve_keys = false' => array( + 'expected' => array( + array( + 'id' => 1, + 'value' => 'one', + ), + array( + 'id' => 2, + 'value' => 'two', + ), + array( + 'id' => 3, + 'value' => 'three', + ), + array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => 'id', + 'order' => 'asc', + 'preserve_keys' => false, + ), + 'array[], string keys, $orderby an existing field, no order and $preserve_keys = false' => array( + 'expected' => array( + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'target_array' => array( + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'orderby' => array( 'id' ), + 'order' => null, + 'preserve_keys' => true, + ), + 'array[], string keys, $orderby two existing fields, differing orders and $preserve_keys = false' => array( + 'expected' => array( + array( + 'id' => 1, + 'value' => 'one', + ), + array( + 'id' => 2, + 'value' => 'two', + ), + array( + 'id' => 3, + 'value' => 'three', + ), + array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 'four' => array( + 'id' => 4, + 'value' => 'four', + ), + 'two' => array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => array( + 'id' => 3, + 'value' => 'three', + ), + 'one' => array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => array( + 'id' => 'asc', + 'value' => 'DESC', + ), + 'order' => null, + 'preserve_keys' => false, + ), + ); + } + + /** + * Data provider that provides object arrays for test_wp_list_util_sort(). + * + * @return array[] + */ + public function data_wp_list_util_sort_object_arrays() { + return array( + 'object[], no keys, no ordering' => array( + 'expected' => array( + (object) array( 'four' ), + (object) array( 'two' ), + (object) array( 'three' ), + (object) array( 'one' ), + ), + 'target_array' => array( + (object) array( 'four' ), + (object) array( 'two' ), + (object) array( 'three' ), + (object) array( 'one' ), + ), + ), + 'object[], int keys, no ordering' => array( + 'expected' => array( + 4 => (object) array( 'four' ), + 2 => (object) array( 'two' ), + 3 => (object) array( 'three' ), + 1 => (object) array( 'one' ), + ), + 'target_array' => array( + 4 => (object) array( 'four' ), + 2 => (object) array( 'two' ), + 3 => (object) array( 'three' ), + 1 => (object) array( 'one' ), + ), + ), + 'object[], int keys, $orderby an existing field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + (object) array( + 'id' => 1, + 'value' => 'one', + ), + (object) array( + 'id' => 2, + 'value' => 'two', + ), + (object) array( + 'id' => 3, + 'value' => 'three', + ), + (object) array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 4 => (object) array( + 'id' => 4, + 'value' => 'four', + ), + 2 => (object) array( + 'id' => 2, + 'value' => 'two', + ), + 3 => (object) array( + 'id' => 3, + 'value' => 'three', + ), + 1 => (object) array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'object[], int keys, $orderby an existing field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 3 => (object) array( + 'id' => 4, + 'value' => 'four', + ), + 2 => (object) array( + 'id' => 3, + 'value' => 'three', + ), + 1 => (object) array( + 'id' => 2, + 'value' => 'two', + ), + 0 => (object) array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'target_array' => array( + (object) array( + 'id' => 1, + 'value' => 'one', + ), + (object) array( + 'id' => 2, + 'value' => 'two', + ), + (object) array( + 'id' => 3, + 'value' => 'three', + ), + (object) array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'object[], string keys, no ordering' => array( + 'expected' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + ), + 'object[], string keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'object[], string keys, $orderby an existing field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + (object) array( + 'id' => 1, + 'value' => 'one', + ), + (object) array( + 'id' => 2, + 'value' => 'two', + ), + (object) array( + 'id' => 3, + 'value' => 'three', + ), + (object) array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'target_array' => array( + 'four' => (object) array( + 'id' => 4, + 'value' => 'four', + ), + 'two' => (object) array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => (object) array( + 'id' => 3, + 'value' => 'three', + ), + 'one' => (object) array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'object[], string keys, $orderby an existing field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => (object) array( + 'id' => 4, + 'value' => 'four', + ), + 'three' => (object) array( + 'id' => 3, + 'value' => 'three', + ), + 'two' => (object) array( + 'id' => 2, + 'value' => 'two', + ), + 'one' => (object) array( + 'id' => 1, + 'value' => 'one', + ), + ), + 'target_array' => array( + 'one' => (object) array( + 'id' => 1, + 'value' => 'one', + ), + 'two' => (object) array( + 'id' => 2, + 'value' => 'two', + ), + 'three' => (object) array( + 'id' => 3, + 'value' => 'three', + ), + 'four' => (object) array( + 'id' => 4, + 'value' => 'four', + ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + ); + } + + /** + * Data provider for test_wp_list_util_sort(). + * + * @return array[] + */ + public function data_wp_list_util_sort_non_existent_orderby_fields() { + return array( + 'int[], int keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( 4, 2, 3, 1 ), + 'target_array' => array( + 4 => 4, + 2 => 2, + 3 => 3, + 1 => 1, + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'int[], string keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( 4, 2, 3, 1 ), + 'target_array' => array( + 'four' => 4, + 'two' => 2, + 'three' => 3, + 'one' => 1, + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'string[], int keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( 'four', 'two', 'three', 'one' ), + 'target_array' => array( + 4 => 'four', + 2 => 'two', + 3 => 'three', + 1 => 'one', + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'string[], string keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( 'four', 'two', 'three', 'one' ), + 'target_array' => array( + 'four' => 'four', + 'two' => 'two', + 'three' => 'three', + 'one' => 'one', + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'array[], int keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + array( 'value' => 'four' ), + array( 'value' => 'two' ), + array( 'value' => 'three' ), + array( 'value' => 'one' ), + ), + 'target_array' => array( + 4 => array( 'value' => 'four' ), + 2 => array( 'value' => 'two' ), + 3 => array( 'value' => 'three' ), + 1 => array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'array[], string keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + array( 'value' => 'four' ), + array( 'value' => 'two' ), + array( 'value' => 'three' ), + array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => array( 'value' => 'four' ), + 'two' => array( 'value' => 'two' ), + 'three' => array( 'value' => 'three' ), + 'one' => array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'object[], int keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + (object) array( 'value' => 'four' ), + (object) array( 'value' => 'two' ), + (object) array( 'value' => 'three' ), + (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 4 => (object) array( 'value' => 'four' ), + 2 => (object) array( 'value' => 'two' ), + 3 => (object) array( 'value' => 'three' ), + 1 => (object) array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'object[], int keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 4 => (object) array( 'value' => 'four' ), + 2 => (object) array( 'value' => 'two' ), + 3 => (object) array( 'value' => 'three' ), + 1 => (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 4 => (object) array( 'value' => 'four' ), + 2 => (object) array( 'value' => 'two' ), + 3 => (object) array( 'value' => 'three' ), + 1 => (object) array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + 'object[], string keys, $orderby a non-existent field, $order = ASC and $preserve_keys = false' => array( + 'expected' => array( + (object) array( 'value' => 'four' ), + (object) array( 'value' => 'two' ), + (object) array( 'value' => 'three' ), + (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'ASC', + 'preserve_keys' => false, + ), + 'object[], string keys, $orderby a non-existent field, $order = DESC and $preserve_keys = true' => array( + 'expected' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'target_array' => array( + 'four' => (object) array( 'value' => 'four' ), + 'two' => (object) array( 'value' => 'two' ), + 'three' => (object) array( 'value' => 'three' ), + 'one' => (object) array( 'value' => 'one' ), + ), + 'orderby' => 'id', + 'order' => 'DESC', + 'preserve_keys' => true, + ), + ); + } } diff --git a/tests/phpunit/tests/functions/wpMysqlWeek.php b/tests/phpunit/tests/functions/wpMysqlWeek.php new file mode 100644 index 0000000000000..3649ee6c4c98c --- /dev/null +++ b/tests/phpunit/tests/functions/wpMysqlWeek.php @@ -0,0 +1,44 @@ +<?php + +/** + * Tests for the _wp_mysql_week() function. + * + * @group functions + * + * @covers ::_wp_mysql_week + */ +class Tests_Functions_WpMysqlWeek extends WP_UnitTestCase { + + /** + * @ticket 59931 + * + * @dataProvider data_wp_mysql_week + */ + public function test_wp_mysql_week( $date, $start_of_week, $expected_sql ) { + + add_filter( + 'pre_option_start_of_week', + static function ( $value ) use ( $start_of_week ) { + return $start_of_week ?? $value; + } + ); + + $this->assertSame( $expected_sql, _wp_mysql_week( 'col_name' ) ); + } + + /** + * @return array[] + */ + public function data_wp_mysql_week() { + return array( + array( '1969-12-25', 0, 'WEEK( col_name, 0 )' ), + array( '1969-12-25', 1, 'WEEK( col_name, 1 )' ), + array( '1969-12-25', 2, 'WEEK( DATE_SUB( col_name, INTERVAL 2 DAY ), 0 )' ), + array( '1969-12-25', 3, 'WEEK( DATE_SUB( col_name, INTERVAL 3 DAY ), 0 )' ), + array( '1969-12-25', 4, 'WEEK( DATE_SUB( col_name, INTERVAL 4 DAY ), 0 )' ), + array( '1969-12-25', 5, 'WEEK( DATE_SUB( col_name, INTERVAL 5 DAY ), 0 )' ), + array( '1969-12-25', 6, 'WEEK( DATE_SUB( col_name, INTERVAL 6 DAY ), 0 )' ), + array( '1969-12-25', 9, 'WEEK( col_name, 0 )' ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpNonceAys.php b/tests/phpunit/tests/functions/wpNonceAys.php index 06b807da42685..b7f864d23b1dd 100644 --- a/tests/phpunit/tests/functions/wpNonceAys.php +++ b/tests/phpunit/tests/functions/wpNonceAys.php @@ -5,7 +5,8 @@ * * @since 5.9.0 * - * @group functions.php + * @group functions + * * @covers ::wp_nonce_ays */ class Tests_Functions_wpNonceAys extends WP_UnitTestCase { @@ -26,7 +27,7 @@ public function test_wp_nonce_ays() { */ public function test_wp_nonce_ays_log_out() { $this->expectException( 'WPDieException' ); - $this->expectExceptionMessageMatches( '#You are attempting to log out of Test Blog</p><p>Do you really want to <a href="http://example\.org/wp-login\.php\?action=logout&_wpnonce=.{10}">log out</a>\?#m' ); + $this->expectExceptionMessageMatches( '#You are attempting to log out of Test Blog</p><p>Do you really want to <a href="http://' . WP_TESTS_DOMAIN . '/wp-login\.php\?action=logout&_wpnonce=.{10}">log out</a>\?#m' ); $this->expectExceptionCode( 403 ); wp_nonce_ays( 'log-out' ); diff --git a/tests/phpunit/tests/functions/wpNonceField.php b/tests/phpunit/tests/functions/wpNonceField.php new file mode 100644 index 0000000000000..7ca6e80d0da77 --- /dev/null +++ b/tests/phpunit/tests/functions/wpNonceField.php @@ -0,0 +1,91 @@ +<?php + +/** + * Tests for the wp_nonce_field() function. + * + * @since 6.1.0 + * + * @group functions + * + * @covers ::wp_nonce_field + */ +class Tests_Functions_wpNonceField extends WP_UnitTestCase { + + /** + * @ticket 55578 + */ + public function test_wp_nonce_field() { + wp_nonce_field(); + $this->expectOutputRegex( + '#^<input type="hidden" id="_wpnonce" name="_wpnonce" value=".{10}" />' . + '<input type="hidden" name="_wp_http_referer" value="" />$#' + ); + } + + /** + * @ticket 55578 + * + * @dataProvider data_wp_nonce_field + * + * @param int|string $action Action name. + * @param string $name Nonce name. + * @param bool $referer Whether to set the referer field for validation. + * @param string $expected_regexp The expected regular expression. + */ + public function test_wp_nonce_field_return( $action, $name, $referer, $expected_regexp ) { + if ( -1 !== $action ) { + $nonce_value = wp_create_nonce( $action ); + $expected_regexp = str_replace( '%%NONCE_VALUE%%', $nonce_value, $expected_regexp ); + } + + $this->assertMatchesRegularExpression( $expected_regexp, wp_nonce_field( $action, $name, $referer, false ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_nonce_field() { + return array( + 'default' => array( + 'action' => -1, + 'name' => '_wpnonce', + 'referer' => true, + 'expected_regexp' => + '#^<input type="hidden" id="_wpnonce" name="_wpnonce" value=".{10}" />' . + '<input type="hidden" name="_wp_http_referer" value="" />$#', + ), + 'action_name' => array( + 'action' => 'action_name', + 'name' => '_wpnonce', + 'referer' => true, + 'expected_regexp' => + '#^<input type="hidden" id="_wpnonce" name="_wpnonce" value="%%NONCE_VALUE%%" />' . + '<input type="hidden" name="_wp_http_referer" value="" />$#', + ), + 'nonce_name' => array( + 'action' => -1, + 'name' => 'nonce_name', + 'referer' => true, + 'expected_regexp' => + '#^<input type="hidden" id="nonce_name" name="nonce_name" value=".{10}" />' . + '<input type="hidden" name="_wp_http_referer" value="" />$#', + ), + 'no_referer' => array( + 'action' => -1, + 'name' => '_wpnonce', + 'referer' => false, + 'expected_regexp' => + '#^<input type="hidden" id="_wpnonce" name="_wpnonce" value=".{10}" />$#', + ), + '& in name' => array( + 'action' => -1, + 'name' => 'a&b', + 'referer' => false, + 'expected_regexp' => + '#^<input type="hidden" id="a\&b" name="a\&b" value=".{10}" />$#', + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpNonceUrl.php b/tests/phpunit/tests/functions/wpNonceUrl.php new file mode 100644 index 0000000000000..a4d409dd86d4c --- /dev/null +++ b/tests/phpunit/tests/functions/wpNonceUrl.php @@ -0,0 +1,145 @@ +<?php + +/** + * @group functions + * + * @covers ::wp_nonce_url + */ +class Tests_Functions_WpNonceUrl extends WP_UnitTestCase { + /** + * Tests that wp_nonce_url() appends the nonce name and value to the URL. + * + * @ticket 54870 + * + * @dataProvider data_should_append_nonce_name_and_value + * + * @param string $actionurl URL to add nonce action. + * @param int|string $action Optional. Nonce action name. Default -1. + * @param string $name Optional. Nonce name. Default '_wpnonce'. + */ + public function test_should_append_nonce_name_and_value( $actionurl, $action = -1, $name = '_wpnonce' ) { + $actual = wp_nonce_url( $actionurl, $action, $name ); + $url_with_name = "$actionurl?$name="; + $nonce = str_replace( $url_with_name, '', $actual ); + + $this->assertStringContainsString( + $url_with_name, + $actual, + 'The URL did not contain the action URL and the nonce name' + ); + + $this->assertNotFalse( + wp_verify_nonce( $nonce, $action ), + 'The nonce is invalid' + ); + } + + /** + * Data provider for test_should_append_nonce_name_and_value(). + * + * @return array[] + */ + public function data_should_append_nonce_name_and_value() { + return array( + 'http:// and default action/name' => array( + 'actionurl' => 'http://example.org/', + ), + 'http:// and a custom nonce action' => array( + 'actionurl' => 'http://example.org/', + 'action' => 'my_action', + ), + 'http:// and a custom nonce name' => array( + 'actionurl' => 'http://example.org/', + 'action' => -1, + 'name' => 'my_nonce', + ), + 'http:// and a custom nonce action and name' => array( + 'actionurl' => 'http://example.org/', + 'action' => 'my_action', + 'name' => 'my_nonce', + ), + 'https:// and default action/name' => array( + 'actionurl' => 'https://example.org/', + ), + 'https:// and a custom nonce action' => array( + 'actionurl' => 'https://example.org/', + 'action' => 'my_action', + ), + 'https:// and a custom nonce name' => array( + 'actionurl' => 'https://example.org/', + 'action' => -1, + 'name' => 'my_nonce', + ), + 'https:// and a custom nonce action and name' => array( + 'actionurl' => 'https://example.org/', + 'action' => 'my_action', + 'name' => 'my_nonce', + ), + '/ and default nonce action/name' => array( + 'actionurl' => '/', + ), + '/ and a custom nonce action' => array( + 'actionurl' => '/', + 'action' => 'my_action', + ), + '/ and a custom nonce name' => array( + 'actionurl' => '/', + 'action' => -1, + 'name' => 'my_nonce', + ), + '/ and a custom nonce action and name' => array( + 'actionurl' => '/', + 'action' => 'my_action', + 'name' => 'my_nonce', + ), + ); + } + + /** + * Tests that wp_nonce_url() handles existing query args. + * + * @ticket 54870 + * + * @dataProvider data_should_handle_existing_query_args + * + * @param string $actionurl URL to add nonce action. + * @param string $expected The expected result. + */ + public function test_should_handle_existing_query_args( $actionurl, $expected ) { + $actual = wp_nonce_url( $actionurl ); + + $this->assertStringStartsWith( + $expected, + $actual, + 'The nonced URL did not start with the expected value.' + ); + + $this->assertSame( + strlen( $expected ) + 10, + strlen( $actual ), + 'The nonced URL was not the expected length.' + ); + } + + /** + * Data provider for test_should_handle_existing_query_args(). + * + * @return array[] + */ + public function data_should_handle_existing_query_args() { + return array( + 'one query arg' => array( + 'actionurl' => 'http://example.org/?hello=world', + 'expected' => 'http://example.org/?hello=world&_wpnonce=', + ), + 'two query args' => array( + 'actionurl' => 'http://example.org/?hello=world&howdy=admin', + 'expected' => 'http://example.org/?hello=world&howdy=admin&_wpnonce=', + ), + 'two query args and &' => array( + 'actionurl' => 'http://example.org/?hello=world&howdy=admin', + 'expected' => 'http://example.org/?hello=world&howdy=admin&_wpnonce=', + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpParseIdList.php b/tests/phpunit/tests/functions/wpParseIdList.php new file mode 100644 index 0000000000000..cd1af1e923166 --- /dev/null +++ b/tests/phpunit/tests/functions/wpParseIdList.php @@ -0,0 +1,102 @@ +<?php + +/** + * Tests for the wp_parse_id_list() function. + * + * @group functions + * + * @covers ::wp_parse_id_list + */ +class Tests_Functions_wpParseIdList extends WP_UnitTestCase { + + /** + * @ticket 22074 + * @ticket 60218 + * + * @dataProvider data_wp_parse_id_list + * @dataProvider data_unexpected_input + */ + public function test_wp_parse_id_list( $input_list, $expected ) { + $this->assertSameSets( $expected, wp_parse_id_list( $input_list ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_parse_id_list() { + return array( + 'regular' => array( + 'input_list' => '1,2,3,4', + 'expected' => array( 1, 2, 3, 4 ), + ), + 'double comma' => array( + 'input_list' => '1, 2,,3,4', + 'expected' => array( 1, 2, 3, 4 ), + ), + 'duplicate id in a string' => array( + 'input_list' => '1,2,2,3,4', + 'expected' => array( 1, 2, 3, 4 ), + ), + 'duplicate id in an array' => array( + 'input_list' => array( '1', '2', '3', '4', '3' ), + 'expected' => array( 1, 2, 3, 4 ), + ), + 'mixed type' => array( + 'input_list' => array( 1, '2', 3, '4' ), + 'expected' => array( 1, 2, 3, 4 ), + ), + 'negative ids in a string' => array( + 'input_list' => '-1,2,-3,4', + 'expected' => array( 1, 2, 3, 4 ), + ), + 'negative ids in an array' => array( + 'input_list' => array( -1, 2, '-3', '4' ), + 'expected' => array( 1, 2, 3, 4 ), + ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_unexpected_input() { + return array( + 'string with commas' => array( + 'input_list' => '1,2,string with spaces', + 'expected' => array( 1, 2, 0 ), + ), + 'array' => array( + 'input_list' => array( '1', 2, 'string with spaces' ), + 'expected' => array( 1, 2, 0 ), + ), + 'string with spaces' => array( + 'input_list' => '1 2 string with spaces', + 'expected' => array( 1, 2, 0 ), + ), + 'array with spaces' => array( + 'input_list' => array( '1 2 string with spaces' ), + 'expected' => array( 1 ), + ), + 'string with html' => array( + 'input_list' => '1 2 string <strong>with</strong> <h1>HEADING</h1>', + 'expected' => array( 1, 2, 0 ), + ), + 'array with html' => array( + 'input_list' => array( '1', 2, 'string <strong>with</strong> <h1>HEADING</h1>' ), + 'expected' => array( 1, 2, 0 ), + ), + 'array with null' => array( + 'input_list' => array( 1, 2, null ), + 'expected' => array( 1, 2 ), + ), + 'array with false' => array( + 'input_list' => array( 1, 2, false ), + 'expected' => array( 1, 2, 0 ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpParseList.php b/tests/phpunit/tests/functions/wpParseList.php new file mode 100644 index 0000000000000..2f7bf086e4193 --- /dev/null +++ b/tests/phpunit/tests/functions/wpParseList.php @@ -0,0 +1,74 @@ +<?php + +/** + * Tests for the wp_parse_list() function. + * + * @group functions + * + * @covers ::wp_parse_list + */ +class Tests_Functions_wpParseList extends WP_UnitTestCase { + + /** + * @ticket 43977 + * + * @dataProvider data_wp_parse_list + */ + public function test_wp_parse_list( $input_list, $expected ) { + $this->assertSameSets( $expected, wp_parse_list( $input_list ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_parse_list() { + return array( + 'ids only' => array( + 'input_list' => '1,2,3,4', + 'expected' => array( '1', '2', '3', '4' ), + ), + 'slugs only' => array( + 'input_list' => 'apple,banana,carrot,dog', + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'ids and slugs' => array( + 'input_list' => '1,2,apple,banana', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'space after comma' => array( + 'input_list' => '1, 2,apple,banana', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'double comma' => array( + 'input_list' => '1,2,apple,,banana', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'leading comma' => array( + 'input_list' => ',1,2,apple,banana', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'trailing comma' => array( + 'input_list' => '1,2,apple,banana,', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'space before comma' => array( + 'input_list' => '1,2 ,apple,banana', + 'expected' => array( '1', '2', 'apple', 'banana' ), + ), + 'empty string' => array( + 'input_list' => '', + 'expected' => array(), + ), + 'comma only' => array( + 'input_list' => ',', + 'expected' => array(), + ), + 'double comma only' => array( + 'input_list' => ',,', + 'expected' => array(), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpParseSlugList.php b/tests/phpunit/tests/functions/wpParseSlugList.php new file mode 100644 index 0000000000000..8b9ccb2ddb091 --- /dev/null +++ b/tests/phpunit/tests/functions/wpParseSlugList.php @@ -0,0 +1,98 @@ +<?php + +/** + * Tests for the wp_parse_slug_list() function. + * + * @group functions + * + * @covers ::wp_parse_slug_list + */ +class Tests_Functions_WpParseSlugList extends WP_UnitTestCase { + + /** + * @ticket 35582 + * @ticket 60217 + * + * @dataProvider data_wp_parse_slug_list + * @dataProvider data_unexpected_input + */ + public function test_wp_parse_slug_list( $input_list, $expected ) { + $this->assertSameSets( $expected, wp_parse_slug_list( $input_list ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_parse_slug_list() { + return array( + 'regular' => array( + 'input_list' => 'apple,banana,carrot,dog', + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'double comma' => array( + 'input_list' => 'apple, banana,,carrot,dog', + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'duplicate slug in a string' => array( + 'input_list' => 'apple,banana,carrot,carrot,dog', + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'duplicate slug in an array' => array( + 'input_list' => array( 'apple', 'banana', 'carrot', 'carrot', 'dog' ), + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'string with spaces' => array( + 'input_list' => 'apple banana carrot dog', + 'expected' => array( 'apple', 'banana', 'carrot', 'dog' ), + ), + 'array with spaces' => array( + 'input_list' => array( 'apple ', 'banana carrot', 'd o g' ), + 'expected' => array( 'apple', 'banana-carrot', 'd-o-g' ), + ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_unexpected_input() { + return array( + 'string with commas' => array( + 'input_list' => '1,2,string with spaces', + 'expected' => array( '1', '2', 'string', 'with', 'spaces' ), + ), + 'array' => array( + 'input_list' => array( '1', 2, 'string with spaces' ), + 'expected' => array( '1', '2', 'string-with-spaces' ), + ), + 'string with spaces' => array( + 'input_list' => '1 2 string with spaces', + 'expected' => array( '1', '2', 'string', 'with', 'spaces' ), + ), + 'array with spaces' => array( + 'input_list' => array( '1 2 string with spaces' ), + 'expected' => array( '1-2-string-with-spaces' ), + ), + 'string with html' => array( + 'input_list' => '1 2 string <strong>with</strong> <h1>HEADING</h1>', + 'expected' => array( '1', '2', 'string', 'with', 'heading' ), + ), + 'array with html' => array( + 'input_list' => array( '1', 2, 'string <strong>with</strong> <h1>HEADING</h1>' ), + 'expected' => array( '1', '2', 'string-with-heading' ), + ), + 'array with null' => array( + 'input_list' => array( 1, 2, null ), + 'expected' => array( '1', '2' ), + ), + 'array with false' => array( + 'input_list' => array( 1, 2, false ), + 'expected' => array( '1', '2', '' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpRefererField.php b/tests/phpunit/tests/functions/wpRefererField.php new file mode 100644 index 0000000000000..72e2fe99d5fde --- /dev/null +++ b/tests/phpunit/tests/functions/wpRefererField.php @@ -0,0 +1,78 @@ +<?php + +/** + * Tests for the wp_referer_field() function. + * + * @since 6.1.0 + * + * @group functions + * + * @covers ::wp_referer_field + */ +class Tests_Functions_wpRefererField extends WP_UnitTestCase { + + /** + * @ticket 55578 + */ + public function test_wp_referer_field() { + $_SERVER['REQUEST_URI'] = '/test/'; + + wp_referer_field(); + $this->expectOutputString( '<input type="hidden" name="_wp_http_referer" value="/test/" />' ); + } + + /** + * @ticket 55578 + */ + public function test_wp_referer_field_return() { + $_SERVER['REQUEST_URI'] = '/test/'; + + $this->assertSame( '<input type="hidden" name="_wp_http_referer" value="/test/" />', wp_referer_field( false ) ); + } + + /** + * Tests that the display argument is respected. + * + * @ticket 54106 + * + * @dataProvider data_wp_referer_field_should_respect_display_arg + * + * @param mixed $display Whether to echo or return the referer field. + */ + public function test_wp_referer_field_should_respect_display_arg( $display ) { + $actual = $display ? get_echo( 'wp_referer_field' ) : wp_referer_field( false ); + + $this->assertSame( '<input type="hidden" name="_wp_http_referer" value="" />', $actual ); + } + + /** + * Data provider for test_wp_referer_field_should_respect_display_arg(). + * + * @return array[] + */ + public function data_wp_referer_field_should_respect_display_arg() { + return array( + 'true' => array( true ), + '(int) 1' => array( 1 ), + '(string) "1"' => array( '1' ), + 'false' => array( false ), + 'null' => array( null ), + '(int) 0' => array( 0 ), + '(string) "0"' => array( '0' ), + ); + } + + /** + * @ticket 54106 + */ + public function test_wp_referer_field_with_referer() { + $old_request_uri = $_SERVER['REQUEST_URI']; + $_SERVER['REQUEST_URI'] = 'edit.php?_wp_http_referer=edit.php'; + + $actual = wp_referer_field( false ); + + $_SERVER['REQUEST_URI'] = $old_request_uri; + + $this->assertSame( '<input type="hidden" name="_wp_http_referer" value="edit.php" />', $actual ); + } +} diff --git a/tests/phpunit/tests/functions/wpRemoteFopen.php b/tests/phpunit/tests/functions/wpRemoteFopen.php index e29c9d31813ff..51394fd031497 100644 --- a/tests/phpunit/tests/functions/wpRemoteFopen.php +++ b/tests/phpunit/tests/functions/wpRemoteFopen.php @@ -2,7 +2,8 @@ /** * @group http * @group external-http - * @group functions.php + * @group functions + * * @covers ::wp_remote_fopen */ class Tests_Functions_wpRemoteFopen extends WP_UnitTestCase { @@ -26,10 +27,10 @@ public function test_wp_remote_fopen_bad_url() { */ public function test_wp_remote_fopen() { // This URL gives a direct 200 response. - $url = 'https://asdftestblog1.files.wordpress.com/2007/09/2007-06-30-dsc_4700-1.jpg'; + $url = 'https://s.w.org/screenshots/3.9/dashboard.png'; $response = wp_remote_fopen( $url ); $this->assertIsString( $response ); - $this->assertSame( 40148, strlen( $response ) ); + $this->assertSame( 153204, strlen( $response ) ); } } diff --git a/tests/phpunit/tests/functions/wpScheduledDelete.php b/tests/phpunit/tests/functions/wpScheduledDelete.php new file mode 100644 index 0000000000000..c7963578db43c --- /dev/null +++ b/tests/phpunit/tests/functions/wpScheduledDelete.php @@ -0,0 +1,171 @@ +<?php + +/** + * Tests for the wp_scheduled_delete() function. + * + * @group functions + * + * @covers ::wp_scheduled_delete + */ +class Tests_Functions_wpScheduledDelete extends WP_UnitTestCase { + + protected static $comment_id; + protected static $page_id; + + public function tear_down() { + // Remove comment. + if ( self::$comment_id ) { + wp_delete_comment( self::$comment_id ); + } + + // Remove page. + if ( self::$page_id ) { + wp_delete_post( self::$page_id ); + } + + parent::tear_down(); + } + + /** + * Tests that old trashed posts/pages are deleted. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete() { + self::$page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'trash', + ) + ); + add_post_meta( self::$page_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS + 1 ) ); + add_post_meta( self::$page_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Post', get_post( self::$page_id ) ); + + wp_scheduled_delete(); + + $this->assertNull( get_post( self::$page_id ) ); + } + + /** + * Tests that old trashed posts/pages are not deleted if status is not 'trash'. + * + * Ensures that the trash meta status is removed. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete_status_not_trash() { + self::$page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'published', + ) + ); + add_post_meta( self::$page_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS + 1 ) ); + add_post_meta( self::$page_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Post', get_post( self::$page_id ) ); + + wp_scheduled_delete(); + + $this->assertInstanceOf( 'WP_Post', get_post( self::$page_id ) ); + $this->assertSame( '', get_post_meta( self::$page_id, '_wp_trash_meta_time', true ) ); + $this->assertSame( '', get_post_meta( self::$page_id, '_wp_trash_meta_status', true ) ); + } + + + /** + * Tests that old trashed posts/pages are not deleted if not old enough. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete_page_not_old_enough() { + self::$page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'trash', + ) + ); + add_post_meta( self::$page_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS - 1 ) ); + add_post_meta( self::$page_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Post', get_post( self::$page_id ) ); + + wp_scheduled_delete(); + + $this->assertInstanceOf( 'WP_Post', get_post( self::$page_id ) ); + $this->assertIsNumeric( get_post_meta( self::$page_id, '_wp_trash_meta_time', true ) ); + $this->assertSame( 'published', get_post_meta( self::$page_id, '_wp_trash_meta_status', true ) ); + } + + /** + * Tests that old trashed comments are deleted. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete_comment() { + self::$comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 'trash', + ) + ); + add_comment_meta( self::$comment_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS + 1 ) ); + add_post_meta( self::$comment_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Comment', get_comment( self::$comment_id ) ); + + wp_scheduled_delete(); + + $this->assertNull( get_comment( self::$comment_id ) ); + } + + /** + * Tests that old trashed comments are not deleted if status is not 'trash'. + * + * Ensures that the trash meta status is removed. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete_comment_status_not_trash() { + self::$comment_id = self::factory()->comment->create( + array( + 'comment_approved' => '1', + ) + ); + add_comment_meta( self::$comment_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS + 1 ) ); + add_comment_meta( self::$comment_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Comment', get_comment( self::$comment_id ) ); + + wp_scheduled_delete(); + + $this->assertInstanceOf( 'WP_Comment', get_comment( self::$comment_id ) ); + $this->assertSame( '', get_comment_meta( self::$comment_id, '_wp_trash_meta_time', true ) ); + $this->assertSame( '', get_comment_meta( self::$comment_id, '_wp_trash_meta_status', true ) ); + } + + + /** + * Tests that old trashed comments are not deleted if not old enough. + * + * @ticket 59938 + */ + public function test_wp_scheduled_delete_comment_not_old_enough() { + self::$comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 'trash', + ) + ); + add_comment_meta( self::$comment_id, '_wp_trash_meta_time', time() - ( DAY_IN_SECONDS * EMPTY_TRASH_DAYS - 1 ) ); + add_comment_meta( self::$comment_id, '_wp_trash_meta_status', 'published' ); + + $this->assertInstanceOf( 'WP_Comment', get_comment( self::$comment_id ) ); + + wp_scheduled_delete(); + + $this->assertInstanceOf( 'WP_Comment', get_comment( self::$comment_id ) ); + $this->assertIsNumeric( get_comment_meta( self::$comment_id, '_wp_trash_meta_time', true ) ); + $this->assertSame( 'published', get_comment_meta( self::$comment_id, '_wp_trash_meta_status', true ) ); + } +} diff --git a/tests/phpunit/tests/functions/wpTimezoneChoice.php b/tests/phpunit/tests/functions/wpTimezoneChoice.php new file mode 100644 index 0000000000000..bb2ff0057222b --- /dev/null +++ b/tests/phpunit/tests/functions/wpTimezoneChoice.php @@ -0,0 +1,146 @@ +<?php + +/** + * Tests for the wp_timezone_choice() function. + * + * @group functions + * + * @covers ::wp_timezone_choice + */ +class Tests_Functions_WpTimezoneChoice extends WP_UnitTestCase { + + /** + * Restores the current locale after each test runs. + */ + public function tear_down(): void { + restore_current_locale(); + parent::tear_down(); + } + + /** + * Tests default values. + * + * @ticket 59941 + * @dataProvider data_wp_timezone_choice + * + * @param string $expected Expected string HTML fragment. + */ + public function test_wp_timezone_choice( string $expected ): void { + $timezone_list = wp_timezone_choice( '' ); + $this->assertStringContainsString( $expected, $timezone_list ); + } + + /** + * Data provider for test_wp_timezone_choice(). + * + * @return array<string, array{ 0: string }> + */ + public function data_wp_timezone_choice(): array { + return array( + 'placeholder option' => array( '<option selected="selected" value="">Select a city</option>' ), + 'city in Americas' => array( '<option value="America/Los_Angeles" dir="auto">Los Angeles</option>' ), + 'deprecated timezone' => array( '<option value="Pacific/Honolulu" dir="auto">Honolulu</option>' ), + 'manual offset example' => array( '<option value="UTC-8" dir="auto">UTC-8</option>' ), + 'UTC option' => array( '<option value="UTC" dir="auto">UTC</option>' ), + 'continent example' => array( '<option value="Africa/Johannesburg" dir="auto">Johannesburg</option>' ), + 'city example' => array( '<option value="Asia/Kuala_Lumpur" dir="auto">Kuala Lumpur</option>' ), + 'city with sub-city' => array( '<option value="America/Argentina/Buenos_Aires" dir="auto">Argentina - Buenos Aires</option>' ), + 'translated city name appears' => array( '<option value="Pacific/Port_Moresby" dir="auto">Port Moresby</option>' ), + ); + } + + /** + * Tests zones are selected from the list. + * + * @ticket 59941 + * @dataProvider data_wp_timezone_choice_selected + * + * @param string $selected_zone The timezone to select. + * @param string $expected Expected string HTML fragment. + */ + public function test_wp_timezone_choice_selected( string $selected_zone, string $expected ): void { + $actual = wp_timezone_choice( $selected_zone ); + $this->assertStringContainsString( $expected, $actual ); + } + + /** + * Data provider for test_wp_timezone_choice_selected(). + * + * @return array<string, array{ 0: string }> + */ + public function data_wp_timezone_choice_selected(): array { + return array( + 'city from the list' => array( + 'America/Los_Angeles', + '<option selected="selected" value="America/Los_Angeles" dir="auto">Los Angeles</option>', + ), + 'deprecated but valid timezone string' => array( + 'Pacific/Auckland', + '<option selected="selected" value="Pacific/Auckland" dir="auto">Auckland</option>', + ), + 'UTC' => array( + 'UTC', + '<option selected="selected" value="UTC" dir="auto">UTC</option>', + ), + 'manual UTC offset' => array( + 'UTC+10', + '<option selected="selected" value="UTC+10" dir="auto">UTC+10</option>', + ), + ); + } + + /** + * Tests passing in the locale. + * + * @ticket 59941 + * @dataProvider data_wp_timezone_choice_es + * + * @param string $expected Expected string HTML fragment. + */ + public function test_wp_timezone_choice_es( string $expected ): void { + $timezone_list = wp_timezone_choice( '', 'es_ES' ); + $this->assertStringContainsString( $expected, $timezone_list ); + } + + /** + * Data provider for test_wp_timezone_choice_es(). + * + * @return array<string, array{ 0: string }> + */ + public function data_wp_timezone_choice_es(): array { + return array( + 'placeholder remains in English (no translation override passed)' => array( '<option selected="selected" value="">Select a city</option>' ), + 'spanish city translation' => array( '<option value="Pacific/Port_Moresby" dir="auto">Puerto Moresby</option>' ), + 'spanish optgroup Arctic' => array( '<optgroup label="Ártico" dir="auto">' ), + 'spanish optgroup Manual Offsets untranslated' => array( '<optgroup label="Manual Offsets" dir="auto">' ), + ); + } + + /** + * Tests setting the locale globally. + * + * @ticket 59941 + * @dataProvider data_wp_timezone_choice_es_set + * + * @param string $expected Expected string HTML fragment. + */ + public function test_wp_timezone_choice_es_set( string $expected ): void { + switch_to_locale( 'es_ES' ); + $timezone_list = wp_timezone_choice( '' ); + $this->assertStringContainsString( $expected, $timezone_list ); + } + + /** + * Data provider for test_wp_timezone_choice_es_set(). + * + * @return array<string, array{ 0: string }> + */ + public static function data_wp_timezone_choice_es_set(): array { + return array( + 'placeholder in Spanish' => array( '<option selected="selected" value="">Elige una ciudad</option>' ), + 'spanish city translation' => array( '<option value="Pacific/Port_Moresby" dir="auto">Puerto Moresby</option>' ), + 'spanish optgroup Arctic' => array( '<optgroup label="Ártico" dir="auto">' ), + 'spanish optgroup Manual Offsets' => array( '<optgroup label="Compensaciones manuales" dir="auto">' ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpTimezoneChoiceUsortCallback.php b/tests/phpunit/tests/functions/wpTimezoneChoiceUsortCallback.php new file mode 100644 index 0000000000000..5c89cbaf4eb8f --- /dev/null +++ b/tests/phpunit/tests/functions/wpTimezoneChoiceUsortCallback.php @@ -0,0 +1,642 @@ +<?php + +/** + * Tests for the _wp_timezone_choice_usort_callback() function. + * + * @group functions + * + * @covers ::_wp_timezone_choice_usort_callback + */ +class Tests_Functions_WpTimezoneChoiceUsortCallback extends WP_UnitTestCase { + + /** + * @ticket 59953 + * + * @dataProvider data_wp_timezone_choice_usort_callback + */ + public function test_wp_timezone_choice_usort_callback( $unsorted, $sorted ) { + usort( $unsorted, '_wp_timezone_choice_usort_callback' ); + + $this->assertSame( $sorted, $unsorted ); + } + + public function data_wp_timezone_choice_usort_callback() { + return array( + 'just GMT+' => array( + 'unsorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'GMT+a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+b', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+e', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+d', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'GMT+e', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+d', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+b', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + + 'mixed UTC and GMT' => array( + 'unsorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'GMT+a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'UTC', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'UTC', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+d', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'GMT+d', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'GMT+a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'UTC', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'UTC', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + + 'just alpha city' => array( + 'unsorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'e', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'b', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'd', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => 'Etc', + 'city' => 'a', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'b', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'c', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'd', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => 'e', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + + 'not Etc continents are not sorted' => array( + 'unsorted' => array( + array( + 'continent' => 'd', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'c', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'a', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'd', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'e', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => 'd', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'c', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'a', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'd', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'e', + 'city' => '', + 't_continent' => '', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + + 'not Etc just t_continent' => array( + 'unsorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'd', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'b', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'e', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'c', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'b', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'c', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'd', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'e', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + + 'not Etc just t_city' => array( + 'unsorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'd', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'e', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'c', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'b', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'b', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'c', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'd', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'e', + 't_subcity' => '', + ), + ), + ), + + 'not Etc just t_subcity' => array( + 'unsorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'b', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'e', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'a', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'c', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'd', + ), + ), + 'sorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'a', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'b', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'c', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'd', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => 'a', + 't_subcity' => 'e', + ), + ), + ), + + 'just continent with Etc which pulls 1 to bottom' => array( + 'unsorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'b', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'c', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => '', + 't_continent' => '1', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'd', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => '', + 't_subcity' => '', + ), + ), + 'sorted' => array( + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'a', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'b', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'c', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => '', + 'city' => '', + 't_continent' => 'd', + 't_city' => '', + 't_subcity' => '', + ), + array( + 'continent' => 'Etc', + 'city' => '', + 't_continent' => '1', + 't_city' => '', + 't_subcity' => '', + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpTimezoneOverrideOffset.php b/tests/phpunit/tests/functions/wpTimezoneOverrideOffset.php new file mode 100644 index 0000000000000..10c90b7037c3b --- /dev/null +++ b/tests/phpunit/tests/functions/wpTimezoneOverrideOffset.php @@ -0,0 +1,57 @@ +<?php + +/** + * Tests for the wp_timezone_override_offset() function. + * + * @group functions + * + * @covers ::wp_timezone_override_offset + */ +class Tests_Functions_wpTimezoneOverrideOffset extends WP_UnitTestCase { + + /** + * @ticket 59980 + * + * @dataProvider data_wp_timezone_override_offset + */ + public function test_wp_timezone_override_offset( $timezone_string, $expected ) { + update_option( 'timezone_string', $timezone_string ); + $this->assertSame( $expected, wp_timezone_override_offset() ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type string $timezone_string Test value. + * @type string $expected Expected return value. + * } + */ + public function data_wp_timezone_override_offset() { + return array( + 'no timezone string option set' => array( '', false ), + 'bad option set' => array( 'BAD_TIME_ZONE', false ), + 'UTC option set' => array( 'UTC', 0.0 ), + 'EST option set' => array( 'EST', -5.0 ), + 'NST option set' => array( 'America/St_Johns', $this->is_timezone_in_dst( 'America/St_Johns' ) ? -2.5 : -3.5 ), + ); + } + + /** + * Determines whether the current timezone offset is observing daylight saving time (DST). + * + * @param string $timezone_string The timezone identifier (e.g., 'America/St_Johns'). + * @return bool Whether the timezone is observing DST. + */ + private function is_timezone_in_dst( $timezone_string ) { + $timezone = new DateTimeZone( $timezone_string ); + $timestamp = time(); + $transitions = $timezone->getTransitions( $timestamp, $timestamp ); + + if ( false === $transitions || ! is_array( $transitions ) || ! isset( $transitions[0]['isdst'] ) ) { + return false; + } + + return $transitions[0]['isdst']; + } +} diff --git a/tests/phpunit/tests/functions/wpToKebabCase.php b/tests/phpunit/tests/functions/wpToKebabCase.php index 24b2e2d187951..c7fb5a4a7ed54 100644 --- a/tests/phpunit/tests/functions/wpToKebabCase.php +++ b/tests/phpunit/tests/functions/wpToKebabCase.php @@ -5,36 +5,61 @@ * * @since 5.8.0 * - * @group functions.php + * @group functions + * * @covers ::_wp_to_kebab_case */ class Tests_Functions_wpToKebabCase extends WP_UnitTestCase { - public function test_wp_to_kebab_case() { - $this->assertSame( 'white', _wp_to_kebab_case( 'white' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white+black' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white:black' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white*black' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white.black' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white black' ) ); - $this->assertSame( 'white-black', _wp_to_kebab_case( 'white black' ) ); - $this->assertSame( 'white-to-black', _wp_to_kebab_case( 'white-to-black' ) ); - $this->assertSame( 'white-2-white', _wp_to_kebab_case( 'white2white' ) ); - $this->assertSame( 'white-2nd', _wp_to_kebab_case( 'white2nd' ) ); - $this->assertSame( 'white-2-ndcolor', _wp_to_kebab_case( 'white2ndcolor' ) ); - $this->assertSame( 'white-2nd-color', _wp_to_kebab_case( 'white2ndColor' ) ); - $this->assertSame( 'white-2nd-color', _wp_to_kebab_case( 'white2nd_color' ) ); - $this->assertSame( 'white-23-color', _wp_to_kebab_case( 'white23color' ) ); - $this->assertSame( 'white-23', _wp_to_kebab_case( 'white23' ) ); - $this->assertSame( '23-color', _wp_to_kebab_case( '23color' ) ); - $this->assertSame( 'white-4th', _wp_to_kebab_case( 'white4th' ) ); - $this->assertSame( 'font-2-xl', _wp_to_kebab_case( 'font2xl' ) ); - $this->assertSame( 'white-to-white', _wp_to_kebab_case( 'whiteToWhite' ) ); - $this->assertSame( 'white-t-owhite', _wp_to_kebab_case( 'whiteTOwhite' ) ); - $this->assertSame( 'whit-eto-white', _wp_to_kebab_case( 'WHITEtoWHITE' ) ); - $this->assertSame( '42', _wp_to_kebab_case( 42 ) ); - $this->assertSame( 'ive-done', _wp_to_kebab_case( "i've done" ) ); - $this->assertSame( 'ffffff', _wp_to_kebab_case( '#ffffff' ) ); - $this->assertSame( 'ffffff', _wp_to_kebab_case( '$ffffff' ) ); + /** + * Tests _wp_to_kebab_case(). + * + * @dataProvider data_wp_to_kebab_case + * + * @ticket 53397 + * + * @param string $test_value Test value. + * @param string $expected Expected return value. + */ + public function test_wp_to_kebab_case( $test_value, $expected ) { + $this->assertSame( $expected, _wp_to_kebab_case( $test_value ) ); + } + + /** + * Data provider for test_wp_to_kebab_case(). + * + * @return array[] Test parameters { + * @type string $test_value Test value. + * @type string $expected Expected return value. + * } + */ + public function data_wp_to_kebab_case() { + return array( + array( 'white', 'white' ), + array( 'white+black', 'white-black' ), + array( 'white:black', 'white-black' ), + array( 'white*black', 'white-black' ), + array( 'white.black', 'white-black' ), + array( 'white black', 'white-black' ), + array( 'white black', 'white-black' ), + array( 'white-to-black', 'white-to-black' ), + array( 'white2white', 'white-2-white' ), + array( 'white2nd', 'white-2nd' ), + array( 'white2ndcolor', 'white-2-ndcolor' ), + array( 'white2ndColor', 'white-2nd-color' ), + array( 'white2nd_color', 'white-2nd-color' ), + array( 'white23color', 'white-23-color' ), + array( 'white23', 'white-23' ), + array( '23color', '23-color' ), + array( 'white4th', 'white-4th' ), + array( 'font2xl', 'font-2-xl' ), + array( 'whiteToWhite', 'white-to-white' ), + array( 'whiteTOwhite', 'white-t-owhite' ), + array( 'WHITEtoWHITE', 'whit-eto-white' ), + array( 42, '42' ), + array( "i've done", 'ive-done' ), + array( '#ffffff', 'ffffff' ), + array( '$ffffff', 'ffffff' ), + ); } } diff --git a/tests/phpunit/tests/functions/wpTriggerError.php b/tests/phpunit/tests/functions/wpTriggerError.php new file mode 100644 index 0000000000000..b642b7b08f6ae --- /dev/null +++ b/tests/phpunit/tests/functions/wpTriggerError.php @@ -0,0 +1,117 @@ +<?php + +/** + * Test cases for the `wp_trigger_error()` function. + * + * @since 6.4.0 + * + * @group functions + * + * @covers ::wp_trigger_error + */ +class Tests_Functions_WpTriggerError extends WP_UnitTestCase { + + /** + * @ticket 57686 + * + * @dataProvider data_should_trigger_error + * + * @param string $function_name The function name to test. + * @param string $message The message to test. + * @param string $expected_message The expected error message. + */ + public function test_should_throw_exception( $function_name, $message, $expected_message ) { + $this->expectException( WP_Exception::class ); + $this->expectExceptionMessage( $expected_message ); + + wp_trigger_error( $function_name, $message, E_USER_ERROR ); + } + + /** + * @ticket 57686 + * + * @dataProvider data_should_trigger_error + * + * @param string $function_name The function name to test. + * @param string $message The message to test. + * @param string $expected_message The expected error message. + */ + public function test_should_trigger_warning( $function_name, $message, $expected_message ) { + $this->expectWarning(); + $this->expectWarningMessage( $expected_message ); + + wp_trigger_error( $function_name, $message, E_USER_WARNING ); + } + + /** + * @ticket 57686 + * + * @dataProvider data_should_trigger_error + * + * @param string $function_name The function name to test. + * @param string $message The message to test. + * @param string $expected_message The expected error message. + */ + public function test_should_trigger_notice( $function_name, $message, $expected_message ) { + $this->expectNotice(); + $this->expectNoticeMessage( $expected_message ); + + wp_trigger_error( $function_name, $message ); + } + + /** + * @ticket 57686 + * + * @dataProvider data_should_trigger_error + * + * @param string $function_name The function name to test. + * @param string $message The message to test. + * @param string $expected_message The expected error message. + */ + public function test_should_trigger_deprecation( $function_name, $message, $expected_message ) { + $this->expectDeprecation(); + $this->expectDeprecationMessage( $expected_message ); + + wp_trigger_error( $function_name, $message, E_USER_DEPRECATED ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_trigger_error() { + return array( + 'function name and message are given' => array( + 'function_name' => 'some_function', + 'message' => 'expected the function name and message', + 'expected_message' => 'some_function(): expected the function name and message', + ), + 'message is given' => array( + 'function_name' => '', + 'message' => 'expect only the message', + 'expected_message' => 'expect only the message', + ), + 'function name is given' => array( + 'function_name' => 'some_function', + 'message' => '', + 'expected_message' => 'some_function(): ', + ), + 'allowed HTML elements are present in message' => array( + 'function_name' => 'some_function', + 'message' => '<strong>expected</strong> the function name and message', + 'expected_message' => 'some_function(): <strong>expected</strong> the function name and message', + ), + 'HTML links are present in message' => array( + 'function_name' => 'some_function', + 'message' => '<a href="https://example.com">expected the function name and message</a>', + 'expected_message' => 'some_function(): <a href="https://example.com">expected the function name and message</a>', + ), + 'disallowed HTML elements are present in message' => array( + 'function_name' => 'some_function', + 'message' => '<script>alert("expected the function name and message")</script>', + 'expected_message' => 'some_function(): alert("expected the function name and message")', + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpUniqueIdFromValues.php b/tests/phpunit/tests/functions/wpUniqueIdFromValues.php new file mode 100644 index 0000000000000..d46afd12f5fba --- /dev/null +++ b/tests/phpunit/tests/functions/wpUniqueIdFromValues.php @@ -0,0 +1,154 @@ +<?php + +/** + * Test cases for the `wp_unique_id_from_values()` function. + * + * @package WordPress\UnitTests + * + * @since 6.8.0 + * + * @group functions.php + * @covers ::wp_unique_id_from_values + */ +class Tests_Functions_WpUniqueIdFromValues extends WP_UnitTestCase { + + /** + * Prefix used for testing. + * + * @var string + */ + private $prefix = 'my-prefix-'; + + /** + * Test that the function returns consistent ids for the passed params. + * + * @ticket 62985 + * + * @dataProvider data_wp_unique_id_from_values + * + * @since 6.8.0 + */ + public function test_wp_unique_id_from_values( $data ) { + // Generate IDs. + $unique_id_original = wp_unique_id_from_values( $data ); + $unique_id_prefixed = wp_unique_id_from_values( $data, $this->prefix ); + + // Ensure that the same input produces the same ID. + $this->assertSame( $unique_id_original, wp_unique_id_from_values( $data ) ); + $this->assertSame( $unique_id_prefixed, wp_unique_id_from_values( $data, $this->prefix ) ); + + // Ensure that the prefixed ID is the prefix + the original ID. + $this->assertSame( $this->prefix . $unique_id_original, $unique_id_prefixed ); + } + + /** + * Test that different input data generates distinct IDs. + * + * @ticket 62985 + * + * @dataProvider data_wp_unique_id_from_values + * + * @since 6.8.0 + */ + public function test_wp_unique_id_from_values_uniqueness( $data ) { + // Generate IDs. + $unique_id_original = wp_unique_id_from_values( $data ); + $unique_id_prefixed = wp_unique_id_from_values( $data, $this->prefix ); + + // Modify the data slightly to generate a different ID. + $data_modified = $data; + $data_modified['value'] = 'modified'; + + // Generate new IDs with the modified data. + $unique_id_modified = wp_unique_id_from_values( $data_modified ); + $unique_id_prefixed_modified = wp_unique_id_from_values( $data_modified, $this->prefix ); + + // Assert that the IDs for different data are distinct. + $this->assertNotSame( $unique_id_original, $unique_id_modified ); + $this->assertNotSame( $unique_id_prefixed, $unique_id_prefixed_modified ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_wp_unique_id_from_values() { + return array( + 'string' => array( array( 'value' => 'text' ) ), + 'integer' => array( array( 'value' => 123 ) ), + 'float' => array( array( 'value' => 1.23 ) ), + 'boolean' => array( array( 'value' => true ) ), + 'object' => array( array( 'value' => new StdClass() ) ), + 'null' => array( array( 'value' => null ) ), + 'multiple values' => array( + array( + 'value1' => 'text', + 'value2' => 123, + 'value3' => 1.23, + 'value4' => true, + 'value5' => new StdClass(), + 'value6' => null, + ), + ), + 'nested arrays' => array( + array( + 'list1' => array( + 'value1' => 'text', + 'value2' => 123, + 'value3' => 1.23, + ), + 'list2' => array( + 'value4' => true, + 'value5' => new StdClass(), + 'value6' => null, + ), + ), + ), + ); + } + + /** + * Test that passing an empty array is not allowed. + * + * @ticket 62985 + * + * @expectedIncorrectUsage wp_unique_id_from_values + * + * @since 6.8.0 + */ + public function test_wp_unique_id_from_values_empty_array() { + wp_unique_id_from_values( array(), $this->prefix ); + } + + /** + * Test that passing non-array data throws an error. + * + * @ticket 62985 + * + * @dataProvider data_wp_unique_id_from_values_invalid_data + * + * @since 6.8.0 + */ + public function test_wp_unique_id_from_values_invalid_data( $data ) { + $this->expectException( TypeError::class ); + + wp_unique_id_from_values( $data, $this->prefix ); + } + + /** + * Data provider for invalid data tests. + * + * @return array[] + */ + public function data_wp_unique_id_from_values_invalid_data() { + return array( + 'string' => array( 'text' ), + 'integer' => array( 123 ), + 'float' => array( 1.23 ), + 'boolean' => array( true ), + 'object' => array( new StdClass() ), + 'null' => array( null ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpUniquePrefixedId.php b/tests/phpunit/tests/functions/wpUniquePrefixedId.php new file mode 100644 index 0000000000000..ed6e489a3e259 --- /dev/null +++ b/tests/phpunit/tests/functions/wpUniquePrefixedId.php @@ -0,0 +1,196 @@ +<?php + +/** + * Test cases for the `wp_unique_prefixed_id()` function. + * + * @package WordPress\UnitTests + * + * @since 6.4.0 + * + * @group functions + * @covers ::wp_unique_prefixed_id + */ +class Tests_Functions_WpUniquePrefixedId extends WP_UnitTestCase { + + /** + * Tests that the expected unique prefixed IDs are created. + * + * @ticket 59681 + * + * @dataProvider data_should_create_unique_prefixed_ids + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param mixed $prefix The prefix. + * @param array $expected The next two expected IDs. + */ + public function test_should_create_unique_prefixed_ids( $prefix, $expected ) { + $id1 = wp_unique_prefixed_id( $prefix ); + $id2 = wp_unique_prefixed_id( $prefix ); + + $this->assertNotSame( $id1, $id2, 'The IDs are not unique.' ); + $this->assertSame( $expected, array( $id1, $id2 ), 'The IDs did not match the expected values.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_create_unique_prefixed_ids() { + return array( + 'prefix as empty string' => array( + 'prefix' => '', + 'expected' => array( '1', '2' ), + ), + 'prefix as (string) "0"' => array( + 'prefix' => '0', + 'expected' => array( '01', '02' ), + ), + 'prefix as string' => array( + 'prefix' => 'test', + 'expected' => array( 'test1', 'test2' ), + ), + 'prefix as string with spaces' => array( + 'prefix' => ' ', + 'expected' => array( ' 1', ' 2' ), + ), + 'prefix as (string) "1"' => array( + 'prefix' => '1', + 'expected' => array( '11', '12' ), + ), + 'prefix as a (string) "."' => array( + 'prefix' => '.', + 'expected' => array( '.1', '.2' ), + ), + 'prefix as a block name' => array( + 'prefix' => 'core/list-item', + 'expected' => array( 'core/list-item1', 'core/list-item2' ), + ), + ); + } + + /** + * @ticket 59681 + * + * @dataProvider data_should_raise_notice_and_use_empty_string_prefix_when_nonstring_given + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param mixed $non_string_prefix Non-string prefix. + * @param int $number_of_ids_to_generate Number of IDs to generate. + * As the prefix will default to an empty string, changing the number of IDs generated within each dataset further tests ID uniqueness. + * @param string $expected_message Expected notice message. + * @param array $expected_ids Expected unique IDs. + */ + public function test_should_raise_notice_and_use_empty_string_prefix_when_nonstring_given( $non_string_prefix, $number_of_ids_to_generate, $expected_message, $expected_ids ) { + $this->expectNotice(); + $this->expectNoticeMessage( $expected_message ); + + $ids = array(); + for ( $i = 0; $i < $number_of_ids_to_generate; $i++ ) { + $ids[] = wp_unique_prefixed_id( $non_string_prefix ); + } + + $this->assertSameSets( $ids, array_unique( $ids ), 'IDs are not unique.' ); + $this->assertSameSets( $expected_ids, $ids, 'The IDs did not match the expected values.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_should_raise_notice_and_use_empty_string_prefix_when_nonstring_given() { + $message = 'wp_unique_prefixed_id(): The prefix must be a string. "%s" data type given.'; + return array( + 'prefix as null' => array( + 'non_string_prefix' => null, + 'number_of_ids_to_generate' => 2, + 'expected_message' => sprintf( $message, 'NULL' ), + 'expected_ids' => array( '1', '2' ), + ), + 'prefix as (int) 0' => array( + 'non_string_prefix' => 0, + 'number_of_ids_to_generate' => 3, + 'expected_message' => sprintf( $message, 'integer' ), + 'expected_ids' => array( '1', '2', '3' ), + ), + 'prefix as (int) 1' => array( + 'non_string_prefix' => 1, + 'number_of_ids_to_generate' => 4, + 'expected_data_type' => sprintf( $message, 'integer' ), + 'expected_ids' => array( '1', '2', '3', '4' ), + ), + 'prefix as (bool) false' => array( + 'non_string_prefix' => false, + 'number_of_ids_to_generate' => 5, + 'expected_data_type' => sprintf( $message, 'boolean' ), + 'expected_ids' => array( '1', '2', '3', '4', '5' ), + ), + 'prefix as (double) 98.7' => array( + 'non_string_prefix' => 98.7, + 'number_of_ids_to_generate' => 6, + 'expected_data_type' => sprintf( $message, 'double' ), + 'expected_ids' => array( '1', '2', '3', '4', '5', '6' ), + ), + ); + } + + /** + * Prefixes that are or will become the same should generate unique IDs. + * + * This test is added to avoid future regressions if the function's prefix data type check is + * modified to type juggle or check for scalar data types. + * + * @ticket 59681 + * + * @dataProvider data_same_prefixes_should_generate_unique_ids + * + * @runInSeparateProcess + * @preserveGlobalState disabled + * + * @param array $prefixes The prefixes to check. + * @param array $expected The expected unique IDs. + */ + public function test_same_prefixes_should_generate_unique_ids( array $prefixes, array $expected ) { + // Suppress E_USER_NOTICE, which will be raised when a prefix is non-string. + $original_error_reporting = error_reporting(); + error_reporting( $original_error_reporting & ~E_USER_NOTICE ); + + $ids = array(); + foreach ( $prefixes as $prefix ) { + $ids[] = wp_unique_prefixed_id( $prefix ); + } + + // Reset error reporting. + error_reporting( $original_error_reporting ); + + $this->assertSameSets( $ids, array_unique( $ids ), 'IDs are not unique.' ); + $this->assertSameSets( $expected, $ids, 'The IDs did not match the expected values.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_same_prefixes_should_generate_unique_ids() { + return array( + 'prefixes = empty string' => array( + 'prefixes' => array( null, true, '' ), + 'expected' => array( '1', '2', '3' ), + ), + 'prefixes = 0' => array( + 'prefixes' => array( '0', 0, 0.0, false ), + 'expected' => array( '01', '1', '2', '3' ), + ), + 'prefixes = 1' => array( + 'prefixes' => array( '1', 1, 1.0, true ), + 'expected' => array( '11', '1', '2', '3' ), + ), + ); + } +} diff --git a/tests/phpunit/tests/functions/wpValidateBoolean.php b/tests/phpunit/tests/functions/wpValidateBoolean.php index 785d1bc98641e..832e28ffc5b3a 100644 --- a/tests/phpunit/tests/functions/wpValidateBoolean.php +++ b/tests/phpunit/tests/functions/wpValidateBoolean.php @@ -1,59 +1,64 @@ <?php /** - * Tests the wp_validate_boolean function. + * Tests for the wp_validate_boolean() function. + * + * @group functions * - * @group functions.php * @covers ::wp_validate_boolean */ class Tests_Functions_wpValidateBoolean extends WP_UnitTestCase { - /** - * Provides test scenarios for all possible scenarios in wp_validate_boolean(). - * - * @return array - */ - public function data_provider() { - $std = new \stdClass(); - - return array( - array( null, false ), - array( true, true ), - array( false, false ), - array( 'true', true ), - array( 'false', false ), - array( 'FalSE', false ), // @ticket 30238 - array( 'FALSE', false ), // @ticket 30238 - array( 'TRUE', true ), - array( ' FALSE ', true ), - array( 'yes', true ), - array( 'no', true ), - array( 'string', true ), - array( '', false ), - array( array(), false ), - array( 1, true ), - array( 0, false ), - array( -1, true ), - array( 99, true ), - array( 0.1, true ), - array( 0.0, false ), - array( '1', true ), - array( '0', false ), - array( $std, true ), - ); - } /** - * Test wp_validate_boolean(). - * - * @dataProvider data_provider + * Tests wp_validate_boolean(). * - * @param mixed $test_value - * @param bool $expected + * @dataProvider data_wp_validate_boolean * * @ticket 30238 * @ticket 39868 + * + * @param mixed $test_value Test value. + * @param bool $expected Expected return value. */ public function test_wp_validate_boolean( $test_value, $expected ) { - $this->assertSame( wp_validate_boolean( $test_value ), $expected ); + $this->assertSame( $expected, wp_validate_boolean( $test_value ) ); + } + + /** + * Data provider for test_wp_validate_boolean(). + * + * @return array[] Test parameters { + * @type mixed $test_value Test value. + * @type bool $expected Expected return value. + * } + */ + public function data_wp_validate_boolean() { + $std = new \stdClass(); + + return array( + array( null, false ), + array( true, true ), + array( false, false ), + array( 'true', true ), + array( 'false', false ), + array( 'FalSE', false ), // @ticket 30238 + array( 'FALSE', false ), // @ticket 30238 + array( 'TRUE', true ), + array( ' FALSE ', true ), + array( 'yes', true ), + array( 'no', true ), + array( 'string', true ), + array( '', false ), + array( array(), false ), + array( 1, true ), + array( 0, false ), + array( -1, true ), + array( 99, true ), + array( 0.1, true ), + array( 0.0, false ), + array( '1', true ), + array( '0', false ), + array( $std, true ), + ); } } diff --git a/tests/phpunit/tests/functions/wpVerifyFastHash.php b/tests/phpunit/tests/functions/wpVerifyFastHash.php new file mode 100644 index 0000000000000..56413873a466b --- /dev/null +++ b/tests/phpunit/tests/functions/wpVerifyFastHash.php @@ -0,0 +1,57 @@ +<?php + +/** + * Tests for the behavior of `wp_verify_fast_hash()`. + * + * @group functions + * + * @covers ::wp_verify_fast_hash + */ +class Tests_Functions_wpVerifyFastHash extends WP_UnitTestCase { + + /** + * @ticket 21022 + */ + public function test_wp_verify_fast_hash_verifies_hash() { + $password = 'password'; + + $hash = wp_fast_hash( $password ); + + $this->assertTrue( wp_verify_fast_hash( $password, $hash ) ); + } + + /** + * @ticket 21022 + */ + public function test_wp_verify_fast_hash_fails_unprefixed_hash() { + $password = 'password'; + + $hash = wp_fast_hash( $password ); + + $this->assertFalse( wp_verify_fast_hash( $password, substr( $hash, 9 ) ) ); + } + + /** + * @ticket 21022 + */ + public function test_wp_verify_fast_hash_fails_partial_hash() { + $password = 'password'; + + $hash = wp_fast_hash( $password ); + + $this->assertFalse( wp_verify_fast_hash( $password, substr( $hash, 0, -3 ) ) ); + } + + /** + * @ticket 21022 + */ + public function test_wp_verify_fast_hash_verifies_phpass_hash() { + require_once ABSPATH . WPINC . '/class-phpass.php'; + + $password = 'password'; + + $hash = ( new PasswordHash( 8, true ) )->HashPassword( $password ); + + $this->assertTrue( wp_verify_fast_hash( $password, $hash ) ); + } +} diff --git a/tests/phpunit/tests/functions/xmlrpc.php b/tests/phpunit/tests/functions/xmlrpc.php new file mode 100644 index 0000000000000..925801326f0af --- /dev/null +++ b/tests/phpunit/tests/functions/xmlrpc.php @@ -0,0 +1,70 @@ +<?php + +/** + * @group functions + * @group xmlrpc + * + * @ticket 53490 + */ +class Tests_Functions_XMLRPC extends WP_UnitTestCase { + + private $test_content = ' + <title>title + category,category1 + content + '; + + /** + * Tests that xmlrpc_getposttitle() returns the post title if found in the XML. + * + * @covers ::xmlrpc_getposttitle + */ + public function test_xmlrpc_getposttitle() { + $this->assertSame( 'title', xmlrpc_getposttitle( $this->test_content ) ); + } + + /** + * Tests that xmlrpc_getposttitle() defaults to the `$post_default_title` global. + * + * @covers ::xmlrpc_getposttitle + */ + public function test_xmlrpc_getposttitle_default() { + global $post_default_title; + + $post_default_title = 'post_default_title'; + + $this->assertSame( 'post_default_title', xmlrpc_getposttitle( '' ) ); + } + + + /** + * Tests that xmlrpc_getpostcategory() returns post categories if found in the XML. + * + * @covers ::xmlrpc_getpostcategory + */ + public function test_xmlrpc_getpostcategory() { + $this->assertSame( array( 'category', 'category1' ), xmlrpc_getpostcategory( $this->test_content ) ); + } + + /** + * Tests that xmlrpc_getpostcategory() defaults to the `$post_default_category` global. + * + * @covers ::xmlrpc_getpostcategory + */ + public function test_xmlrpc_getpostcategory_default() { + global $post_default_category; + + $post_default_category = 'post_default_category'; + + $this->assertSame( 'post_default_category', xmlrpc_getpostcategory( '' ) ); + } + + /** + * Tests that xmlrpc_removepostdata() returns XML content without title and category elements. + * + * @covers ::xmlrpc_removepostdata + */ + public function test_xmlrpc_removepostdata() { + $this->assertSame( 'content', xmlrpc_removepostdata( $this->test_content ) ); + } +} diff --git a/tests/phpunit/tests/general/feedLinksExtra.php b/tests/phpunit/tests/general/feedLinksExtra.php new file mode 100644 index 0000000000000..0e42e3d01e23a --- /dev/null +++ b/tests/phpunit/tests/general/feedLinksExtra.php @@ -0,0 +1,645 @@ +user->create( + array( + 'user_login' => 'author_feed_links_extra', + 'role' => 'administrator', + ) + ); + + // Category. + self::$category_id = $factory->category->create( + array( 'name' => 'cat_feed_links_extra' ) + ); + + // Tag. + self::$tag_id = $factory->tag->create( + array( 'name' => 'tag_feed_links_extra' ) + ); + + // Taxonomy. + self::$tax_id = 'tax_feed_links_extra'; + + // Post type. + self::$post_type = 'cpt_feed_links_extra'; + + register_taxonomy( + self::$tax_id, + self::$post_type, + array( + 'labels' => array( + 'name' => 'Taxonomy Terms', + 'singular_name' => 'Taxonomy Term', + ), + ) + ); + + register_post_type( + self::$post_type, + array( + 'public' => true, + 'has_archive' => true, + 'taxonomies' => array( self::$tax_id ), + 'labels' => array( 'name' => 'CPT for feed_links_extra()' ), + ) + ); + + // Posts. + self::$post_no_comment_id = $factory->post->create( + array( 'post_title' => 'Post with no comments' ) + ); + + self::$post_with_comment_id = $factory->post->create( + array( 'post_title' => 'Post with a comment' ) + ); + + $factory->comment->create( + array( + 'comment_author' => self::$author_id, + 'comment_post_ID' => self::$post_with_comment_id, + ) + ); + + self::$post_with_cpt_id = $factory->post->create( + array( + 'post_title' => 'Post with a custom post type', + 'post_type' => self::$post_type, + ) + ); + + wp_set_object_terms( self::$post_with_cpt_id, 'tax_term', self::$tax_id ); + } + + public function set_up() { + parent::set_up(); + + register_taxonomy( + self::$tax_id, + self::$post_type, + array( + 'labels' => array( + 'name' => 'Taxonomy Terms', + 'singular_name' => 'Taxonomy Term', + ), + ) + ); + + register_post_type( + self::$post_type, + array( + 'public' => true, + 'has_archive' => true, + 'taxonomies' => array( self::$tax_id ), + 'labels' => array( 'name' => 'CPT for feed_links_extra()' ), + ) + ); + } + + /** + * @ticket 54713 + * + * @dataProvider data_feed_links_extra + * + * @param string $title The expected title. + * @param string $type The name of the test class property containing the object ID. + * @param array $args { + * Optional arguments. Default empty. + * + * @type string $separator The separator between site name and feed type. + * @type string $singletitle The title of the comments feed. + * @type string $cattitle The title of the category feed. + * @type string $tagtitle The title of the tag feed. + * @type string $taxtitle The title of the taxonomy feed. + * @type string $authortitle The title of the author feed. + * @type string $searchtitle The title of the search feed. + * @type string $posttypetitle The title of the post type feed. + * } + */ + public function test_feed_links_extra( $title, $type, array $args = array() ) { + $permalink = $this->helper_get_the_permalink( $type ); + $this->go_to( $permalink ); + + $expected = ''; + + if ( '' !== $title ) { + if ( 'post_type' === $type || 'search' === $type ) { + $feed_link = $permalink . '&feed=rss2'; + } else { + $feed_link = str_replace( '?', '?feed=rss2&', $permalink ); + } + + $expected = sprintf( + '' . "\n", + esc_attr( $title ), + esc_url( $feed_link ) + ); + } + + $this->assertSame( $expected, get_echo( 'feed_links_extra', array( $args ) ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_feed_links_extra() { + return array( + 'a post with a comment' => array( + 'title' => 'Test Blog » Post with a comment Comments Feed', + 'type' => 'post_with_comment', + ), + 'a post with a comment and a custom separator' => array( + 'title' => 'Test Blog // Post with a comment Comments Feed', + 'type' => 'post_with_comment', + 'args' => array( + 'separator' => '//', + ), + ), + 'a post with a comment and a custom title' => array( + 'title' => 'Custom Title for Singular Feed', + 'type' => 'post_with_comment', + 'args' => array( + 'singletitle' => 'Custom Title for Singular Feed', + ), + ), + 'a post with a comment, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Singular Feed', + 'type' => 'post_with_comment', + 'args' => array( + 'separator' => '//', + 'singletitle' => '%1$s %2$s Custom Title for Singular Feed', + ), + ), + 'a custom post type' => array( + 'title' => 'Test Blog » CPT for feed_links_extra() Feed', + 'type' => 'post_type', + ), + 'a custom post type and a custom separator' => array( + 'title' => 'Test Blog // CPT for feed_links_extra() Feed', + 'type' => 'post_type', + 'args' => array( + 'separator' => '//', + ), + ), + 'a custom post type and a custom title' => array( + 'title' => 'Custom Title for CPT Feed', + 'type' => 'post_type', + 'args' => array( + 'posttypetitle' => 'Custom Title for CPT Feed', + ), + ), + 'a custom post type, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for CPT Feed', + 'type' => 'post_type', + 'args' => array( + 'separator' => '//', + 'posttypetitle' => '%1$s %2$s Custom Title for CPT Feed', + ), + ), + 'a category' => array( + 'title' => 'Test Blog » cat_feed_links_extra Category Feed', + 'type' => 'category', + ), + 'a category and a custom separator' => array( + 'title' => 'Test Blog // cat_feed_links_extra Category Feed', + 'type' => 'category', + 'args' => array( + 'separator' => '//', + ), + ), + 'a category and a custom title' => array( + 'title' => 'Custom Title for Category Feed', + 'type' => 'category', + 'args' => array( + 'cattitle' => 'Custom Title for Category Feed', + ), + ), + 'a category, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Category Feed', + 'type' => 'category', + 'args' => array( + 'separator' => '//', + 'cattitle' => '%1$s %2$s Custom Title for Category Feed', + ), + ), + 'a tag' => array( + 'title' => 'Test Blog » tag_feed_links_extra Tag Feed', + 'type' => 'tag', + ), + 'a tag and a custom separator' => array( + 'title' => 'Test Blog // tag_feed_links_extra Tag Feed', + 'type' => 'tag', + 'args' => array( + 'separator' => '//', + ), + ), + 'a tag and a custom title' => array( + 'title' => 'Custom Title for Tag Feed', + 'type' => 'tag', + 'args' => array( + 'tagtitle' => 'Custom Title for Tag Feed', + ), + ), + 'a tag, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Tag Feed', + 'type' => 'tag', + 'args' => array( + 'separator' => '//', + 'tagtitle' => '%1$s %2$s Custom Title for Tag Feed', + ), + ), + 'a taxonomy' => array( + 'title' => 'Test Blog » tax_term Taxonomy Term Feed', + 'type' => 'tax', + ), + 'a taxonomy and a custom separator' => array( + 'title' => 'Test Blog // tax_term Taxonomy Term Feed', + 'type' => 'tax', + 'args' => array( + 'separator' => '//', + ), + ), + 'a taxonomy and a custom title' => array( + 'title' => 'Custom Title for Taxonomy Feed', + 'type' => 'tax', + 'args' => array( + 'taxtitle' => 'Custom Title for Taxonomy Feed', + ), + ), + 'a taxonomy, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Taxonomy Feed', + 'type' => 'tax', + 'args' => array( + 'separator' => '//', + 'taxtitle' => '%1$s %2$s Custom Title for Taxonomy Feed', + ), + ), + 'an author' => array( + 'title' => 'Test Blog » Posts by author_feed_links_extra Feed', + 'type' => 'author', + ), + 'an author and a custom separator' => array( + 'title' => 'Test Blog // Posts by author_feed_links_extra Feed', + 'type' => 'author', + 'args' => array( + 'separator' => '//', + ), + ), + 'an author and a custom title' => array( + 'title' => 'Custom Title for Author Feed', + 'type' => 'author', + 'args' => array( + 'authortitle' => 'Custom Title for Author Feed', + ), + ), + 'an author, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Author Feed', + 'type' => 'author', + 'args' => array( + 'separator' => '//', + 'authortitle' => '%1$s %2$s Custom Title for Author Feed', + ), + ), + 'search results' => array( + 'title' => 'Test Blog » Search Results for “Search” Feed', + 'type' => 'search', + ), + 'search results and a custom separator' => array( + 'title' => 'Test Blog // Search Results for “Search” Feed', + 'type' => 'search', + 'args' => array( + 'separator' => '//', + ), + ), + 'search results and a custom title' => array( + 'title' => 'Custom Title for Search Feed', + 'type' => 'search', + 'args' => array( + 'searchtitle' => 'Custom Title for Search Feed', + ), + ), + 'search results, a custom separator and a custom title' => array( + 'title' => 'Test Blog // Custom Title for Search Feed', + 'type' => 'search', + 'args' => array( + 'separator' => '//', + 'searchtitle' => '%1$s %2$s Custom Title for Search Feed', + ), + ), + ); + } + + /** + * Helper function to get the permalink based on type. + * + * @param string $type The name of the test class property containing the object ID. + * @return string The permalink. + */ + private function helper_get_the_permalink( $type ) { + if ( 'category' === $type || 'tag' === $type ) { + return get_term_link( self::${$type . '_id'} ); + } + + if ( 'tax' === $type ) { + return get_term_link( 'tax_term', self::$tax_id ); + } + + if ( 'post_type' === $type ) { + return get_post_type_archive_link( self::$post_type ); + } + + if ( 'author' === $type ) { + return get_author_posts_url( self::$author_id ); + } + + if ( 'search' === $type ) { + return home_url( '?s=Search' ); + } + + return get_the_permalink( self::${$type . '_id'} ); + } + + /** + * @ticket 54713 + */ + public function test_feed_links_extra_should_respect_comments_open() { + add_filter( 'comments_open', '__return_true' ); + add_filter( 'pings_open', '__return_false' ); + + $this->go_to( get_the_permalink( self::$post_no_comment_id ) ); + + $expected = '' . "\n"; + $this->assertSame( $expected, get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54713 + */ + public function test_feed_links_extra_should_respect_pings_open() { + add_filter( 'pings_open', '__return_true' ); + add_filter( 'comments_open', '__return_false' ); + + $this->go_to( get_the_permalink( self::$post_no_comment_id ) ); + + $expected = '' . "\n"; + $this->assertSame( $expected, get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54713 + */ + public function test_feed_links_extra_should_respect_post_comment_count() { + add_filter( 'pings_open', '__return_false' ); + add_filter( 'comments_open', '__return_false' ); + + $this->go_to( get_the_permalink( self::$post_with_comment_id ) ); + + $expected = '' . "\n"; + $this->assertSame( $expected, get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54713 + */ + public function test_feed_links_extra_should_return_empty_when_comments_and_pings_are_closed_and_post_has_no_comments() { + add_filter( 'comments_open', '__return_false' ); + add_filter( 'pings_open', '__return_false' ); + + $this->go_to( get_the_permalink( self::$post_no_comment_id ) ); + $this->assertEmpty( get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54713 + */ + public function test_feed_links_extra_should_respect_feed_type() { + add_filter( + 'default_feed', + static function () { + return 'foo'; + } + ); + + add_filter( + 'feed_content_type', + static function () { + return 'testing/foo'; + } + ); + + $this->go_to( get_the_permalink( self::$post_with_comment_id ) ); + + $expected = '' . "\n"; + $this->assertSame( $expected, get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54703 + */ + public function test_feed_links_extra_should_output_nothing_when_show_comments_feed_filter_returns_false() { + add_filter( 'feed_links_show_comments_feed', '__return_false' ); + + $this->go_to( get_the_permalink( self::$post_with_comment_id ) ); + $this->assertEmpty( get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 54703 + * + * @dataProvider data_feed_links_extra_should_output_nothing_when_post_comments_feed_link_is_falsy + * + * @param string $callback The callback to use for the 'post_comments_feed_link' filter. + */ + public function test_feed_links_extra_should_output_nothing_when_post_comments_feed_link_is_falsy( $callback ) { + add_filter( 'post_comments_feed_link', $callback ); + + $this->go_to( get_the_permalink( self::$post_with_comment_id ) ); + $this->assertEmpty( get_echo( 'feed_links_extra' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_feed_links_extra_should_output_nothing_when_post_comments_feed_link_is_falsy() { + return array( + 'empty string' => array( 'callback' => '__return_empty_string' ), + 'empty array' => array( 'callback' => '__return_empty_array' ), + 'zero int' => array( 'callback' => '__return_zero' ), + 'zero float' => array( 'callback' => array( $this, 'cb_return_zero_float' ) ), + 'zero string' => array( 'callback' => array( $this, 'cb_return_zero_string' ) ), + 'null' => array( 'callback' => '__return_null' ), + 'false' => array( 'callback' => '__return_false' ), + ); + } + + /** + * Callback that returns 0.0. + * + * @return float 0.0. + */ + public function cb_return_zero_float() { + return 0.0; + } + + /** + * Callback that returns '0'. + * + * @return string '0'. + */ + public function cb_return_zero_string() { + return '0'; + } + + /** + * @ticket 54703 + */ + public function test_feed_links_extra_should_output_the_comments_feed_link_when_show_comments_feed_filter_returns_true() { + add_filter( 'feed_links_show_comments_feed', '__return_true' ); + + $this->go_to( get_the_permalink( self::$post_with_comment_id ) ); + $this->assertNotEmpty( get_echo( 'feed_links_extra' ) ); + } + + /** + * @ticket 55904 + * + * @dataProvider data_feed_links_extra_should_output_nothing_when_filters_return_false + * + * @param string $type The name of the test class property containing the object ID. + * @param string $filter The name of the filter to set to false. + */ + public function test_feed_links_extra_should_output_nothing_when_filters_return_false( $type, $filter ) { + $permalink = $this->helper_get_the_permalink( $type ); + $this->go_to( $permalink ); + + add_filter( $filter, '__return_false' ); + + $this->assertEmpty( get_echo( 'feed_links_extra' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_feed_links_extra_should_output_nothing_when_filters_return_false() { + return array( + 'a post with a comment' => array( + 'type' => 'post_with_comment', + 'filter' => 'feed_links_extra_show_post_comments_feed', + ), + 'a custom post type' => array( + 'type' => 'post_type', + 'filter' => 'feed_links_extra_show_post_type_archive_feed', + ), + 'a category' => array( + 'type' => 'category', + 'filter' => 'feed_links_extra_show_category_feed', + ), + 'a tag' => array( + 'type' => 'tag', + 'filter' => 'feed_links_extra_show_tag_feed', + ), + 'a taxonomy' => array( + 'type' => 'tax', + 'filter' => 'feed_links_extra_show_tax_feed', + ), + 'an author' => array( + 'type' => 'author', + 'filter' => 'feed_links_extra_show_author_feed', + ), + 'search results' => array( + 'type' => 'search', + 'filter' => 'feed_links_extra_show_search_feed', + ), + ); + } + + /** + * @ticket 63263 + */ + public function test_feed_links_extra_should_work_fail_if_global_post_empty() { + $post_id = self::factory()->post->create(); + $this->go_to( get_permalink( $post_id ) ); + $GLOBALS['post'] = null; + + $this->assertNotEmpty( get_echo( 'feed_links_extra' ) ); + } +} diff --git a/tests/phpunit/tests/general/getCalendar.php b/tests/phpunit/tests/general/getCalendar.php new file mode 100644 index 0000000000000..a8a3d1361f494 --- /dev/null +++ b/tests/phpunit/tests/general/getCalendar.php @@ -0,0 +1,195 @@ +post->create_many( + 3, + array( + 'post_date' => '2025-02-01 12:00:00', + ) + ); + + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_date' => '2025-02-03 12:00:00', + ) + ); + } + + /** + * Set up for each test. + */ + public function set_up() { + parent::set_up(); + + /* + * Navigate to February 2025. + * + * All posts within this test suite are published in February 2025, + * navigating to the month ensures that the correct month is displayed + * in the calendar to allow the assertions to pass. + */ + $this->go_to( '/?m=202502' ); + } + + /** + * Test that get_calendar() displays output when display is true. + * + * @ticket 34093 + */ + public function test_get_calendar_display() { + $calendar_html = get_echo( 'get_calendar', array( array( 'display' => true ) ) ); + $this->assertStringContainsString( 'M
      assertStringContainsString( 'Posts published on February 1, 2025', $calendar_html, 'Calendar is expected to display posts published on February 1, 2025.' ); + $this->assertStringContainsString( '', $calendar_html, 'Calendar is expected to use initials for day names' ); + $this->assertStringContainsString( '
      February 2025assertStringContainsString( '
      M
      assertStringContainsString( 'Posts published on February 3, 2025', $calendar_html, 'Calendar is expected to display page published on February 3, 2025.' ); + $this->assertStringNotContainsString( 'Posts published on February 1, 2025', $calendar_html, 'Calendar is not expected to display posts published on February 1, 2025.' ); + $this->assertStringContainsString( '', $calendar_html, 'Calendar is expected to use initials for day names' ); + $this->assertStringContainsString( '
      February 2025 'page' ) ) ); + + $this->assertStringContainsString( '
      M
      assertStringContainsString( 'Posts published on February 3, 2025', $calendar_html, 'Calendar is expected to display page published on February 3, 2025.' ); + $this->assertStringNotContainsString( 'Posts published on February 1, 2025', $calendar_html, 'Calendar is not expected to display posts published on February 1, 2025.' ); + $this->assertStringContainsString( '', $first_calendar_html, 'First calendar is expected to use initials for day names' ); + $this->assertStringContainsString( '', $second_calendar_html, 'Second calendar is expected to use abbreviations for day names' ); + } + + /** + * Test that get_calendar() uses a different cache for different arguments. + * + * @ticket 34093 + */ + public function test_get_calendar_caching_accounts_for_args() { + $first_calendar_html = get_echo( 'get_calendar' ); + $second_calendar_html = get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + + $this->assertNotSame( $first_calendar_html, $second_calendar_html, 'Each calendar should be different' ); + } + + /** + * Test that get_calendar() uses the same cache for equivalent arguments. + * + * @ticket 34093 + */ + public function test_get_calendar_caching_accounts_for_equivalent_args() { + get_echo( 'get_calendar', array( array( 'post_type' => 'page' ) ) ); + + $num_queries_start = get_num_queries(); + // Including an argument that is the same as the default value shouldn't miss the cache. + get_echo( + 'get_calendar', + array( + array( + 'post_type' => 'page', + 'initial' => true, + ), + ) + ); + + // Changing the order of arguments shouldn't miss the cache. + get_echo( + 'get_calendar', + array( + array( + 'initial' => true, + 'post_type' => 'page', + ), + ) + ); + + // Display param should be ignored for the cache. + get_calendar( + array( + 'post_type' => 'page', + 'initial' => true, + 'display' => false, + ) + ); + $num_queries_end = get_num_queries(); + + $this->assertSame( 0, $num_queries_end - $num_queries_start, 'Cache should be hit for subsequent equivalent calendar queries.' ); + } + + /** + * Test that get_calendar() maintains backwards compatibility with old parameter format. + * + * @ticket 34093 + */ + public function test_get_calendar_backwards_compatibility() { + $first_calendar_html = get_echo( 'get_calendar', array( false ) ); + + wp_cache_delete( 'get_calendar', 'calendar' ); + + $second_calendar_html = get_calendar( false, false ); + + $this->assertStringContainsString( '', $first_calendar_html, 'Calendar is expected to use abbreviations for day names' ); + $this->assertStringContainsString( '', $first_calendar_html, 'Calendar is expected to be captioned February 2025' ); + $this->assertStringContainsString( '
      February 2025 true ) ) ); + $second_calendar_html = get_echo( 'get_calendar', array( array( 'initial' => false ) ) ); + + $this->assertStringContainsString( '
      MMonMon
      February 2025
      assertSame( $first_calendar_html, $second_calendar_html, 'Both calendars should be identical' ); + } +} diff --git a/tests/phpunit/tests/general/paginateLinks.php b/tests/phpunit/tests/general/paginateLinks.php index 516afa25b9fd9..638d777dad149 100644 --- a/tests/phpunit/tests/general/paginateLinks.php +++ b/tests/phpunit/tests/general/paginateLinks.php @@ -7,7 +7,28 @@ */ class Tests_General_PaginateLinks extends WP_UnitTestCase { - private $i18n_count = 0; + private int $i18n_count = 0; + + /** + * Set up shared fixtures. + * + * @param WP_UnitTest_Factory $factory Factory instance. + */ + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ): void { + $category_id = $factory->term->create( + array( + 'taxonomy' => 'category', + 'name' => 'Categorized', + ) + ); + self::assertIsInt( $category_id ); + + $post_ids = $factory->post->create_many( 10 ); + foreach ( $post_ids as $post_id ) { + self::assertIsInt( $post_id ); + self::assertIsArray( wp_set_post_categories( $post_id, array( $category_id ) ) ); + } + } public function set_up() { parent::set_up(); @@ -33,11 +54,17 @@ public function test_defaults() { $this->assertSameIgnoreEOL( $expected, $links ); } - public function test_format() { - $page2 = home_url( '/page/2/' ); - $page3 = home_url( '/page/3/' ); - $page50 = home_url( '/page/50/' ); - + /** + * Test the format parameter behaves as expected. + * + * @dataProvider data_format + * + * @param string $format Format to test. + * @param string $page2 Expected URL for page 2. + * @param string $page3 Expected URL for page 3. + * @param string $page50 Expected URL for page 50. + */ + public function test_format( $format, $page2, $page3, $page50 ) { $expected = <<1 2 @@ -50,12 +77,27 @@ public function test_format() { $links = paginate_links( array( 'total' => 50, - 'format' => 'page/%#%/', + 'format' => $format, ) ); $this->assertSameIgnoreEOL( $expected, $links ); } + /** + * Data provider for test_format. + * + * @return array[] Data provider. + */ + public function data_format() { + return array( + 'pretty permalinks' => array( 'page/%#%/', home_url( '/page/2/' ), home_url( '/page/3/' ), home_url( '/page/50/' ) ), + 'plain permalinks' => array( '?page=%#%', home_url( '/?page=2' ), home_url( '/?page=3' ), home_url( '/?page=50' ) ), + 'custom format - html extension' => array( 'page/%#%.html', home_url( '/page/2.html' ), home_url( '/page/3.html' ), home_url( '/page/50.html' ) ), + 'custom format - hyphen separated' => array( 'page-%#%', home_url( '/page-2' ), home_url( '/page-3' ), home_url( '/page-50' ) ), + 'custom format - fragment' => array( '#%#%', home_url( '/#2' ), home_url( '/#3' ), home_url( '/#50' ) ), + ); + } + public function test_prev_next_false() { $home = home_url( '/' ); $page3 = get_pagenum_link( 3 ); @@ -362,4 +404,163 @@ public function test_custom_base_query_arg_should_be_stripped_from_current_url_b $page_2_url = home_url() . '?foo=2'; $this->assertContains( "2", $links ); } + + /** + * Ensures pagination links include trailing slashes when the permalink structure includes them. + * + * @ticket 61393 + */ + public function test_permalinks_with_trailing_slash_produce_links_with_trailing_slashes(): void { + update_option( 'posts_per_page', 2 ); + $this->set_permalink_structure( '/%postname%/' ); + + $this->go_to( '/category/categorized/page/2/' ); + + // `current` needs to be passed as it's not picked up from the query vars set by `go_to()` above. + $links = paginate_links( array( 'current' => 2 ) ); + + $processor = new WP_HTML_Tag_Processor( $links ); + $found_links = 0; + while ( $processor->next_tag( 'A' ) ) { + ++$found_links; + $href = (string) $processor->get_attribute( 'href' ); + $this->assertStringEndsWith( '/', $href, "Pagination links should end with a trailing slash, found: $href" ); + } + $this->assertGreaterThan( 0, $found_links, 'There should be pagination links found.' ); + } + + /** + * Ensures pagination links do not include trailing slashes when the permalink structure doesn't include them. + * + * @ticket 61393 + */ + public function test_permalinks_without_trailing_slash_produce_links_without_trailing_slashes(): void { + update_option( 'posts_per_page', 2 ); + $this->set_permalink_structure( '/%postname%' ); + + $this->go_to( '/category/categorized/page/2' ); + + // `current` needs to be passed as it's not picked up from the query vars set by `go_to()` above. + $links = paginate_links( array( 'current' => 2 ) ); + + $processor = new WP_HTML_Tag_Processor( $links ); + $found_links = 0; + while ( $processor->next_tag( 'A' ) ) { + ++$found_links; + $href = (string) $processor->get_attribute( 'href' ); + $this->assertStringEndsNotWith( '/', $href, "Pagination links should not end with a trailing slash, found: $href" ); + } + $this->assertGreaterThan( 0, $found_links, 'There should be pagination links found.' ); + } + + /** + * Ensures pagination links do not include trailing slashes when the permalink structure is plain. + * + * @ticket 61393 + */ + public function test_plain_permalinks_are_not_modified_with_trailing_slash(): void { + update_option( 'posts_per_page', 2 ); + $this->set_permalink_structure( '' ); + + $term = get_category_by_slug( 'categorized' ); + $this->assertInstanceOf( WP_Term::class, $term ); + $category_id = $term->term_id; + $this->go_to( "/?cat={$category_id}&paged=2" ); + + // `current` needs to be passed as it's not picked up from the query vars set by `go_to()` above. + $links = paginate_links( array( 'current' => 2 ) ); + + $expected_links = array( + home_url( "?cat={$category_id}" ), // Previous + home_url( "?cat={$category_id}" ), // Page 1 + home_url( "?paged=3&cat={$category_id}" ), // Page 3 + home_url( "?paged=4&cat={$category_id}" ), // Page 4 + home_url( "?paged=5&cat={$category_id}" ), // Page 5 + home_url( "?paged=3&cat={$category_id}" ), // Next + ); + + $processor = new WP_HTML_Tag_Processor( $links ); + $found_links = 0; + while ( $processor->next_tag( 'A' ) ) { + $expected_link = $expected_links[ $found_links ] ?? ''; + ++$found_links; + $href = (string) $processor->get_attribute( 'href' ); + $this->assertSame( $expected_link, $href, "Pagination links should include the category query string, found: $href" ); + } + $this->assertSame( count( $expected_links ), $found_links, 'There should be this number of pagination links found.' ); + } + + /** + * Ensures the pagination links do not modify query strings (permalinks with trailing slash). + * + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_query_strings + * + * @param string $query_string Query string. + */ + public function test_permalinks_with_trailing_slash_do_not_modify_query_strings( string $query_string ): void { + update_option( 'posts_per_page', 2 ); + $this->set_permalink_structure( '/%postname%/' ); + + $this->go_to( "/page/2/?{$query_string}" ); + + // `current` needs to be passed as it's not picked up from the query vars set by `go_to()` above. + $links = paginate_links( array( 'current' => 2 ) ); + + $processor = new WP_HTML_Tag_Processor( $links ); + $found_links = 0; + while ( $processor->next_tag( 'A' ) ) { + ++$found_links; + $href = (string) $processor->get_attribute( 'href' ); + $this->assertStringEndsWith( "/?{$query_string}", $href, "Pagination links should not modify the query string, found: $href" ); + } + $this->assertGreaterThan( 0, $found_links, 'There should be pagination links found.' ); + } + + /** + * Ensures the pagination links do not modify query strings (permalinks without trailing slash). + * + * @ticket 61393 + * @ticket 63123 + * + * @dataProvider data_query_strings + * + * @param string $query_string Query string. + */ + public function test_permalinks_without_trailing_slash_do_not_modify_query_strings( string $query_string ): void { + update_option( 'posts_per_page', 2 ); + $this->set_permalink_structure( '/%postname%' ); + + $this->go_to( "/page/2?{$query_string}" ); + + // `current` needs to be passed as it's not picked up from the query vars set by `go_to()` above. + $links = paginate_links( array( 'current' => 2 ) ); + + $processor = new WP_HTML_Tag_Processor( $links ); + $found_links = 0; + while ( $processor->next_tag( 'A' ) ) { + ++$found_links; + $href = (string) $processor->get_attribute( 'href' ); + $this->assertStringEndsWith( "?{$query_string}", $href, "Pagination links should not modify the query string, found: $href" ); + $this->assertStringEndsNotWith( "/?{$query_string}", $href, "Pagination links should not be slashed before the query string, found: $href" ); + } + $this->assertGreaterThan( 0, $found_links, 'There should be pagination links found.' ); + } + + /** + * Data provider. + * + * @see self::test_permalinks_without_trailing_slash_do_not_modify_query_strings() + * @see self::test_permalinks_with_trailing_slash_do_not_modify_query_strings() + * + * @return array Data provider. + */ + public function data_query_strings(): array { + return array( + 'single query var' => array( 'foo=bar' ), + 'multi query vars' => array( 'foo=bar&pen=pencil' ), + ); + } } diff --git a/tests/phpunit/tests/general/template.php b/tests/phpunit/tests/general/template.php index ca2e6063a1397..d3b35a2c46c2b 100644 --- a/tests/phpunit/tests/general/template.php +++ b/tests/phpunit/tests/general/template.php @@ -10,6 +10,7 @@ require_once ABSPATH . 'wp-admin/includes/class-wp-site-icon.php'; class Tests_General_Template extends WP_UnitTestCase { + protected $wp_site_icon; public $site_icon_id; public $site_icon_url; @@ -17,9 +18,83 @@ class Tests_General_Template extends WP_UnitTestCase { public $custom_logo_id; public $custom_logo_url; + /** + * Blog page used by aria tests. + * + * @var int + */ + public static $blog_page_id; + + /** + * Home page used by aria tests. + * + * @var int + */ + public static $home_page_id; + + /** + * ID of the administrator user. + * + * @var int + */ + public static $administrator_id; + + /** + * ID of the author user. + * + * @var int + */ + public static $author_id; + + /** + * Set up the shared fixtures. + * + * @param WP_UnitTest_Factory $factory Factory instance. + */ + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$administrator_id = $factory->user->create( array( 'role' => 'administrator' ) ); + self::$author_id = $factory->user->create( array( 'role' => 'author' ) ); + + /* + * Declare theme support for custom logo. + * + * This ensures that the `site_logo` option gets deleted in + * _delete_site_logo_on_remove_theme_mods(), which in turn + * prevents the `core/site-logo` block filters from affecting + * the custom logo tests. + * + * Alternatively, these filters can be removed instead: + * + * remove_filter( 'theme_mod_custom_logo', '_override_custom_logo_theme_mod' ); + * remove_filter( 'pre_set_theme_mod_custom_logo', '_sync_custom_logo_to_site_logo' ); + */ + add_theme_support( 'custom-logo' ); + + self::$blog_page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Blog', + 'page_name' => 'blog', + ) + ); + + self::$home_page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_title' => 'Home', + 'page_name' => 'home', + ) + ); + } + + public static function wpTearDownAfterClass() { + remove_theme_support( 'custom-logo' ); + } + public function set_up() { parent::set_up(); + switch_theme( 'default' ); $this->wp_site_icon = new WP_Site_Icon(); } @@ -38,13 +113,13 @@ public function tear_down() { * @requires function imagejpeg */ public function test_get_site_icon_url() { - $this->assertEmpty( get_site_icon_url() ); + $this->assertEmpty( get_site_icon_url(), 'Site icon URL should not be set initially.' ); $this->set_site_icon(); - $this->assertSame( $this->site_icon_url, get_site_icon_url() ); + $this->assertSame( $this->site_icon_url, get_site_icon_url(), 'Site icon URL should be set.' ); $this->remove_site_icon(); - $this->assertEmpty( get_site_icon_url() ); + $this->assertEmpty( get_site_icon_url(), 'Site icon URL should not be set after removal.' ); } /** @@ -67,13 +142,13 @@ public function test_site_icon_url() { * @requires function imagejpeg */ public function test_has_site_icon() { - $this->assertFalse( has_site_icon() ); + $this->assertFalse( has_site_icon(), 'Site icon should not be set initially.' ); $this->set_site_icon(); - $this->assertTrue( has_site_icon() ); + $this->assertTrue( has_site_icon(), 'Site icon should be set.' ); $this->remove_site_icon(); - $this->assertFalse( has_site_icon() ); + $this->assertFalse( has_site_icon(), 'Site icon should not be set after removal.' ); } /** @@ -83,7 +158,7 @@ public function test_has_site_icon() { * @covers ::has_site_icon */ public function test_has_site_icon_returns_true_when_called_for_other_site_with_site_icon_set() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $this->set_site_icon(); restore_current_blog(); @@ -98,7 +173,7 @@ public function test_has_site_icon_returns_true_when_called_for_other_site_with_ * @covers ::has_site_icon */ public function test_has_site_icon_returns_false_when_called_for_other_site_without_site_icon_set() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); $this->assertFalse( has_site_icon( $blog_id ) ); } @@ -159,7 +234,7 @@ public function test_wp_site_icon_with_filter() { */ public function test_customize_preview_wp_site_icon_empty() { global $wp_customize; - wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + wp_set_current_user( self::$administrator_id ); require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; $wp_customize = new WP_Customize_Manager(); @@ -177,7 +252,7 @@ public function test_customize_preview_wp_site_icon_empty() { */ public function test_customize_preview_wp_site_icon_dirty() { global $wp_customize; - wp_set_current_user( $this->factory()->user->create( array( 'role' => 'administrator' ) ) ); + wp_set_current_user( self::$administrator_id ); require_once ABSPATH . WPINC . '/class-wp-customize-manager.php'; $wp_customize = new WP_Customize_Manager(); @@ -261,13 +336,13 @@ private function insert_attachment() { * @since 4.5.0 */ public function test_has_custom_logo() { - $this->assertFalse( has_custom_logo() ); + $this->assertFalse( has_custom_logo(), 'Custom logo should not be set initially.' ); $this->set_custom_logo(); - $this->assertTrue( has_custom_logo() ); + $this->assertTrue( has_custom_logo(), 'Custom logo should be set.' ); $this->remove_custom_logo(); - $this->assertFalse( has_custom_logo() ); + $this->assertFalse( has_custom_logo(), 'Custom logo should not be set after removal.' ); } /** @@ -277,7 +352,7 @@ public function test_has_custom_logo() { * @covers ::has_custom_logo */ public function test_has_custom_logo_returns_true_when_called_for_other_site_with_custom_logo_set() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $this->set_custom_logo(); restore_current_blog(); @@ -292,7 +367,7 @@ public function test_has_custom_logo_returns_true_when_called_for_other_site_wit * @covers ::has_custom_logo */ public function test_has_custom_logo_returns_false_when_called_for_other_site_without_custom_logo_set() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); $this->assertFalse( has_custom_logo( $blog_id ) ); } @@ -304,15 +379,15 @@ public function test_has_custom_logo_returns_false_when_called_for_other_site_wi * @since 4.5.0 */ public function test_get_custom_logo() { - $this->assertEmpty( get_custom_logo() ); + $this->assertEmpty( get_custom_logo(), 'Custom logo should not be set initially.' ); $this->set_custom_logo(); $custom_logo = get_custom_logo(); - $this->assertNotEmpty( $custom_logo ); - $this->assertIsString( $custom_logo ); + $this->assertNotEmpty( $custom_logo, 'Custom logo markup should not be empty.' ); + $this->assertIsString( $custom_logo, 'Custom logo markup should be a string.' ); $this->remove_custom_logo(); - $this->assertEmpty( get_custom_logo() ); + $this->assertEmpty( get_custom_logo(), 'Custom logo should not be set after removal.' ); } /** @@ -322,7 +397,7 @@ public function test_get_custom_logo() { * @covers ::get_custom_logo */ public function test_get_custom_logo_returns_logo_when_called_for_other_site_with_custom_logo_set() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $this->set_custom_logo(); @@ -402,7 +477,7 @@ public function test_the_custom_logo_with_alt() { } /** - * Sets a site icon in options for testing. + * Sets a custom logo in options for testing. * * @since 4.5.0 */ @@ -415,7 +490,7 @@ private function set_custom_logo() { } /** - * Removes the site icon from options. + * Removes the custom logo from options. * * @since 4.5.0 */ @@ -445,7 +520,7 @@ private function insert_custom_logo() { * @covers ::get_site_icon_url */ public function test_get_site_icon_url_preserves_switched_state() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $expected = $GLOBALS['_wp_switched_stack']; @@ -465,7 +540,7 @@ public function test_get_site_icon_url_preserves_switched_state() { * @covers ::has_custom_logo */ public function test_has_custom_logo_preserves_switched_state() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $expected = $GLOBALS['_wp_switched_stack']; @@ -485,7 +560,7 @@ public function test_has_custom_logo_preserves_switched_state() { * @covers ::get_custom_logo */ public function test_get_custom_logo_preserves_switched_state() { - $blog_id = $this->factory->blog->create(); + $blog_id = self::factory()->blog->create(); switch_to_blog( $blog_id ); $expected = $GLOBALS['_wp_switched_stack']; @@ -499,6 +574,139 @@ public function test_get_custom_logo_preserves_switched_state() { $this->assertSame( $expected, $result ); } + /** + * Test the aria attribute for the custom logo on the front page set to the blog. + * + * @ticket 62879 + * + * @covers ::get_custom_logo + * + * @dataProvider data_get_custom_logo_aria_current_attribute_blog_front_page + * + * @param string $url The URL to visit. + * @param bool $attribute_expected Whether the aria-current attribute is expected. + */ + public function test_get_custom_logo_aria_current_attribute_blog_front_page( $url, $attribute_expected ) { + // Set the custom logo. + $this->set_custom_logo(); + $this->go_to( $url ); + + $this->assertNotEmpty( get_custom_logo(), 'Custom logo is expected to be set' ); + + if ( $attribute_expected ) { + $this->assertStringContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } else { + $this->assertStringNotContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } + } + + /** + * Data provider for the test_get_custom_logo_aria_current_attribute_blog_front_page. + * + * @return array[] + */ + public function data_get_custom_logo_aria_current_attribute_blog_front_page() { + return array( + 'Front page' => array( home_url(), true ), + 'Blog post' => array( home_url( '/?p=1' ), false ), + 'Sample page' => array( home_url( '/?page_id=2' ), false ), + ); + } + + /** + * Test the aria attribute for the custom logo on the front page set to the blog. + * + * @ticket 62879 + * + * @covers ::get_custom_logo + * + * @dataProvider data_get_custom_logo_aria_current_attribute_blog_set_to_page_without_front_page_defined + * @param string $url The URL to visit. + * @param bool $attribute_expected Whether the aria-current attribute is expected. + */ + public function test_get_custom_logo_aria_current_attribute_blog_set_to_page_without_front_page_defined( $url, $attribute_expected ) { + // Set up pretty permalinks. + update_option( 'permalink_structure', '/%postname%/' ); + + // Set posts to show on a static page. + update_option( 'show_on_front', 'page' ); + update_option( 'page_for_posts', self::$blog_page_id ); + + // Set the custom logo. + $this->set_custom_logo(); + $this->go_to( $url ); + + $this->assertNotEmpty( get_custom_logo(), 'Custom logo is expected to be set' ); + + if ( $attribute_expected ) { + $this->assertStringContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } else { + $this->assertStringNotContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } + } + + /** + * Data provider for the test_get_custom_logo_aria_current_attribute_blog_set_to_page_without_front_page_defined. + * + * @return array[] + */ + public function data_get_custom_logo_aria_current_attribute_blog_set_to_page_without_front_page_defined() { + return array( + 'Front page' => array( home_url(), true ), + 'Blog index' => array( home_url( '/blog/' ), true ), + 'Blog post' => array( home_url( '/?p=1' ), false ), + 'Sample page' => array( home_url( '/?page_id=2' ), false ), + ); + } + + /** + * Test the aria attribute for the custom logo on the front page set to the blog. + * + * @ticket 62879 + * + * @covers ::get_custom_logo + * + * @dataProvider data_get_custom_logo_aria_current_attribute_blog_set_to_page_with_front_page_defined + * + * @param string $url The URL to visit. + * @param bool $attribute_expected Whether the aria-current attribute is expected. + */ + public function test_get_custom_logo_aria_current_attribute_blog_set_to_page_with_front_page_defined( $url, $attribute_expected ) { + // Set up pretty permalinks. + update_option( 'permalink_structure', '/%postname%/' ); + + // Set posts to show on a static page, show static page on front. + update_option( 'show_on_front', 'page' ); + update_option( 'page_for_posts', self::$blog_page_id ); + update_option( 'page_on_front', self::$home_page_id ); + + // Set the custom logo. + $this->set_custom_logo(); + $this->go_to( $url ); + + $this->assertNotEmpty( get_custom_logo(), 'Custom logo is expected to be set' ); + + if ( $attribute_expected ) { + $this->assertStringContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } else { + $this->assertStringNotContainsString( 'aria-current="page"', get_custom_logo(), 'Custom logo is expected to contain aria-current attribute' ); + } + } + + /** + * Data provider for the test_get_custom_logo_aria_current_attribute_blog_set_to_page_with_front_page_defined. + * + * @return array[] + */ + public function data_get_custom_logo_aria_current_attribute_blog_set_to_page_with_front_page_defined() { + return array( + 'Front page' => array( home_url(), true ), + 'Blog index' => array( home_url( '/blog/' ), true ), + 'Blog post' => array( home_url( '/?p=1' ), false ), + 'Sample page' => array( home_url( '/?page_id=2' ), false ), + ); + } + /** * @ticket 40969 * @@ -577,18 +785,10 @@ public function test_get_template_part_passes_arguments_to_template() { * @covers ::get_the_archive_title */ public function test_get_the_archive_title_is_correct_for_author_queries() { - $user_with_posts = $this->factory()->user->create_and_get( - array( - 'role' => 'author', - ) - ); - $user_with_no_posts = $this->factory()->user->create_and_get( - array( - 'role' => 'author', - ) - ); + $user_with_posts = get_user_by( 'id', self::$administrator_id ); + $user_with_no_posts = get_user_by( 'id', self::$author_id ); - $this->factory()->post->create( + self::factory()->post->create( array( 'post_author' => $user_with_posts->ID, ) diff --git a/tests/phpunit/tests/general/template_CheckedSelectedHelper.php b/tests/phpunit/tests/general/template_CheckedSelectedHelper.php index 2c3afa9a80726..c6d3a5b9deb89 100644 --- a/tests/phpunit/tests/general/template_CheckedSelectedHelper.php +++ b/tests/phpunit/tests/general/template_CheckedSelectedHelper.php @@ -56,12 +56,9 @@ public function test_disabled_with_equal_values() { * * @ticket 53858 * @covers ::readonly + * @requires PHP < 8.1 */ public function test_readonly_with_equal_values() { - if ( ! function_exists( 'readonly' ) ) { - $this->markTestSkipped( 'readonly() function is not available on PHP 8.1' ); - } - $this->setExpectedDeprecated( 'readonly' ); // Call the function via a variable to prevent a parse error for this file on PHP 8.1. diff --git a/tests/phpunit/tests/general/wpError.php b/tests/phpunit/tests/general/wpError.php index fee4233d6ab2d..cae5b2593f2cb 100644 --- a/tests/phpunit/tests/general/wpError.php +++ b/tests/phpunit/tests/general/wpError.php @@ -5,7 +5,6 @@ * @group general * @group errors * - * @covers WP_Error * @coversDefaultClass WP_Error */ class Tests_General_wpError extends WP_UnitTestCase { @@ -42,7 +41,6 @@ public function test_WP_Error_with_default_empty_parameters_should_add_no_errors /** * @covers ::__construct - * @covers ::get_error_code */ public function test_WP_Error_with_empty_code_should_add_no_code() { $this->assertSame( '', $this->wp_error->get_error_code() ); @@ -50,7 +48,6 @@ public function test_WP_Error_with_empty_code_should_add_no_code() { /** * @covers ::__construct - * @covers ::get_error_message */ public function test_WP_Error_with_empty_code_should_add_no_message() { $this->assertSame( '', $this->wp_error->get_error_message() ); @@ -65,7 +62,6 @@ public function test_WP_Error_with_empty_code_should_add_no_error_data() { /** * @covers ::__construct - * @covers ::get_error_code */ public function test_WP_Error_with_code_and_empty_message_should_add_error_with_that_code() { $wp_error = new WP_Error( 'code' ); @@ -75,7 +71,6 @@ public function test_WP_Error_with_code_and_empty_message_should_add_error_with_ /** * @covers ::__construct - * @covers ::get_error_message */ public function test_WP_Error_with_code_and_empty_message_should_add_error_with_that_code_and_empty_message() { $wp_error = new WP_Error( 'code' ); @@ -85,7 +80,6 @@ public function test_WP_Error_with_code_and_empty_message_should_add_error_with_ /** * @covers ::__construct - * @covers ::get_error_data */ public function test_WP_Error_with_code_and_empty_message_and_empty_data_should_add_error_but_not_associated_data() { $wp_error = new WP_Error( 'code' ); @@ -95,7 +89,6 @@ public function test_WP_Error_with_code_and_empty_message_and_empty_data_should_ /** * @covers ::__construct - * @covers ::get_error_data */ public function test_WP_Error_with_code_and_empty_message_and_non_empty_data_should_add_error_with_empty_message_and_that_stored_data() { $wp_error = new WP_Error( 'code', '', 'data' ); @@ -105,7 +98,6 @@ public function test_WP_Error_with_code_and_empty_message_and_non_empty_data_sho /** * @covers ::__construct - * @covers ::get_error_code */ public function test_WP_Error_with_code_and_message_should_add_error_with_that_code() { $wp_error = new WP_Error( 'code', 'message' ); @@ -115,7 +107,6 @@ public function test_WP_Error_with_code_and_message_should_add_error_with_that_c /** * @covers ::__construct - * @covers ::get_error_message */ public function test_WP_Error_with_code_and_message_should_add_error_with_that_message() { $wp_error = new WP_Error( 'code', 'message' ); @@ -125,7 +116,6 @@ public function test_WP_Error_with_code_and_message_should_add_error_with_that_m /** * @covers ::__construct - * @covers ::get_error_code */ public function test_WP_Error_with_code_and_message_and_data_should_add_error_with_that_code() { $wp_error = new WP_Error( 'code', 'message', 'data' ); @@ -135,7 +125,6 @@ public function test_WP_Error_with_code_and_message_and_data_should_add_error_wi /** * @covers ::__construct - * @covers ::get_error_message */ public function test_WP_Error_with_code_and_message_and_data_should_add_error_with_that_message() { $wp_error = new WP_Error( 'code', 'message', 'data' ); @@ -145,7 +134,6 @@ public function test_WP_Error_with_code_and_message_and_data_should_add_error_wi /** * @covers ::__construct - * @covers ::get_error_data */ public function test_WP_Error_with_code_and_message_and_data_should_add_error_with_that_data() { $wp_error = new WP_Error( 'code', 'message', 'data' ); @@ -154,7 +142,6 @@ public function test_WP_Error_with_code_and_message_and_data_should_add_error_wi } /** - * @covers ::__construct * @covers ::get_error_codes */ public function test_get_error_codes_with_no_errors_should_return_empty_array() { @@ -162,7 +149,6 @@ public function test_get_error_codes_with_no_errors_should_return_empty_array() } /** - * @covers ::add * @covers ::get_error_codes */ public function test_get_error_codes_with_one_error_should_return_an_array_with_only_that_code() { @@ -172,7 +158,6 @@ public function test_get_error_codes_with_one_error_should_return_an_array_with_ } /** - * @covers ::add * @covers ::get_error_codes */ public function test_get_error_codes_with_multiple_errors_should_return_an_array_of_those_codes() { @@ -185,7 +170,6 @@ public function test_get_error_codes_with_multiple_errors_should_return_an_array } /** - * @covers ::__construct * @covers ::get_error_code */ public function test_get_error_code_with_no_errors_should_return_an_empty_string() { @@ -193,7 +177,6 @@ public function test_get_error_code_with_no_errors_should_return_an_empty_string } /** - * @covers ::add * @covers ::get_error_code */ public function test_get_error_code_with_one_error_should_return_that_error_code() { @@ -203,7 +186,6 @@ public function test_get_error_code_with_one_error_should_return_that_error_code } /** - * @covers ::add * @covers ::get_error_code */ public function test_get_error_code_with_multiple_errors_should_return_only_the_first_error_code() { @@ -214,7 +196,6 @@ public function test_get_error_code_with_multiple_errors_should_return_only_the_ } /** - * @covers ::__construct * @covers ::get_error_messages */ public function test_get_error_messages_with_empty_code_and_no_errors_should_return_an_empty_array() { @@ -222,7 +203,6 @@ public function test_get_error_messages_with_empty_code_and_no_errors_should_ret } /** - * @covers ::add * @covers ::get_error_messages */ public function test_get_error_messages_with_empty_code_one_error_should_return_an_array_with_that_message() { @@ -232,7 +212,6 @@ public function test_get_error_messages_with_empty_code_one_error_should_return_ } /** - * @covers ::add * @covers ::get_error_messages */ public function test_get_error_messages_with_empty_code_multiple_errors_should_return_an_array_of_messages() { @@ -243,7 +222,6 @@ public function test_get_error_messages_with_empty_code_multiple_errors_should_r } /** - * @covers ::__construct * @covers ::get_error_messages */ public function test_get_error_messages_with_an_invalid_code_should_return_an_empty_array() { @@ -251,7 +229,6 @@ public function test_get_error_messages_with_an_invalid_code_should_return_an_em } /** - * @covers ::add * @covers ::get_error_messages */ public function test_get_error_messages_with_one_error_should_return_an_array_with_that_message() { @@ -261,7 +238,6 @@ public function test_get_error_messages_with_one_error_should_return_an_array_wi } /** - * @covers ::add * @covers ::get_error_messages */ public function test_get_error_messages_with_multiple_errors_same_code_should_return_an_array_with_all_messages() { @@ -272,7 +248,6 @@ public function test_get_error_messages_with_multiple_errors_same_code_should_re } /** - * @covers ::__construct * @covers ::get_error_message */ public function test_get_error_message_with_empty_code_and_no_errors_should_return_an_empty_string() { @@ -280,7 +255,6 @@ public function test_get_error_message_with_empty_code_and_no_errors_should_retu } /** - * @covers ::add * @covers ::get_error_message */ public function test_get_error_message_with_empty_code_and_one_error_should_return_that_message() { @@ -290,7 +264,6 @@ public function test_get_error_message_with_empty_code_and_one_error_should_retu } /** - * @covers ::add * @covers ::get_error_message */ public function test_get_error_message_with_empty_code_and_multiple_errors_should_return_the_first_message() { @@ -301,7 +274,6 @@ public function test_get_error_message_with_empty_code_and_multiple_errors_shoul } /** - * @covers ::add * @covers ::get_error_message */ public function test_get_error_message_with_empty_code_and_multiple_errors_multiple_codes_should_return_the_first_message() { @@ -313,7 +285,6 @@ public function test_get_error_message_with_empty_code_and_multiple_errors_multi } /** - * @covers ::__construct * @covers ::get_error_message */ public function test_get_error_message_with_invalid_code_and_no_errors_should_return_empty_string() { @@ -321,7 +292,6 @@ public function test_get_error_message_with_invalid_code_and_no_errors_should_re } /** - * @covers ::add * @covers ::get_error_message */ public function test_get_error_message_with_invalid_code_and_one_error_should_return_an_empty_string() { @@ -331,7 +301,6 @@ public function test_get_error_message_with_invalid_code_and_one_error_should_re } /** - * @covers ::add * @covers ::get_error_message */ public function test_get_error_message_with_invalid_code_and_multiple_errors_should_return_an_empty_string() { @@ -342,7 +311,6 @@ public function test_get_error_message_with_invalid_code_and_multiple_errors_sho } /** - * @covers ::__construct * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_and_no_errors_should_evaluate_as_null() { @@ -350,7 +318,6 @@ public function test_get_error_data_with_empty_code_and_no_errors_should_evaluat } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_one_error_no_data_should_evaluate_as_null() { @@ -360,7 +327,6 @@ public function test_get_error_data_with_empty_code_one_error_no_data_should_eva } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_multiple_errors_no_data_should_evaluate_as_null() { @@ -371,7 +337,6 @@ public function test_get_error_data_with_empty_code_multiple_errors_no_data_shou } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_and_one_error_with_data_should_return_that_data() { @@ -382,7 +347,6 @@ public function test_get_error_data_with_empty_code_and_one_error_with_data_shou } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_and_multiple_errors_different_codes_should_return_the_last_data_of_the_first_code() { @@ -394,7 +358,6 @@ public function test_get_error_data_with_empty_code_and_multiple_errors_differen } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_empty_code_and_multiple_errors_same_code_should_return_the_last_data_of_the_first_code() { @@ -406,7 +369,6 @@ public function test_get_error_data_with_empty_code_and_multiple_errors_same_cod } /** - * @covers ::__construct * @covers ::get_error_data */ public function test_get_error_data_with_code_and_no_errors_should_evaluate_as_null() { @@ -414,7 +376,6 @@ public function test_get_error_data_with_code_and_no_errors_should_evaluate_as_n } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_code_and_one_error_with_no_data_should_evaluate_as_null() { @@ -424,7 +385,6 @@ public function test_get_error_data_with_code_and_one_error_with_no_data_should_ } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_code_and_one_error_with_data_should_return_that_data() { @@ -435,7 +395,6 @@ public function test_get_error_data_with_code_and_one_error_with_data_should_ret } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_code_and_multiple_errors_different_codes_should_return_the_last_stored_data_of_the_code() { @@ -448,7 +407,6 @@ public function test_get_error_data_with_code_and_multiple_errors_different_code } /** - * @covers ::add * @covers ::get_error_data */ public function test_get_error_data_with_code_and_multiple_errors_same_code_should_return_the_last_stored_data() { @@ -460,7 +418,6 @@ public function test_get_error_data_with_code_and_multiple_errors_same_code_shou } /** - * @covers ::__construct * @covers ::get_all_error_data */ public function test_get_all_error_data_with_code_and_no_errors_should_evaluate_as_empty_array() { @@ -468,7 +425,6 @@ public function test_get_all_error_data_with_code_and_no_errors_should_evaluate_ } /** - * @covers ::add * @covers ::get_all_error_data */ public function test_get_all_error_data_with_code_and_one_error_with_no_data_should_evaluate_as_empty_array() { @@ -478,7 +434,6 @@ public function test_get_all_error_data_with_code_and_one_error_with_no_data_sho } /** - * @covers ::add * @covers ::get_all_error_data */ public function test_get_all_error_data_with_code_and_one_error_with_data_should_return_that_data() { @@ -491,7 +446,6 @@ public function test_get_all_error_data_with_code_and_one_error_with_data_should } /** - * @covers ::add * @covers ::get_all_error_data */ public function test_get_all_error_data_with_code_and_multiple_errors_same_code_should_return_all_data() { @@ -503,7 +457,6 @@ public function test_get_all_error_data_with_code_and_multiple_errors_same_code_ } /** - * @covers ::add * @covers ::get_all_error_data */ public function test_get_all_error_data_should_handle_manipulation_of_error_data_property() { @@ -517,7 +470,6 @@ public function test_get_all_error_data_should_handle_manipulation_of_error_data } /** - * @covers ::__construct * @covers ::has_errors */ public function test_has_errors_with_no_errors_returns_false() { @@ -525,7 +477,6 @@ public function test_has_errors_with_no_errors_returns_false() { } /** - * @covers ::add * @covers ::has_errors */ public function test_has_errors_with_errors_returns_true() { @@ -571,7 +522,6 @@ public function test_add_with_empty_code_empty_message_non_empty_data_should_sto /** * @covers ::add - * @covers ::get_error_code */ public function test_add_with_code_empty_message_empty_data_should_add_error_with_code() { $this->wp_error->add( 'code', '' ); @@ -581,7 +531,6 @@ public function test_add_with_code_empty_message_empty_data_should_add_error_wit /** * @covers ::add - * @covers ::get_error_message */ public function test_add_with_code_empty_message_empty_data_should_add_error_with_empty_message() { $this->wp_error->add( 'code', '' ); @@ -591,7 +540,6 @@ public function test_add_with_code_empty_message_empty_data_should_add_error_wit /** * @covers ::add - * @covers ::get_error_data */ public function test_add_with_code_empty_message_empty_data_should_not_add_error_data() { $this->wp_error->add( 'code', '' ); @@ -601,7 +549,6 @@ public function test_add_with_code_empty_message_empty_data_should_not_add_error /** * @covers ::add - * @covers ::get_error_message */ public function test_add_with_code_and_message_and_empty_data_should_should_add_error_with_that_message() { $this->wp_error->add( 'code', 'message' ); @@ -611,7 +558,6 @@ public function test_add_with_code_and_message_and_empty_data_should_should_add_ /** * @covers ::add - * @covers ::get_error_data */ public function test_add_with_code_and_message_and_empty_data_should_not_alter_stored_data() { $this->wp_error->add( 'code', 'message' ); @@ -621,7 +567,6 @@ public function test_add_with_code_and_message_and_empty_data_should_not_alter_s /** * @covers ::add - * @covers ::get_error_code */ public function test_add_with_code_and_empty_message_and_data_should_add_error_with_that_code() { $this->wp_error->add( 'code', '', 'data' ); @@ -631,7 +576,6 @@ public function test_add_with_code_and_empty_message_and_data_should_add_error_w /** * @covers ::add - * @covers ::get_error_data */ public function test_add_with_code_and_empty_message_and_data_should_store_that_data() { $this->wp_error->add( 'code', '', 'data' ); @@ -641,7 +585,6 @@ public function test_add_with_code_and_empty_message_and_data_should_store_that_ /** * @covers ::add - * @covers ::get_error_code */ public function test_add_with_code_and_message_and_data_should_add_an_error_with_that_code() { $this->wp_error->add( 'code', 'message', 'data' ); @@ -651,7 +594,6 @@ public function test_add_with_code_and_message_and_data_should_add_an_error_with /** * @covers ::add - * @covers ::get_error_message */ public function test_add_with_code_and_message_and_data_should_add_an_error_with_that_message() { $this->wp_error->add( 'code', 'message', 'data' ); @@ -661,7 +603,6 @@ public function test_add_with_code_and_message_and_data_should_add_an_error_with /** * @covers ::add - * @covers ::get_error_data */ public function test_add_with_code_and_message_and_data_should_store_that_data() { $this->wp_error->add( 'code', 'message', 'data' ); @@ -671,7 +612,6 @@ public function test_add_with_code_and_message_and_data_should_store_that_data() /** * @covers ::add - * @covers ::get_error_messages */ public function test_add_multiple_times_with_the_same_code_should_add_additional_messages_for_that_code() { $this->wp_error->add( 'code', 'message' ); @@ -684,7 +624,6 @@ public function test_add_multiple_times_with_the_same_code_should_add_additional /** * @covers ::add - * @covers ::get_error_data */ public function test_add_multiple_times_with_the_same_code_and_different_data_should_store_only_the_last_added_data() { $this->wp_error->add( 'code', 'message', 'data-bar' ); @@ -713,7 +652,6 @@ public function test_add_data_with_empty_data_empty_code_no_errors_should_create /** * @covers ::add_data - * @covers ::get_error_data */ public function test_add_data_with_data_empty_code_and_one_error_should_store_the_data_under_that_code() { $this->wp_error->add( 'code', 'message' ); @@ -724,7 +662,6 @@ public function test_add_data_with_data_empty_code_and_one_error_should_store_th /** * @covers ::add_data - * @covers ::get_error_data */ public function test_add_data_with_data_empty_code_and_multiple_errors_with_different_codes_should_store_it_under_the_first_code() { $this->wp_error->add( 'code', 'message' ); @@ -737,7 +674,6 @@ public function test_add_data_with_data_empty_code_and_multiple_errors_with_diff /** * @covers ::add_data - * @covers ::get_error_data */ public function test_add_data_with_data_empty_code_and_multiple_errors_with_same_code_should_store_it_under_the_first_code() { $this->wp_error->add( 'code', 'message' ); @@ -791,7 +727,6 @@ public function test_add_data_with_data_and_code_one_error_different_code_should /** * @covers ::add_data - * @covers ::get_error_data */ public function test_add_data_with_data_and_code_should_add_data() { $this->wp_error->add( 'code', 'message' ); @@ -868,8 +803,6 @@ public function test_remove_should_remove_the_error_with_the_given_code() { /** * @covers ::remove - * @covers ::get_error_data - * @covers ::get_all_error_data */ public function test_remove_should_remove_the_error_data_associated_with_the_given_code() { $this->wp_error->add( 'code', 'message', 'data' ); @@ -884,10 +817,6 @@ public function test_remove_should_remove_the_error_data_associated_with_the_giv /** * @covers ::merge_from - * @covers ::get_error_messages - * @covers ::get_error_data - * @covers ::get_all_error_data - * @covers ::get_error_message */ public function test_merge_from_should_copy_other_error_into_instance() { $this->wp_error->add( 'code1', 'message1', 'data1' ); @@ -904,7 +833,6 @@ public function test_merge_from_should_copy_other_error_into_instance() { /** * @covers ::merge_from - * @covers ::has_errors */ public function test_merge_from_with_no_errors_should_not_add_to_instance() { $other = new WP_Error(); @@ -916,10 +844,6 @@ public function test_merge_from_with_no_errors_should_not_add_to_instance() { /** * @covers ::export_to - * @covers ::get_error_messages - * @covers ::get_error_data - * @covers ::get_all_error_data - * @covers ::get_error_message */ public function test_export_to_should_copy_instance_into_other_error() { $other = new WP_Error(); @@ -938,7 +862,6 @@ public function test_export_to_should_copy_instance_into_other_error() { /** * @covers ::export_to - * @covers ::has_errors */ public function test_export_to_with_no_errors_should_not_add_to_other_error() { $other = new WP_Error(); diff --git a/tests/phpunit/tests/general/wpGetArchives.php b/tests/phpunit/tests/general/wpGetArchives.php index 5409795fd71e1..8d506a6bb2c33 100644 --- a/tests/phpunit/tests/general/wpGetArchives.php +++ b/tests/phpunit/tests/general/wpGetArchives.php @@ -16,13 +16,11 @@ public function set_up() { * @ticket 23206 */ public function test_get_archives_cache() { - global $wpdb; - self::factory()->post->create_many( 3, array( 'post_type' => 'post' ) ); wp_cache_delete( 'last_changed', 'posts' ); $this->assertFalse( wp_cache_get( 'last_changed', 'posts' ) ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -34,9 +32,9 @@ public function test_get_archives_cache() { $this->assertIsString( $result ); $time1 = wp_cache_get( 'last_changed', 'posts' ); $this->assertNotEmpty( $time1 ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -47,7 +45,7 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); // Change args, resulting in a different query string. Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -59,9 +57,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -73,9 +71,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Change type. Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -86,9 +84,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -99,7 +97,7 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); // Change type. Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -110,9 +108,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -123,7 +121,7 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); // Change type. Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -134,9 +132,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -147,7 +145,7 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); // Change type. Cache is not primed, expect 1 query. $result = wp_get_archives( @@ -158,9 +156,9 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries + 1, $wpdb->num_queries ); + $this->assertSame( $num_queries + 1, get_num_queries() ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); // Cache is primed, expect no queries. $result = wp_get_archives( @@ -171,6 +169,6 @@ public function test_get_archives_cache() { ); $this->assertIsString( $result ); $this->assertSame( $time1, wp_cache_get( 'last_changed', 'posts' ) ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); } } diff --git a/tests/phpunit/tests/general/wpGetDocumentTitle.php b/tests/phpunit/tests/general/wpGetDocumentTitle.php index 08d9e033f3f02..b12990fd5c665 100644 --- a/tests/phpunit/tests/general/wpGetDocumentTitle.php +++ b/tests/phpunit/tests/general/wpGetDocumentTitle.php @@ -60,6 +60,18 @@ public function add_title_tag_support() { public function test__wp_render_title_tag() { $this->go_to( '/' ); + $this->expectOutputString( sprintf( "%s\n", $this->blog_name ) ); + _wp_render_title_tag(); + } + + /** + * @ticket 6479 + */ + public function test__wp_render_title_tag_with_blog_description() { + $this->go_to( '/' ); + + update_option( 'blogdescription', 'A blog description' ); + $this->expectOutputString( sprintf( "%s – %s\n", $this->blog_name, get_option( 'blogdescription' ) ) ); _wp_render_title_tag(); } @@ -89,7 +101,7 @@ public function test_front_page_title() { update_option( 'show_on_front', 'page' ); update_option( 'page_on_front', - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'front-page', 'post_type' => 'page', @@ -99,12 +111,12 @@ public function test_front_page_title() { add_filter( 'document_title_parts', array( $this, 'front_page_title_parts' ) ); $this->go_to( '/' ); - $this->assertSame( sprintf( '%s – Just another WordPress site', $this->blog_name ), wp_get_document_title() ); + $this->assertSame( sprintf( '%s', $this->blog_name ), wp_get_document_title() ); update_option( 'show_on_front', 'posts' ); $this->go_to( '/' ); - $this->assertSame( sprintf( '%s – Just another WordPress site', $this->blog_name ), wp_get_document_title() ); + $this->assertSame( sprintf( '%s', $this->blog_name ), wp_get_document_title() ); } public function front_page_title_parts( $parts ) { @@ -116,7 +128,7 @@ public function front_page_title_parts( $parts ) { } public function test_home_title() { - $blog_page_id = $this->factory->post->create( + $blog_page_id = self::factory()->post->create( array( 'post_title' => 'blog-page', 'post_type' => 'page', @@ -135,7 +147,7 @@ public function test_paged_title() { add_filter( 'document_title_parts', array( $this, 'paged_title_parts' ) ); - $this->assertSame( sprintf( '%s – Page 4 – Just another WordPress site', $this->blog_name ), wp_get_document_title() ); + $this->assertSame( sprintf( '%s – Page 4', $this->blog_name ), wp_get_document_title() ); } public function paged_title_parts( $parts ) { @@ -193,7 +205,7 @@ public function test_post_type_archive_title() { ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_type' => 'cpt', ) diff --git a/tests/phpunit/tests/general/wpPreloadResources.php b/tests/phpunit/tests/general/wpPreloadResources.php new file mode 100644 index 0000000000000..778805e84015b --- /dev/null +++ b/tests/phpunit/tests/general/wpPreloadResources.php @@ -0,0 +1,262 @@ +assertSame( $expected, $actual ); + } + + /** + * Test provider for all preload link possible combinations. + * + * @return array[] + */ + public function data_preload_resources() { + return array( + 'basic_preload' => array( + 'expected' => "\n", + 'urls' => array( + array( + 'href' => 'https://example.com/style.css', + 'as' => 'style', + ), + ), + ), + 'multiple_links' => array( + 'expected' => "\n" . + "\n", + 'urls' => array( + array( + 'href' => 'https://example.com/style.css', + 'as' => 'style', + ), + array( + 'href' => 'https://example.com/main.js', + 'as' => 'script', + ), + ), + ), + 'MIME_types' => array( + 'expected' => "\n" . + "\n" . + "\n", + 'urls' => array( + array( + // Should ignore not valid attributes. + 'not' => 'valid', + 'href' => 'https://example.com/style.css', + 'as' => 'style', + ), + array( + 'href' => 'https://example.com/video.mp4', + 'as' => 'video', + 'type' => 'video/mp4', + ), + array( + 'href' => 'https://example.com/main.js', + 'as' => 'script', + ), + ), + ), + 'CORS' => array( + 'expected' => "\n" . + "\n" . + "\n" . + "\n", + 'urls' => array( + array( + 'href' => 'https://example.com/style.css', + 'as' => 'style', + 'crossorigin' => 'anonymous', + ), + array( + 'href' => 'https://example.com/video.mp4', + 'as' => 'video', + 'type' => 'video/mp4', + ), + array( + 'href' => 'https://example.com/main.js', + 'as' => 'script', + ), + array( + // Should ignore not valid attributes. + 'ignore' => 'ignore', + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin', + ), + ), + ), + 'media' => array( + 'expected' => "\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n", + 'urls' => array( + array( + 'href' => 'https://example.com/style.css', + 'as' => 'style', + 'crossorigin' => 'anonymous', + ), + array( + 'href' => 'https://example.com/video.mp4', + 'as' => 'video', + 'type' => 'video/mp4', + ), + // Duplicated href should be ignored. + array( + 'href' => 'https://example.com/video.mp4', + 'as' => 'video', + 'type' => 'video/mp4', + ), + array( + 'href' => 'https://example.com/main.js', + 'as' => 'script', + ), + array( + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin', + ), + array( + 'href' => 'https://example.com/image-narrow.png', + 'as' => 'image', + 'media' => '(max-width: 600px)', + ), + array( + 'href' => 'https://example.com/image-wide.png', + 'as' => 'image', + 'media' => '(min-width: 601px)', + ), + + ), + ), + 'media_extra_attributes' => array( + 'expected' => "\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n" . + "\n", + 'urls' => array( + array( + 'href' => 'https://example.com/style.css', + 'as' => 'style', + 'crossorigin' => 'anonymous', + ), + array( + 'href' => 'https://example.com/video.mp4', + 'as' => 'video', + 'type' => 'video/mp4', + ), + array( + 'href' => 'https://example.com/main.js', + 'as' => 'script', + ), + array( + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin', + ), + // imagesrcset only possible when using image, ignore. + array( + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w', + ), + // imagesizes only possible when using image, ignore. + array( + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'imagesizes' => '100vw', + ), + // Duplicated href should be ignored. + array( + 'href' => 'https://example.com/font.woff2', + 'as' => 'font', + 'type' => 'font/woff2', + 'crossorigin', + ), + array( + 'href' => 'https://example.com/image-640.png', + 'as' => 'image', + 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w', + 'imagesizes' => '100vw', + ), + // Omit href so that unsupporting browsers won't request a useless image. + array( + 'as' => 'image', + 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w', + 'imagesizes' => '100vw', + ), + // Duplicated imagesrcset should be ignored. + array( + 'as' => 'image', + 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w', + 'imagesizes' => '100vw', + ), + array( + 'href' => 'https://example.com/image-wide.png', + 'as' => 'image', + 'media' => '(min-width: 601px)', + ), + // No href but not imagesrcset, should be ignored. + array( + 'as' => 'image', + 'media' => '(min-width: 601px)', + ), + // imagesizes is optional. + array( + 'href' => 'https://example.com/image-800.png', + 'as' => 'image', + 'imagesrcset' => '640.png 640w, 800.png 800w, 1024.png 1024w', + ), + // imagesizes should be ignored since imagesrcset not present. + array( + 'href' => 'https://example.com/image-640.png', + 'as' => 'image', + 'imagesizes' => '100vw', + ), + ), + ), + 'fetchpriority' => array( + 'expected' => "\n", + 'resources' => array( + array( + 'href' => 'https://example.com/image.jpg', + 'as' => 'image', + 'fetchpriority' => 'high', + ), + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/general/wpRequiredFieldIndicator.php b/tests/phpunit/tests/general/wpRequiredFieldIndicator.php new file mode 100644 index 0000000000000..bd66379a2ae1b --- /dev/null +++ b/tests/phpunit/tests/general/wpRequiredFieldIndicator.php @@ -0,0 +1,45 @@ +assertSame( '*', wp_required_field_indicator() ); + } + + /** + * Tests that `wp_required_field_indicator()` applies 'wp_required_field_indicator' filters. + * + * @ticket 56389 + */ + public function test_wp_required_field_indicator_should_apply_wp_required_field_indicator_filters() { + $filter = new MockAction(); + add_filter( 'wp_required_field_indicator', array( &$filter, 'filter' ) ); + + wp_required_field_indicator(); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Tests that the final return value of `wp_required_field_indicator()` is the result of + * 'wp_required_field_indicator' filters. + * + * @ticket 56389 + */ + public function test_wp_required_field_indicator_should_return_wp_required_field_indicator_filters() { + add_filter( 'wp_required_field_indicator', '__return_empty_string' ); + $this->assertSame( '', wp_required_field_indicator() ); + } +} diff --git a/tests/phpunit/tests/general/wpRequiredFieldMessage.php b/tests/phpunit/tests/general/wpRequiredFieldMessage.php new file mode 100644 index 0000000000000..2dc1a3b8b83e4 --- /dev/null +++ b/tests/phpunit/tests/general/wpRequiredFieldMessage.php @@ -0,0 +1,48 @@ +'; + $expected .= 'Required fields are marked *'; + $expected .= ''; + $this->assertSame( $expected, wp_required_field_message() ); + } + + /** + * Tests that `wp_required_field_message()` applies 'wp_required_field_message' filters. + * + * @ticket 56389 + */ + public function test_wp_required_field_message_should_apply_wp_required_field_message_filters() { + $filter = new MockAction(); + add_filter( 'wp_required_field_message', array( &$filter, 'filter' ) ); + + wp_required_field_message(); + + $this->assertSame( 1, $filter->get_call_count() ); + } + + /** + * Tests that the final return value of `wp_required_field_message()` is the result of + * 'wp_required_field_message' filters. + * + * @ticket 56389 + */ + public function test_wp_required_field_message_should_return_wp_required_field_message_filters() { + add_filter( 'wp_required_field_message', '__return_empty_string' ); + $this->assertSame( '', wp_required_field_message() ); + } +} diff --git a/tests/phpunit/tests/general/wpResourceHints.php b/tests/phpunit/tests/general/wpResourceHints.php index 1761196b2c738..3b546dc6e254e 100644 --- a/tests/phpunit/tests/general/wpResourceHints.php +++ b/tests/phpunit/tests/general/wpResourceHints.php @@ -12,8 +12,8 @@ class Tests_General_wpResourceHints extends WP_UnitTestCase { public function set_up() { parent::set_up(); - $this->old_wp_scripts = isset( $GLOBALS['wp_scripts'] ) ? $GLOBALS['wp_scripts'] : null; - $this->old_wp_styles = isset( $GLOBALS['wp_styles'] ) ? $GLOBALS['wp_styles'] : null; + $this->old_wp_scripts = $GLOBALS['wp_scripts'] ?? null; + $this->old_wp_styles = $GLOBALS['wp_styles'] ?? null; remove_action( 'wp_default_scripts', 'wp_default_scripts' ); remove_action( 'wp_default_styles', 'wp_default_styles' ); @@ -30,17 +30,8 @@ public function tear_down() { parent::tear_down(); } - public function test_should_have_defaults_on_frontend() { - $expected = "\n"; - - $this->expectOutputString( $expected ); - - wp_resource_hints(); - } - public function test_dns_prefetching() { - $expected = "\n" . - "\n" . + $expected = "\n" . "\n" . "\n"; @@ -70,8 +61,7 @@ public function add_dns_prefetch_domains( $hints, $method ) { * @ticket 37652 */ public function test_preconnect() { - $expected = "\n" . - "\n" . + $expected = "\n" . "\n" . "\n" . "\n"; @@ -98,8 +88,7 @@ public function add_preconnect_domains( $hints, $method ) { } public function test_prerender() { - $expected = "\n" . - "\n" . + $expected = "\n" . "\n" . "\n"; @@ -124,8 +113,7 @@ public function add_prerender_urls( $hints, $method ) { } public function test_parse_url_dns_prefetch() { - $expected = "\n" . - "\n"; + $expected = "\n"; add_filter( 'wp_resource_hints', array( $this, 'add_dns_prefetch_long_urls' ), 10, 2 ); @@ -145,8 +133,7 @@ public function add_dns_prefetch_long_urls( $hints, $method ) { } public function test_dns_prefetch_styles() { - $expected = "\n" . - "\n"; + $expected = "\n"; $args = array( 'family' => 'Open+Sans:400', @@ -160,12 +147,10 @@ public function test_dns_prefetch_styles() { wp_dequeue_style( 'googlefonts' ); $this->assertSame( $expected, $actual ); - } public function test_dns_prefetch_scripts() { - $expected = "\n" . - "\n"; + $expected = "\n"; $args = array( 'family' => 'Open+Sans:400', @@ -185,7 +170,7 @@ public function test_dns_prefetch_scripts() { * @ticket 37385 */ public function test_dns_prefetch_scripts_does_not_include_registered_only() { - $expected = "\n"; + $expected = ''; $unexpected = "\n"; wp_register_script( 'jquery-elsewhere', 'https://wordpress.org/wp-includes/js/jquery/jquery.js' ); @@ -202,7 +187,7 @@ public function test_dns_prefetch_scripts_does_not_include_registered_only() { * @ticket 37502 */ public function test_deregistered_scripts_are_ignored() { - $expected = "\n"; + $expected = ''; wp_enqueue_script( 'test-script', 'http://example.org/script.js' ); wp_deregister_script( 'test-script' ); @@ -215,7 +200,7 @@ public function test_deregistered_scripts_are_ignored() { * @ticket 37652 */ public function test_malformed_urls() { - $expected = "\n"; + $expected = ''; // Errant colon. add_filter( 'wp_resource_hints', array( $this, 'add_malformed_url_errant_colon' ), 10, 2 ); @@ -250,8 +235,7 @@ public function add_malformed_url_unsupported_scheme( $hints, $method ) { * @ticket 38121 */ public function test_custom_attributes() { - $expected = "\n" . - "\n" . + $expected = "\n" . "\n" . "\n" . "\n"; diff --git a/tests/phpunit/tests/general/wpTitle.php b/tests/phpunit/tests/general/wpTitle.php new file mode 100644 index 0000000000000..272441a968671 --- /dev/null +++ b/tests/phpunit/tests/general/wpTitle.php @@ -0,0 +1,62 @@ +post->create( + array( + 'post_status' => 'publish', + 'post_title' => 'Test Post', + 'post_type' => 'post', + 'post_date' => '2021-11-01 18:52:17', + ) + ); + $this->go_to( '?m=' . $query ); + + $this->assertSame( $expected, wp_title( '»', false ) ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_title_archive() { + return array( + 'year with posts' => array( + 'query' => '2021', + 'expected' => ' » 2021', + ), + 'year without posts' => array( + 'query' => '1910', + 'expected' => ' » Page not found', + ), + 'year and month with posts' => array( + 'query' => '202111', + 'expected' => ' » 2021 » November', + ), + 'year and month without posts' => array( + 'query' => '202101', + 'expected' => ' » Page not found', + ), + 'year, month, day with posts' => array( + 'query' => '20211101', + 'expected' => ' » 2021 » November » 1', + ), + 'year, month, day without posts' => array( + 'query' => '20210101', + 'expected' => ' » Page not found', + ), + ); + } +} diff --git a/tests/phpunit/tests/hooks/addFilter.php b/tests/phpunit/tests/hooks/addFilter.php index 5890f22044fd1..de51ec9e9b04b 100644 --- a/tests/phpunit/tests/hooks/addFilter.php +++ b/tests/phpunit/tests/hooks/addFilter.php @@ -1,6 +1,5 @@ action_output = ''; + parent::tear_down(); + } + public function test_add_filter_with_function() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); - $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority ); + $function_index = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); $this->assertSame( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] ); $this->assertSame( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] ); } @@ -29,13 +46,14 @@ public function test_add_filter_with_object() { $a = new MockAction(); $callback = array( $a, 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); - $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority ); + $function_index = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); $this->assertSame( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] ); $this->assertSame( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] ); } @@ -43,13 +61,14 @@ public function test_add_filter_with_object() { public function test_add_filter_with_static_method() { $callback = array( 'MockAction', 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); - $function_index = _wp_filter_build_unique_id( $tag, $callback, $priority ); + $function_index = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); $this->assertSame( $callback, $hook->callbacks[ $priority ][ $function_index ]['function'] ); $this->assertSame( $accepted_args, $hook->callbacks[ $priority ][ $function_index ]['accepted_args'] ); } @@ -58,14 +77,15 @@ public function test_add_two_filters_with_same_priority() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); - $hook->add_filter( $tag, $callback_two, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority, $accepted_args ); $this->assertCount( 2, $hook->callbacks[ $priority ] ); } @@ -73,14 +93,16 @@ public function test_add_two_filters_with_different_priority() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); - $hook->add_filter( $tag, $callback_two, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority + 1, $accepted_args ); + $this->check_priority_exists( $hook, $priority + 1 ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] ); } @@ -88,76 +110,83 @@ public function test_add_two_filters_with_different_priority() { public function test_readd_filter() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); } public function test_readd_filter_with_different_priority() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $this->check_priority_exists( $hook, $priority ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); - $hook->add_filter( $tag, $callback, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority + 1, $accepted_args ); + $this->check_priority_exists( $hook, $priority + 1 ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] ); } public function test_sort_after_add_filter() { - $a = new MockAction(); - $b = new MockAction(); - $c = new MockAction(); - $hook = new WP_Hook(); - $tag = __FUNCTION__; + $a = new MockAction(); + $b = new MockAction(); + $c = new MockAction(); + $hook = new WP_Hook(); + $hook_name = __FUNCTION__; - $hook->add_filter( $tag, array( $a, 'action' ), 10, 1 ); - $hook->add_filter( $tag, array( $b, 'action' ), 5, 1 ); - $hook->add_filter( $tag, array( $c, 'action' ), 8, 1 ); + $hook->add_filter( $hook_name, array( $a, 'action' ), 10, 1 ); + $hook->add_filter( $hook_name, array( $b, 'action' ), 5, 1 ); + $hook->add_filter( $hook_name, array( $c, 'action' ), 8, 1 ); - $this->assertSame( array( 5, 8, 10 ), array_keys( $hook->callbacks ) ); + $this->assertSame( array( 5, 8, 10 ), $this->get_priorities( $hook ) ); } public function test_remove_and_add() { - $this->hook = new Wp_Hook(); + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add', '__return_empty_string', 10, 0 ); - + $this->check_priority_exists( $this->hook, 10 ); $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add2' ), 11, 1 ); - + $this->check_priority_exists( $this->hook, 11 ); $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add4' ), 12, 1 ); - + $this->check_priority_exists( $this->hook, 12 ); $value = $this->hook->apply_filters( '', array() ); + $this->assertSameSets( array( 10, 11, 12 ), $this->get_priorities( $this->hook ), 'The priorities should match this array' ); + $this->assertSame( '24', $value ); } public function test_remove_and_add_last_filter() { - $this->hook = new Wp_Hook(); + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add', '__return_empty_string', 10, 0 ); - + $this->check_priority_exists( $this->hook, 10 ); $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add1' ), 11, 1 ); - + $this->check_priority_exists( $this->hook, 11 ); $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add2' ), 12, 1 ); - + $this->check_priority_exists( $this->hook, 12 ); $value = $this->hook->apply_filters( '', array() ); + $this->assertSameSets( array( 10, 11, 12 ), $this->get_priorities( $this->hook ), 'The priorities should match this array' ); + $this->assertSame( '12', $value ); } public function test_remove_and_recurse_and_add() { - $this->hook = new Wp_Hook(); + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add', '__return_empty_string', 10, 0 ); @@ -167,43 +196,44 @@ public function test_remove_and_recurse_and_add() { $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add4' ), 12, 1 ); + $this->assertSameSets( array( 10, 11, 12 ), $this->get_priorities( $this->hook ), 'The priorities should match this array' ); + $value = $this->hook->apply_filters( '', array() ); $this->assertSame( '1-134-234', $value ); } - public function _filter_remove_and_add1( $string ) { - return $string . '1'; + public function _filter_remove_and_add1( $value ) { + return $value . '1'; } - public function _filter_remove_and_add2( $string ) { + public function _filter_remove_and_add2( $value ) { $this->hook->remove_filter( 'remove_and_add', array( $this, '_filter_remove_and_add2' ), 11 ); $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_add2' ), 11, 1 ); - - return $string . '2'; + $this->check_priority_exists( $this->hook, 11 ); + return $value . '2'; } - public function _filter_remove_and_recurse_and_add2( $string ) { + public function _filter_remove_and_recurse_and_add2( $value ) { $this->hook->remove_filter( 'remove_and_add', array( $this, '_filter_remove_and_recurse_and_add2' ), 11 ); - $string .= '-' . $this->hook->apply_filters( '', array() ) . '-'; + $value .= '-' . $this->hook->apply_filters( '', array() ) . '-'; $this->hook->add_filter( 'remove_and_add', array( $this, '_filter_remove_and_recurse_and_add2' ), 11, 1 ); - - return $string . '2'; + $this->check_priority_exists( $this->hook, 11 ); + return $value . '2'; } - public function _filter_remove_and_add3( $string ) { - return $string . '3'; + public function _filter_remove_and_add3( $value ) { + return $value . '3'; } - public function _filter_remove_and_add4( $string ) { - return $string . '4'; + public function _filter_remove_and_add4( $value ) { + return $value . '4'; } public function test_remove_and_add_action() { - $this->hook = new Wp_Hook(); - $this->action_output = ''; + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add_action', '__return_empty_string', 10, 0 ); @@ -217,8 +247,7 @@ public function test_remove_and_add_action() { } public function test_remove_and_add_last_action() { - $this->hook = new Wp_Hook(); - $this->action_output = ''; + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add_action', '__return_empty_string', 10, 0 ); @@ -232,8 +261,7 @@ public function test_remove_and_add_last_action() { } public function test_remove_and_recurse_and_add_action() { - $this->hook = new Wp_Hook(); - $this->action_output = ''; + $this->hook = new WP_Hook(); $this->hook->add_filter( 'remove_and_add_action', '__return_empty_string', 10, 0 ); @@ -278,4 +306,20 @@ public function _action_remove_and_add3() { public function _action_remove_and_add4() { $this->action_output .= '4'; } + + protected function check_priority_exists( $hook, $priority ) { + $priorities = $this->get_priorities( $hook ); + + $this->assertContains( $priority, $priorities ); + } + + protected function get_priorities( $hook ) { + $reflection = new ReflectionClass( $hook ); + $reflection_property = $reflection->getProperty( 'priorities' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_property->setAccessible( true ); + } + + return $reflection_property->getValue( $hook ); + } } diff --git a/tests/phpunit/tests/hooks/applyFilters.php b/tests/phpunit/tests/hooks/applyFilters.php index fcb8e3b126edb..50c35e3498a96 100644 --- a/tests/phpunit/tests/hooks/applyFilters.php +++ b/tests/phpunit/tests/hooks/applyFilters.php @@ -12,12 +12,12 @@ public function test_apply_filters_with_callback() { $a = new MockAction(); $callback = array( $a, 'filter' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $returned = $hook->apply_filters( $arg, array( $arg ) ); @@ -29,12 +29,12 @@ public function test_apply_filters_with_multiple_calls() { $a = new MockAction(); $callback = array( $a, 'filter' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $returned_one = $hook->apply_filters( $arg, array( $arg ) ); $returned_two = $hook->apply_filters( $returned_one, array( $returned_one ) ); @@ -43,4 +43,131 @@ public function test_apply_filters_with_multiple_calls() { $this->assertSame( 2, $a->get_call_count() ); } + /** + * @ticket 60193 + * + * @dataProvider data_priority_callback_order_with_integers + * @dataProvider data_priority_callback_order_with_unhappy_path_nonintegers + * + * @param array $priorities { + * Indexed array of the priorities for the MockAction callbacks. + * + * @type mixed $0 Priority for 'action' callback. + * @type mixed $1 Priority for 'action2' callback. + * } + * @param array $expected_call_order An array of callback names in expected call order. + * @param string $expected_deprecation Optional. Deprecation message. Default ''. + */ + public function test_priority_callback_order( $priorities, $expected_call_order, $expected_deprecation = '' ) { + $mock = new MockAction(); + $hook = new WP_Hook(); + $hook_name = __FUNCTION__; + + if ( $expected_deprecation && PHP_VERSION_ID >= 80100 ) { + $this->expectDeprecation(); + $this->expectDeprecationMessage( $expected_deprecation ); + } + + $hook->add_filter( $hook_name, array( $mock, 'filter' ), $priorities[0], 1 ); + $hook->add_filter( $hook_name, array( $mock, 'filter2' ), $priorities[1], 1 ); + $hook->apply_filters( __FUNCTION__ . '_val', array( '' ) ); + + $this->assertSame( 2, $mock->get_call_count(), 'The number of call counts does not match' ); + + $actual_call_order = wp_list_pluck( $mock->get_events(), 'filter' ); + $this->assertSame( $expected_call_order, $actual_call_order, 'The filter callback order does not match the expected order' ); + } + + /** + * Happy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_integers() { + return array( + 'int DESC' => array( + 'priorities' => array( 10, 9 ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'int ASC' => array( + 'priorities' => array( 9, 10 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + ); + } + + /** + * Unhappy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_unhappy_path_nonintegers() { + return array( + // Numbers as strings and floats. + 'int as string DESC' => array( + 'priorities' => array( '10', '9' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'int as string ASC' => array( + 'priorities' => array( '9', '10' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'float DESC' => array( + 'priorities' => array( 10.0, 9.5 ), + 'expected_call_order' => array( 'filter2', 'filter' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float ASC' => array( + 'priorities' => array( 9.5, 10.0 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float as string DESC' => array( + 'priorities' => array( '10.0', '9.5' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'float as string ASC' => array( + 'priorities' => array( '9.5', '10.0' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + + // Non-numeric. + 'null' => array( + 'priorities' => array( null, null ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'bool DESC' => array( + 'priorities' => array( true, false ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'bool ASC' => array( + 'priorities' => array( false, true ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'non-numerical string DESC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'non-numerical string ASC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'int, non-numerical string DESC' => array( + 'priorities' => array( 10, 'test' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'int, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + 'float, non-numerical string DESC' => array( + 'priorities' => array( 10.0, 'test' ), + 'expected_call_order' => array( 'filter2', 'filter' ), + ), + 'float, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10.0 ), + 'expected_call_order' => array( 'filter', 'filter2' ), + ), + ); + } } diff --git a/tests/phpunit/tests/hooks/doAction.php b/tests/phpunit/tests/hooks/doAction.php index 0fb5652f3ba65..c9767f865df26 100644 --- a/tests/phpunit/tests/hooks/doAction.php +++ b/tests/phpunit/tests/hooks/doAction.php @@ -20,12 +20,12 @@ public function test_do_action_with_callback() { $a = new MockAction(); $callback = array( $a, 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertSame( 1, $a->get_call_count() ); @@ -35,12 +35,12 @@ public function test_do_action_with_multiple_calls() { $a = new MockAction(); $callback = array( $a, 'filter' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $hook->do_action( array( $arg ) ); @@ -53,13 +53,13 @@ public function test_do_action_with_multiple_callbacks_on_same_priority() { $callback_one = array( $a, 'filter' ); $callback_two = array( $b, 'filter' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertSame( 1, $a->get_call_count() ); @@ -72,28 +72,156 @@ public function test_do_action_with_multiple_callbacks_on_different_priorities() $callback_one = array( $a, 'filter' ); $callback_two = array( $b, 'filter' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority + 1, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertSame( 1, $a->get_call_count() ); $this->assertSame( 1, $a->get_call_count() ); } + /** + * @ticket 60193 + * + * @dataProvider data_priority_callback_order_with_integers + * @dataProvider data_priority_callback_order_with_unhappy_path_nonintegers + * + * @param array $priorities { + * Indexed array of the priorities for the MockAction callbacks. + * + * @type mixed $0 Priority for 'action' callback. + * @type mixed $1 Priority for 'action2' callback. + * } + * @param array $expected_call_order An array of callback names in expected call order. + * @param string $expected_deprecation Optional. Deprecation message. Default ''. + */ + public function test_priority_callback_order( $priorities, $expected_call_order, $expected_deprecation = '' ) { + $mock = new MockAction(); + $hook = new WP_Hook(); + $hook_name = __FUNCTION__; + + if ( $expected_deprecation && PHP_VERSION_ID >= 80100 ) { + $this->expectDeprecation(); + $this->expectDeprecationMessage( $expected_deprecation ); + } + + $hook->add_filter( $hook_name, array( $mock, 'action' ), $priorities[0], 1 ); + $hook->add_filter( $hook_name, array( $mock, 'action2' ), $priorities[1], 1 ); + $hook->do_action( array( '' ) ); + + $this->assertSame( 2, $mock->get_call_count(), 'The number of call counts does not match' ); + + $actual_call_order = wp_list_pluck( $mock->get_events(), 'action' ); + $this->assertSame( $expected_call_order, $actual_call_order, 'The action callback order does not match the expected order' ); + } + + /** + * Happy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_integers() { + return array( + 'int DESC' => array( + 'priorities' => array( 10, 9 ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'int ASC' => array( + 'priorities' => array( 9, 10 ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + ); + } + + /** + * Unhappy path data provider. + * + * @return array[] + */ + public function data_priority_callback_order_with_unhappy_path_nonintegers() { + return array( + // Numbers as strings and floats. + 'int as string DESC' => array( + 'priorities' => array( '10', '9' ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'int as string ASC' => array( + 'priorities' => array( '9', '10' ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'float DESC' => array( + 'priorities' => array( 10.0, 9.5 ), + 'expected_call_order' => array( 'action2', 'action' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float ASC' => array( + 'priorities' => array( 9.5, 10.0 ), + 'expected_call_order' => array( 'action', 'action2' ), + 'expected_deprecation' => 'Implicit conversion from float 9.5 to int loses precision', + ), + 'float as string DESC' => array( + 'priorities' => array( '10.0', '9.5' ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'float as string ASC' => array( + 'priorities' => array( '9.5', '10.0' ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + + // Non-numeric. + 'null' => array( + 'priorities' => array( null, null ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'bool DESC' => array( + 'priorities' => array( true, false ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'bool ASC' => array( + 'priorities' => array( false, true ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'non-numerical string DESC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'non-numerical string ASC' => array( + 'priorities' => array( 'test1', 'test2' ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'int, non-numerical string DESC' => array( + 'priorities' => array( 10, 'test' ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'int, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10 ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + 'float, non-numerical string DESC' => array( + 'priorities' => array( 10.0, 'test' ), + 'expected_call_order' => array( 'action2', 'action' ), + ), + 'float, non-numerical string ASC' => array( + 'priorities' => array( 'test', 10.0 ), + 'expected_call_order' => array( 'action', 'action2' ), + ), + ); + } + public function test_do_action_with_no_accepted_args() { $callback = array( $this, '_action_callback' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; $accepted_args = 0; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertEmpty( $this->events[0]['args'] ); @@ -102,12 +230,12 @@ public function test_do_action_with_no_accepted_args() { public function test_do_action_with_one_accepted_arg() { $callback = array( $this, '_action_callback' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; $accepted_args = 1; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertCount( 1, $this->events[0]['args'] ); @@ -116,12 +244,12 @@ public function test_do_action_with_one_accepted_arg() { public function test_do_action_with_more_accepted_args() { $callback = array( $this, '_action_callback' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 100; $accepted_args = 1000; $arg = __FUNCTION__ . '_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->do_action( array( $arg ) ); $this->assertCount( 1, $this->events[0]['args'] ); diff --git a/tests/phpunit/tests/hooks/doAllHook.php b/tests/phpunit/tests/hooks/doAllHook.php index 29eec96428f25..bec1caa0e1df3 100644 --- a/tests/phpunit/tests/hooks/doAllHook.php +++ b/tests/phpunit/tests/hooks/doAllHook.php @@ -12,12 +12,12 @@ public function test_do_all_hook_with_multiple_calls() { $a = new MockAction(); $callback = array( $a, 'action' ); $hook = new WP_Hook(); - $tag = 'all'; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = 'all'; + $priority = 1; + $accepted_args = 2; $arg = 'all_arg'; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $args = array( $arg ); $hook->do_all_hook( $args ); $hook->do_all_hook( $args ); diff --git a/tests/phpunit/tests/hooks/hasFilter.php b/tests/phpunit/tests/hooks/hasFilter.php index f9d48b3d4eb55..ce4f2e9fb57ec 100644 --- a/tests/phpunit/tests/hooks/hasFilter.php +++ b/tests/phpunit/tests/hooks/hasFilter.php @@ -8,51 +8,60 @@ */ class Tests_Hooks_HasFilter extends WP_UnitTestCase { + /** + * @ticket 64186 + */ public function test_has_filter_with_function() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); - - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - - $this->assertSame( $priority, $hook->has_filter( $tag, $callback ) ); + $hook_name = __FUNCTION__; + $priority_a = 1; + $priority_b = 10; + $accepted_args = 2; + + $hook->add_filter( $hook_name, $callback, $priority_a, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority_b, $accepted_args ); + + $this->assertSame( $priority_a, $hook->has_filter( $hook_name, $callback ) ); + $this->assertTrue( $hook->has_filter( $hook_name, $callback, $priority_a ) ); + $this->assertTrue( $hook->has_filter( $hook_name, $callback, $priority_b ) ); + $hook->remove_filter( $hook_name, $callback, $priority_a ); + $this->assertSame( $priority_b, $hook->has_filter( $hook_name, $callback ) ); } public function test_has_filter_with_object() { $a = new MockAction(); $callback = array( $a, 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); - $this->assertSame( $priority, $hook->has_filter( $tag, $callback ) ); + $this->assertSame( $priority, $hook->has_filter( $hook_name, $callback ) ); } public function test_has_filter_with_static_method() { $callback = array( 'MockAction', 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); - $this->assertSame( $priority, $hook->has_filter( $tag, $callback ) ); + $this->assertSame( $priority, $hook->has_filter( $hook_name, $callback ) ); } public function test_has_filter_without_callback() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $this->assertTrue( $hook->has_filter() ); } @@ -63,22 +72,22 @@ public function test_not_has_filter_without_callback() { } public function test_not_has_filter_with_callback() { - $callback = '__return_null'; - $hook = new WP_Hook(); - $tag = __FUNCTION__; + $callback = '__return_null'; + $hook = new WP_Hook(); + $hook_name = __FUNCTION__; - $this->assertFalse( $hook->has_filter( $tag, $callback ) ); + $this->assertFalse( $hook->has_filter( $hook_name, $callback ) ); } public function test_has_filter_with_wrong_callback() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); - $this->assertFalse( $hook->has_filter( $tag, '__return_false' ) ); + $this->assertFalse( $hook->has_filter( $hook_name, '__return_false' ) ); } } diff --git a/tests/phpunit/tests/hooks/hasFilters.php b/tests/phpunit/tests/hooks/hasFilters.php index f5c1e657fc4a4..3cab1f6f47ebf 100644 --- a/tests/phpunit/tests/hooks/hasFilters.php +++ b/tests/phpunit/tests/hooks/hasFilters.php @@ -11,11 +11,11 @@ class Tests_Hooks_HasFilters extends WP_UnitTestCase { public function test_has_filters_with_callback() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $this->assertTrue( $hook->has_filters() ); } @@ -28,24 +28,24 @@ public function test_has_filters_without_callback() { public function test_not_has_filters_with_removed_callback() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - $hook->remove_filter( $tag, $callback, $priority ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $hook->remove_filter( $hook_name, $callback, $priority ); $this->assertFalse( $hook->has_filters() ); } public function test_not_has_filter_with_directly_removed_callback() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - $function_key = _wp_filter_build_unique_id( $tag, $callback, $priority ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $function_key = _wp_filter_build_unique_id( $hook_name, $callback, $priority ); unset( $hook->callbacks[ $priority ][ $function_key ] ); $this->assertFalse( $hook->has_filters() ); diff --git a/tests/phpunit/tests/hooks/iterator.php b/tests/phpunit/tests/hooks/iterator.php index 4840aa3d4df2b..cf91027e1f1b8 100644 --- a/tests/phpunit/tests/hooks/iterator.php +++ b/tests/phpunit/tests/hooks/iterator.php @@ -12,12 +12,12 @@ public function test_foreach() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority + 1, $accepted_args ); $functions = array(); $priorities = array(); diff --git a/tests/phpunit/tests/hooks/preinitHooks.php b/tests/phpunit/tests/hooks/preinitHooks.php index 551d64400f417..06cd5550c6cda 100644 --- a/tests/phpunit/tests/hooks/preinitHooks.php +++ b/tests/phpunit/tests/hooks/preinitHooks.php @@ -9,12 +9,12 @@ class Tests_Hooks_PreinitHooks extends WP_UnitTestCase { public function test_array_to_hooks() { - $tag1 = __FUNCTION__ . '_1'; - $priority1 = rand( 1, 100 ); - $tag2 = __FUNCTION__ . '_2'; - $priority2 = rand( 1, 100 ); - $filters = array( - $tag1 => array( + $hook_name1 = __FUNCTION__ . '_1'; + $priority1 = 1; + $hook_name2 = __FUNCTION__ . '_2'; + $priority2 = 2; + $filters = array( + $hook_name1 => array( $priority1 => array( 'test1' => array( 'function' => '__return_false', @@ -22,7 +22,7 @@ public function test_array_to_hooks() { ), ), ), - $tag2 => array( + $hook_name2 => array( $priority2 => array( 'test1' => array( 'function' => '__return_null', @@ -34,7 +34,7 @@ public function test_array_to_hooks() { $hooks = WP_Hook::build_preinitialized_hooks( $filters ); - $this->assertSame( $priority1, $hooks[ $tag1 ]->has_filter( $tag1, '__return_false' ) ); - $this->assertSame( $priority2, $hooks[ $tag2 ]->has_filter( $tag2, '__return_null' ) ); + $this->assertSame( $priority1, $hooks[ $hook_name1 ]->has_filter( $hook_name1, '__return_false' ) ); + $this->assertSame( $priority2, $hooks[ $hook_name2 ]->has_filter( $hook_name2, '__return_null' ) ); } } diff --git a/tests/phpunit/tests/hooks/removeAllFilters.php b/tests/phpunit/tests/hooks/removeAllFilters.php index cfaf81f4c8ad8..23a6cc4013e98 100644 --- a/tests/phpunit/tests/hooks/removeAllFilters.php +++ b/tests/phpunit/tests/hooks/removeAllFilters.php @@ -11,13 +11,14 @@ class Tests_Hooks_RemoveAllFilters extends WP_UnitTestCase { public function test_remove_all_filters() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); $hook->remove_all_filters(); + $this->check_priority_non_existent( $hook, $priority ); $this->assertFalse( $hook->has_filters() ); } @@ -26,17 +27,40 @@ public function test_remove_all_filters_with_priority() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority + 1, $accepted_args ); $hook->remove_all_filters( $priority ); + $this->check_priority_non_existent( $hook, $priority ); - $this->assertFalse( $hook->has_filter( $tag, $callback_one ) ); + $this->assertFalse( $hook->has_filter( $hook_name, $callback_one ) ); $this->assertTrue( $hook->has_filters() ); - $this->assertSame( $priority + 1, $hook->has_filter( $tag, $callback_two ) ); + $this->assertSame( $priority + 1, $hook->has_filter( $hook_name, $callback_two ) ); + $this->check_priority_exists( $hook, $priority + 1 ); + } + + protected function check_priority_non_existent( $hook, $priority ) { + $priorities = $this->get_priorities( $hook ); + + $this->assertNotContains( $priority, $priorities ); + } + + protected function check_priority_exists( $hook, $priority ) { + $priorities = $this->get_priorities( $hook ); + + $this->assertContains( $priority, $priorities ); + } + protected function get_priorities( $hook ) { + $reflection = new ReflectionClass( $hook ); + $reflection_property = $reflection->getProperty( 'priorities' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_property->setAccessible( true ); + } + + return $reflection_property->getValue( $hook ); } } diff --git a/tests/phpunit/tests/hooks/removeFilter.php b/tests/phpunit/tests/hooks/removeFilter.php index 793cd02b3cf5d..d3065bb9bd425 100644 --- a/tests/phpunit/tests/hooks/removeFilter.php +++ b/tests/phpunit/tests/hooks/removeFilter.php @@ -11,12 +11,13 @@ class Tests_Hooks_RemoveFilter extends WP_UnitTestCase { public function test_remove_filter_with_function() { $callback = '__return_null'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - $hook->remove_filter( $tag, $callback, $priority ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $hook->remove_filter( $hook_name, $callback, $priority ); + $this->check_priority_non_existent( $hook, $priority ); $this->assertArrayNotHasKey( $priority, $hook->callbacks ); } @@ -25,12 +26,13 @@ public function test_remove_filter_with_object() { $a = new MockAction(); $callback = array( $a, 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - $hook->remove_filter( $tag, $callback, $priority ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $hook->remove_filter( $hook_name, $callback, $priority ); + $this->check_priority_non_existent( $hook, $priority ); $this->assertArrayNotHasKey( $priority, $hook->callbacks ); } @@ -38,12 +40,13 @@ public function test_remove_filter_with_object() { public function test_remove_filter_with_static_method() { $callback = array( 'MockAction', 'action' ); $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback, $priority, $accepted_args ); - $hook->remove_filter( $tag, $callback, $priority ); + $hook->add_filter( $hook_name, $callback, $priority, $accepted_args ); + $hook->remove_filter( $hook_name, $callback, $priority ); + $this->check_priority_non_existent( $hook, $priority ); $this->assertArrayNotHasKey( $priority, $hook->callbacks ); } @@ -52,31 +55,56 @@ public function test_remove_filters_with_another_at_same_priority() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority, $accepted_args ); - $hook->remove_filter( $tag, $callback_one, $priority ); + $hook->remove_filter( $hook_name, $callback_one, $priority ); $this->assertCount( 1, $hook->callbacks[ $priority ] ); + $this->check_priority_exists( $hook, $priority, 'Has priority of 2' ); } public function test_remove_filter_with_another_at_different_priority() { $callback_one = '__return_null'; $callback_two = '__return_false'; $hook = new WP_Hook(); - $tag = __FUNCTION__; - $priority = rand( 1, 100 ); - $accepted_args = rand( 1, 100 ); + $hook_name = __FUNCTION__; + $priority = 1; + $accepted_args = 2; - $hook->add_filter( $tag, $callback_one, $priority, $accepted_args ); - $hook->add_filter( $tag, $callback_two, $priority + 1, $accepted_args ); + $hook->add_filter( $hook_name, $callback_one, $priority, $accepted_args ); + $hook->add_filter( $hook_name, $callback_two, $priority + 1, $accepted_args ); - $hook->remove_filter( $tag, $callback_one, $priority ); + $hook->remove_filter( $hook_name, $callback_one, $priority ); + $this->check_priority_non_existent( $hook, $priority ); $this->assertArrayNotHasKey( $priority, $hook->callbacks ); $this->assertCount( 1, $hook->callbacks[ $priority + 1 ] ); + $this->check_priority_exists( $hook, $priority + 1, 'Should priority of 3' ); + } + + protected function check_priority_non_existent( $hook, $priority ) { + $priorities = $this->get_priorities( $hook ); + + $this->assertNotContains( $priority, $priorities ); + } + + protected function check_priority_exists( $hook, $priority ) { + $priorities = $this->get_priorities( $hook ); + + $this->assertContains( $priority, $priorities ); + } + + protected function get_priorities( $hook ) { + $reflection = new ReflectionClass( $hook ); + $reflection_property = $reflection->getProperty( 'priorities' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_property->setAccessible( true ); + } + + return $reflection_property->getValue( $hook ); } } diff --git a/tests/phpunit/tests/html-api/wpHtmlDecoder.php b/tests/phpunit/tests/html-api/wpHtmlDecoder.php new file mode 100644 index 0000000000000..82d6a10d349db --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlDecoder.php @@ -0,0 +1,141 @@ +assertSame( + $decoded_value, + WP_HTML_Decoder::decode_text_node( $raw_text_node ), + 'Improperly decoded raw text node.' + ); + } + + public static function data_edge_cases() { + return array( + 'Single ampersand' => array( '&', '&' ), + ); + } + + /** + * Ensures proper detection of attribute prefixes ignoring ASCII case. + * + * @ticket 61072 + * + * @dataProvider data_case_variants_of_attribute_prefixes + * + * @param string $attribute_value Raw attribute value from HTML string. + * @param string $search_string Prefix contained in encoded attribute value. + */ + public function test_detects_ascii_case_insensitive_attribute_prefixes( $attribute_value, $search_string ) { + $this->assertTrue( + WP_HTML_Decoder::attribute_starts_with( $attribute_value, $search_string, 'ascii-case-insensitive' ), + "Should have found that '{$attribute_value}' starts with '{$search_string}'" + ); + } + + /** + * Data provider. + * + * @return Generator. + */ + public static function data_case_variants_of_attribute_prefixes() { + $with_javascript_prefix = array( + 'javascript:', + 'JAVASCRIPT:', + 'javascript:', + 'javascript:', + 'javascript:', + 'javascript:', + 'javascript:alert(1)', + 'JaVaScRiPt:alert(1)', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert(1);', + 'javascript:alert('XSS')', + 'javascript:javascript:alert(1);', + 'javascript:javascript:alert(1);', + 'javascript:javascript:alert(1);', + 'javascript:javascript:alert(1);', + 'javascript:javascript:alert(1);', + 'javascript:alert(1)//?:', + 'javascript:alert(1)', + 'javascript:x=1;alert(1)', + ); + + foreach ( $with_javascript_prefix as $attribute_value ) { + yield $attribute_value => array( $attribute_value, 'javascript:' ); + } + } + + /** + * Ensures that `attribute_starts_with` respects the case sensitivity argument. + * + * @ticket 61072 + * + * @dataProvider data_attributes_with_prefix_and_case_sensitive_match + * + * @param string $attribute_value Raw attribute value from HTML string. + * @param string $search_string Prefix contained or not contained in encoded attribute value. + * @param string $case_sensitivity Whether to search with ASCII case sensitivity; + * 'ascii-case-insensitive' or 'case-sensitive'. + * @param bool $is_match Whether the search string is a prefix for the attribute value, + * given the case sensitivity setting. + */ + public function test_attribute_starts_with_heeds_case_sensitivity( $attribute_value, $search_string, $case_sensitivity, $is_match ) { + if ( $is_match ) { + $this->assertTrue( + WP_HTML_Decoder::attribute_starts_with( $attribute_value, $search_string, $case_sensitivity ), + 'Should have found attribute prefix with case-sensitive search.' + ); + } else { + $this->assertFalse( + WP_HTML_Decoder::attribute_starts_with( $attribute_value, $search_string, $case_sensitivity ), + 'Should not have matched attribute with prefix with ASCII-case-insensitive search.' + ); + } + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_attributes_with_prefix_and_case_sensitive_match() { + return array( + array( 'http://wordpress.org', 'http', 'case-sensitive', true ), + array( 'http://wordpress.org', 'http', 'ascii-case-insensitive', true ), + array( 'http://wordpress.org', 'HTTP', 'case-sensitive', false ), + array( 'http://wordpress.org', 'HTTP', 'ascii-case-insensitive', true ), + array( 'http://wordpress.org', 'Http', 'case-sensitive', false ), + array( 'http://wordpress.org', 'Http', 'ascii-case-insensitive', true ), + array( 'http://wordpress.org', 'https', 'case-sensitive', false ), + array( 'http://wordpress.org', 'https', 'ascii-case-insensitive', false ), + ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php b/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php new file mode 100644 index 0000000000000..40400984e5cff --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlDoctypeInfo.php @@ -0,0 +1,118 @@ +assertNotNull( + $doctype, + "Should have parsed the following doctype declaration: {$html}" + ); + + $this->assertSame( + $expected_compat_mode, + $doctype->indicated_compatibility_mode, + 'Failed to infer the expected document compatibility mode.' + ); + + $this->assertSame( + $expected_name, + $doctype->name, + 'Failed to parse the expected DOCTYPE name.' + ); + + $this->assertSame( + $expected_public_id, + $doctype->public_identifier, + 'Failed to parse the expected DOCTYPE public identifier.' + ); + + $this->assertSame( + $expected_system_id, + $doctype->system_identifier, + 'Failed to parse the expected DOCTYPE system identifier.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_parseable_raw_doctypes(): array { + return array( + 'Missing doctype name' => array( '', 'quirks' ), + 'HTML5 doctype' => array( '', 'no-quirks', 'html' ), + 'HTML5 doctype no whitespace before name' => array( '', 'no-quirks', 'html' ), + 'XHTML doctype' => array( '', 'no-quirks', 'html', '-//W3C//DTD HTML 4.01//EN', 'http://www.w3.org/TR/html4/strict.dtd' ), + 'SVG doctype' => array( '', 'quirks', 'svg', '-//W3C//DTD SVG 1.1//EN', 'http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd' ), + 'MathML doctype' => array( '', 'quirks', 'math', '-//W3C//DTD MathML 2.0//EN', 'http://www.w3.org/Math/DTD/mathml2/mathml2.dtd' ), + 'Doctype with null byte replacement' => array( "", 'quirks', "null-\u{FFFD}", "\u{FFFD}", "\u{FFFD}\u{FFFD}" ), + 'Uppercase doctype' => array( '', 'quirks', 'uppercase' ), + 'Lowercase doctype' => array( '', 'quirks', 'lowercase' ), + 'Doctype with whitespace' => array( "", 'no-quirks', 'html', '', '' ), + 'Doctype trailing characters' => array( "", 'no-quirks', 'html', '', '' ), + 'An ugly no-quirks doctype' => array( "", 'no-quirks', 'html', 'pub-id', 'sysid' ), + 'Missing public ID' => array( '', 'quirks', 'html' ), + 'Missing system ID' => array( '', 'quirks', 'html' ), + 'Missing close quote public ID' => array( "", 'quirks', 'html', 'xyz' ), + 'Missing close quote system ID' => array( "", 'quirks', 'html', null, 'xyz' ), + 'Missing close quote system ID with public' => array( "", 'quirks', 'html', 'abc', 'xyz' ), + 'Bogus characters instead of system/public' => array( '', 'quirks', 'html' ), + 'Bogus characters instead of PUBLIC quote' => array( "", 'quirks', 'html' ), + 'Bogus characters instead of SYSTEM quote ' => array( "", 'quirks', 'html' ), + 'Emoji' => array( '', 'quirks', "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}", '🔥', '😈' ), + 'Bogus characters instead of SYSTEM quote after public' => array( "", 'quirks', 'html', '' ), + 'Special quirks mode if system unset' => array( '', 'quirks', 'html', '-//W3C//DTD HTML 4.01 Frameset//' ), + 'Special limited-quirks mode if system set' => array( '', 'limited-quirks', 'html', '-//W3C//DTD HTML 4.01 Frameset//', '' ), + ); + } + + /** + * @dataProvider invalid_inputs + * + * @ticket 61576 + */ + public function test_invalid_inputs_return_null( string $html ) { + $this->assertNull( WP_HTML_Doctype_Info::from_doctype_token( $html ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function invalid_inputs(): array { + return array( + 'Empty string' => array( '' ), + 'Other HTML' => array( '
      ' ), + 'DOCTYPE after HTML' => array( 'x' ), + 'DOCTYPE before HTML' => array( 'x' ), + 'Incomplete DOCTYPE' => array( '"' => array( '">' ), + ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php new file mode 100644 index 0000000000000..094fea367878a --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-bookmark.php @@ -0,0 +1,173 @@ +' ); + $this->assertTrue( $processor->next_tag( 'DIV' ) ); + $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark.' ); + $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed has_bookmark check.' ); + + // Confirm the bookmark works and processing continues normally. + $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark.' ); + $this->assertSame( 'DIV', $processor->get_tag() ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs() ); + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SPAN', $processor->get_tag() ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV', 'SPAN' ), $processor->get_breadcrumbs() ); + } + + /** + * @dataProvider data_processor_constructors + * + * @ticket 62290 + */ + public function test_processor_seek_backward( callable $factory ) { + $processor = $factory( '
      ' ); + $this->assertTrue( $processor->next_tag( 'DIV' ) ); + $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark.' ); + $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed has_bookmark check.' ); + + // Move past the bookmark so it must scan backwards. + $this->assertTrue( $processor->next_tag( 'SPAN' ) ); + + // Confirm the bookmark works. + $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark.' ); + $this->assertSame( 'DIV', $processor->get_tag() ); + } + + /** + * @dataProvider data_processor_constructors + * + * @ticket 62290 + */ + public function test_processor_seek_forward( callable $factory ) { + $processor = $factory( '
      ' ); + $this->assertTrue( $processor->next_tag( 'DIV' ) ); + $this->assertTrue( $processor->set_bookmark( 'one' ), 'Failed to set bookmark "one".' ); + $this->assertTrue( $processor->has_bookmark( 'one' ), 'Failed "one" has_bookmark check.' ); + + // Move past the bookmark so it must scan backwards. + $this->assertTrue( $processor->next_tag( 'SPAN' ) ); + $this->assertTrue( $processor->get_attribute( 'two' ) ); + $this->assertTrue( $processor->set_bookmark( 'two' ), 'Failed to set bookmark "two".' ); + $this->assertTrue( $processor->has_bookmark( 'two' ), 'Failed "two" has_bookmark check.' ); + + // Seek back. + $this->assertTrue( $processor->seek( 'one' ), 'Failed to seek to bookmark "one".' ); + $this->assertSame( 'DIV', $processor->get_tag() ); + + // Seek forward and continue processing. + $this->assertTrue( $processor->seek( 'two' ), 'Failed to seek to bookmark "two".' ); + $this->assertSame( 'SPAN', $processor->get_tag() ); + $this->assertTrue( $processor->get_attribute( 'two' ) ); + + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'A', $processor->get_tag() ); + $this->assertTrue( $processor->get_attribute( 'three' ) ); + } + + /** + * Ensure the parsing namespace is handled when seeking from foreign content. + * + * @dataProvider data_processor_constructors + * + * @ticket 62290 + */ + public function test_seek_back_from_foreign_content( callable $factory ) { + $processor = $factory( '' ); + $this->assertTrue( $processor->next_tag( 'CUSTOM-ELEMENT' ) ); + $this->assertTrue( $processor->set_bookmark( 'mark' ), 'Failed to set bookmark "mark".' ); + $this->assertTrue( $processor->has_bookmark( 'mark' ), 'Failed "mark" has_bookmark check.' ); + + /* + * has self-closing flag, but HTML elements (that are not void elements) cannot self-close, + * they must be closed by some means, usually a closing tag. + * + * If the div were interpreted as foreign content, it would self-close. + */ + $this->assertTrue( $processor->has_self_closing_flag() ); + $this->assertTrue( $processor->expects_closer(), 'Incorrectly interpreted HTML custom-element with self-closing flag as self-closing element.' ); + + // Proceed into foreign content. + $this->assertTrue( $processor->next_tag( 'RECT' ) ); + $this->assertSame( 'svg', $processor->get_namespace() ); + $this->assertTrue( $processor->has_self_closing_flag() ); + $this->assertFalse( $processor->expects_closer() ); + $this->assertSame( array( 'HTML', 'BODY', 'CUSTOM-ELEMENT', 'SVG', 'RECT' ), $processor->get_breadcrumbs() ); + + // Seek back. + $this->assertTrue( $processor->seek( 'mark' ), 'Failed to seek to bookmark "mark".' ); + $this->assertSame( 'CUSTOM-ELEMENT', $processor->get_tag() ); + // If the parsing namespace were not correct here (html), + // then the self-closing flag would be misinterpreted. + $this->assertTrue( $processor->has_self_closing_flag() ); + $this->assertTrue( $processor->expects_closer(), 'Incorrectly interpreted HTML custom-element with self-closing flag as self-closing element.' ); + + // Proceed into foreign content again. + $this->assertTrue( $processor->next_tag( 'RECT' ) ); + $this->assertSame( 'svg', $processor->get_namespace() ); + $this->assertTrue( $processor->has_self_closing_flag() ); + $this->assertFalse( $processor->expects_closer() ); + + // The RECT should still descend from the CUSTOM-ELEMENT despite its self-closing flag. + $this->assertSame( array( 'HTML', 'BODY', 'CUSTOM-ELEMENT', 'SVG', 'RECT' ), $processor->get_breadcrumbs() ); + } + + /** + * Covers a regression where the root node may not be present on the stack of open elements. + * + * Heading elements (h1, h2, etc.) check the current node on the stack of open elements + * and expect it to be defined. If the root-node has been popped, pushing a new heading + * onto the stack will create a warning and fail the test. + * + * @ticket 62290 + */ + public function test_fragment_starts_with_h1() { + $processor = WP_HTML_Processor::create_fragment( '

      ' ); + $this->assertTrue( $processor->next_tag( 'H1' ) ); + $this->assertTrue( $processor->set_bookmark( 'mark' ) ); + $this->assertTrue( $processor->next_token() ); + $this->assertTrue( $processor->seek( 'mark' ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_processor_constructors(): array { + return array( + 'Full parser' => array( array( WP_HTML_Processor::class, 'create_full_parser' ) ), + 'Fragment parser' => array( array( WP_HTML_Processor::class, 'create_fragment' ) ), + ); + } + + /** + * @ticket 62521 + * + * @expectedIncorrectUsage WP_HTML_Processor::set_bookmark + */ + public function test_bookmarks_not_allowed_on_virtual_nodes() { + $processor = WP_HTML_Processor::create_full_parser( 'text' ); + $this->assertTrue( $processor->next_tag( 'BODY' ) ); + $this->assertFalse( $processor->set_bookmark( 'mark' ) ); + $this->assertTrue( $processor->next_token() ); + $this->assertTrue( $processor->set_bookmark( 'mark' ) ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php new file mode 100644 index 0000000000000..175bb3845d554 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor-serialize.php @@ -0,0 +1,392 @@ +assertSame( + WP_HTML_Processor::normalize( "apples > or\x00anges" ), + 'apples > oranges', + 'Should have returned an HTML string with applicable characters properly encoded.' + ); + } + + /** + * Ensures that unclosed elements are explicitly closed to ensure proper HTML isolation. + * + * When thinking about embedding HTML fragments into others, it's important that unclosed + * elements aren't left dangling, otherwise a snippet of HTML may "swallow" parts of the + * document that follow it. + * + * @ticket 62036 + */ + public function test_closes_unclosed_elements_at_end() { + $this->assertSame( + WP_HTML_Processor::normalize( '
      ' ), + '
      ', + 'Should have provided the explicit closer to the un-closed DIV element.' + ); + } + + /** + * Ensures that boolean attributes remain boolean and do not gain values. + * + * @ticket 62036 + */ + public function test_boolean_attributes_remain_boolean() { + $this->assertSame( + WP_HTML_Processor::normalize( '' ), + '', + 'Should have preserved the boolean attribute upon serialization.' + ); + } + + /** + * Ensures that attributes with values result in double-quoted attribute values. + * + * @ticket 62036 + */ + public function test_attributes_are_double_quoted() { + $this->assertSame( + WP_HTML_Processor::normalize( '

      ' ), + '

      ', + 'Should double-quote all attribute values.' + ); + } + + /** + * Ensures that self-closing flags on HTML void elements are not serialized, to + * prevent risk of conflating the flag with unquoted attribute values. + * + * Example: + * + * BR element with "class" attribute having value "clear" + *
      + * + * BR element with "class" attribute having value "clear" + *
      + * + * BR element with "class" attribute having value "clear/" + *
      + * + * @ticket 62036 + */ + public function test_void_elements_get_no_dangerous_self_closing_flag() { + $this->assertSame( + WP_HTML_Processor::normalize( '
      ' ), + '
      ', + 'Should have removed dangerous self-closing flag on HTML void element.' + ); + } + + /** + * Ensures that duplicate attributes are removed upon serialization. + * + * @ticket 62036 + */ + public function test_duplicate_attributes_are_removed() { + $this->assertSame( + WP_HTML_Processor::normalize( '
      ' ), + '
      ', + 'Should have removed all but the first copy of an attribute when duplicates exist.' + ); + } + + /** + * Ensures that SCRIPT contents are not escaped, as they are not parsed like text nodes are. + * + * @ticket 62036 + */ + public function test_script_contents_are_not_escaped() { + $this->assertSame( + WP_HTML_Processor::normalize( "" ), + "", + 'Should have preserved text inside a SCRIPT element, except for replacing NULL bytes.' + ); + } + + /** + * Ensures that STYLE contents are not escaped, as they are not parsed like text nodes are. + * + * @ticket 62036 + */ + public function test_style_contents_are_not_escaped() { + $this->assertSame( + WP_HTML_Processor::normalize( "" ), + "", + 'Should have preserved text inside a STYLE element, except for replacing NULL bytes.' + ); + } + + public function test_unexpected_closing_tags_are_removed() { + $this->assertSame( + WP_HTML_Processor::normalize( 'one
      twothree' ), + 'onetwothree', + 'Should have removed unpected closing tags.' + ); + } + + /** + * Ensures that self-closing elements in foreign content retain their self-closing flag. + * + * @ticket 62036 + */ + public function test_self_closing_foreign_elements_retain_their_self_closing_flag() { + $this->assertSame( + WP_HTML_Processor::normalize( '' ), + '', + 'Should have closed unclosed G element, but preserved the self-closing nature of the other G element.' + ); + } + + /** + * Ensures that incomplete syntax elements at the end of an HTML string are removed from + * the serialization, since these are often vectors of exploits for the successive HTML. + * + * @ticket 62036 + * + * @dataProvider data_incomplete_syntax_tokens + * + * @param string $incomplete_token An incomplete HTML syntax token. + */ + public function test_should_remove_incomplete_input_from_end( string $incomplete_token ) { + $this->assertSame( + WP_HTML_Processor::normalize( "content{$incomplete_token}" ), + 'content', + 'Should have removed the incomplete token from the end of the input.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_incomplete_syntax_tokens() { + return array( + 'Comment opener' => array( '", + 'Should have replaced the invalid comment syntax with normative syntax.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_bogus_comments() { + return array( + 'False DOCTYPE' => array( '' ), + 'CDATA look-alike' => array( '' ), + 'Immediately-closed markup instruction' => array( '' ), + 'Warning Symbol' => array( '' ), + 'PHP block look-alike' => array( '<', '?php foo(); ?', '>' ), + 'Funky comment' => array( '' ), + 'XML Processing Instruction look-alike' => array( '<', '?xml foo ', '>' ), + ); + } + + /** + * Ensures that NULL bytes are properly handled. + * + * @ticket 62036 + * + * @dataProvider data_tokens_with_null_bytes + * + * @param string $html_with_nulls HTML token containing NULL bytes in various places. + * @param string $expected_output Expected parse of HTML after handling NULL bytes. + */ + public function test_replaces_null_bytes_appropriately( string $html_with_nulls, string $expected_output ) { + $this->assertSame( + WP_HTML_Processor::normalize( $html_with_nulls ), + $expected_output, + 'Should have properly replaced or removed NULL bytes.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_tokens_with_null_bytes() { + return array( + 'Tag name' => array( "", "" ), + 'Attribute name' => array( "", "" ), + 'Attribute value' => array( "", "" ), + 'Body text' => array( "one\x00two", 'onetwo' ), + 'Foreign content text' => array( "one\x00two", "one\u{FFFD}two" ), + 'SCRIPT content' => array( "", "" ), + 'STYLE content' => array( "", "" ), + 'Comment text' => array( "", "" ), + ); + } + + /** + * @ticket 62396 + * + * @dataProvider data_provider_serialize_doctype + */ + public function test_full_document_serialize_includes_doctype( string $doctype_input, string $doctype_output ) { + $processor = WP_HTML_Processor::create_full_parser( + "{$doctype_input}👌" + ); + $this->assertSame( + "{$doctype_output}👌", + $processor->serialize() + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_provider_serialize_doctype() { + return array( + 'None' => array( '', '' ), + 'Empty' => array( '', '' ), + 'HTML5' => array( '', '' ), + 'Strange name' => array( '', '' ), + 'With public' => array( '', '' ), + 'With system' => array( '', '' ), + 'With public and system' => array( '', '' ), + 'Weird casing' => array( '', '' ), + 'Single quotes in public ID' => array( '', '' ), + 'Double quotes in public ID' => array( '', '' ), + 'Single quotes in system ID' => array( '', '' ), + 'Double quotes in system ID' => array( '', '' ), + ); + } + + /** + * Ensures that leading newlines in PRE, LISTING, and TEXTAREA elements are preserved upon normalization, + * and that normalization is idempotent in these cases. + * + * @ticket 64607 + * + * @dataProvider data_provider_normalize_special_leading_newline_cases + * + * @param string $input HTML input containing leading newlines in PRE, LISTING, or TEXTAREA elements. + * @param string $expected Expected output after normalization, which should preserve leading newlines. + */ + public function test_normalize_special_leading_newline_handling( string $input, string $expected ) { + $normalized = WP_HTML_Processor::normalize( $input ); + $this->assertEqualHTML( $expected, $normalized ); + $normalized_twice = WP_HTML_Processor::normalize( $normalized ); + $this->assertEqualHTML( $expected, $normalized_twice ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_provider_normalize_special_leading_newline_cases() { + return array( + 'Leading newline in PRE' => array( + "
      \nline 1\nline 2
      ", + "
      line 1\nline 2
      ", + ), + 'Double leading newline in PRE' => array( + "
      \n\nline 2\nline 3
      ", + "
      \n\nline 2\nline 3
      ", + ), + 'Multiple text nodes inside PRE' => array( + "
      \nline 1 still line 1
      ", + '
      line 1 still line 1
      ', + ), + 'Multiple text nodes inside PRE with leading newlines' => array( + "
      \n\nline 2 still line 2
      ", + "
      \n\nline 2 still line 2
      ", + ), + 'Leading newline in LISTING' => array( + "\nline 1\nline 2", + "line 1\nline 2", + ), + 'Double leading newline in LISTING' => array( + "\n\nline 2\nline 3", + "\n\nline 2\nline 3", + ), + 'Multiple text nodes inside LISTING' => array( + "\nline 1 still line 1", + 'line 1 still line 1', + ), + 'Multiple text nodes inside LISTING with leading newlines' => array( + "\n\nline 2 still line 2", + "\n\nline 2 still line 2", + ), + 'Leading newline in TEXTAREA' => array( + "", + "", + ), + 'Double leading newline in TEXTAREA' => array( + "", + "", + ), + ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessor.php b/tests/phpunit/tests/html-api/wpHtmlProcessor.php new file mode 100644 index 0000000000000..a89014282df73 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessor.php @@ -0,0 +1,1184 @@ +Light roast.

      ' ); + } + + /** + * @ticket 63854 + * + * @covers ::create_fragment + * @expectedIncorrectUsage WP_HTML_Processor::create_fragment + */ + public function test_create_fragment_validates_html_parameter() { + $processor = WP_HTML_Processor::create_fragment( null ); + $this->assertNull( $processor ); + } + + /** + * @ticket 63854 + * + * @covers ::create_full_parser + * @expectedIncorrectUsage WP_HTML_Processor::create_full_parser + */ + public function test_create_full_parser_validates_html_parameter() { + $processor = WP_HTML_Processor::create_full_parser( null ); + $this->assertNull( $processor ); + } + + /** + * Once stepping to the end of the document, WP_HTML_Processor::get_tag + * should no longer report a tag. It should report `null` because there + * is no tag matched or open. + * + * @ticket 59167 + * + * @covers WP_HTML_Processor::get_tag + */ + public function test_get_tag_is_null_once_document_is_finished() { + $processor = WP_HTML_Processor::create_fragment( '
      Test
      ' ); + $processor->next_tag(); + $this->assertSame( 'DIV', $processor->get_tag() ); + + $this->assertFalse( $processor->next_tag() ); + $this->assertNull( $processor->get_tag() ); + } + + /** + * Ensures that the proper tag-name remapping happens for the `IMAGE` tag. + * + * An HTML parser should treat an IMAGE tag as if it were an IMG tag, but + * only when found in the HTML namespace. As part of this rule, IMAGE tags + * in the HTML namespace are also void elements, while those in foreign + * content are not, making the self-closing flag significant. + * + * Example: + * + * // This input... + * + * + * // ...is equivalent to this normative HTML. + * + * + * @ticket 61576 + * + * @covers WP_HTML_Processor::get_tag + */ + public function test_get_tag_replaces_image_with_namespace_awareness() { + $processor = WP_HTML_Processor::create_fragment( '' ); + + $this->assertTrue( + $processor->next_tag(), + 'Could not find initial "" tag: check test setup.' + ); + + $this->assertSame( + 'IMG', + $processor->get_tag(), + 'HTML tags with the name "IMAGE" should be remapped to "IMG"' + ); + + $this->assertTrue( + $processor->next_tag(), + 'Could not find "" tag: check test setup.' + ); + + $this->assertTrue( + $processor->next_tag(), + 'Could not find SVG "" tag: check test setup.' + ); + + $this->assertSame( + 'IMAGE', + $processor->get_tag(), + 'Should not remap "IMAGE" to "IMG" for foreign elements.' + ); + } + + /** + * Ensures that the HTML Processor maintains its internal state through seek calls. + * + * Because the HTML Processor must track a stack of open elements and active formatting + * elements, when it seeks to another location within its document it must adjust those + * stacks, its internal state, in such a way that they remain valid after the seek. + * + * For instance, if currently matched inside an LI element and the Processor seeks to + * an earlier location before the parent UL, then it should not report that it's still + * inside an open LI element. + * + * @ticket 58517 + * + * @covers WP_HTML_Processor::next_tag + * @covers WP_HTML_Processor::seek + */ + public function test_clear_to_navigate_after_seeking() { + $processor = WP_HTML_Processor::create_fragment( '

      ' ); + + while ( $processor->next_tag() ) { + // Create a bookmark before entering a stack of elements and formatting elements. + if ( null !== $processor->get_attribute( 'one' ) ) { + $this->assertTrue( $processor->set_bookmark( 'one' ) ); + continue; + } + + // Create a bookmark inside of that stack. + if ( null !== $processor->get_attribute( 'two' ) ) { + $this->assertTrue( $processor->set_bookmark( 'two' ) ); + break; + } + } + + // Ensure that it's possible to seek back to the outside location. + $this->assertTrue( $processor->seek( 'one' ), 'Could not seek to earlier-seen location.' ); + $this->assertSame( 'DIV', $processor->get_tag(), "Should have jumped back to DIV but found {$processor->get_tag()} instead." ); + + /* + * Ensure that the P element from the inner location isn't still on the stack of open elements. + * If it were, then the first STRONG element, inside the outer DIV would match the next call. + */ + $this->assertTrue( $processor->next_tag( array( 'breadcrumbs' => array( 'P', 'STRONG' ) ) ), 'Failed to find given location after seeking.' ); + + // Only if the stack is properly managed will the processor advance to the inner STRONG element. + $this->assertTrue( $processor->get_attribute( 'two' ), "Found the wrong location given the breadcrumbs, at {$processor->get_tag()}." ); + + // Ensure that in seeking backwards the processor reports the correct full set of breadcrumbs. + $this->assertTrue( $processor->seek( 'one' ), 'Failed to jump back to first bookmark.' ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs(), 'Found wrong set of breadcrumbs navigating to node "one".' ); + + // Ensure that in seeking forwards the processor reports the correct full set of breadcrumbs. + $this->assertTrue( $processor->seek( 'two' ), 'Failed to jump forward to second bookmark.' ); + $this->assertTrue( $processor->get_attribute( 'two' ), "Found the wrong location given the bookmark, at {$processor->get_tag()}." ); + + $this->assertSame( array( 'HTML', 'BODY', 'P', 'STRONG' ), $processor->get_breadcrumbs(), 'Found wrong set of bookmarks navigating to node "two".' ); + } + + /** + * Ensures that support is added for reconstructing active formatting elements + * before the HTML Processor handles situations with unclosed formats requiring it. + * + * @ticket 58517 + * + * @covers WP_HTML_Processor::reconstruct_active_formatting_elements + */ + public function test_fails_to_reconstruct_formatting_elements() { + $processor = WP_HTML_Processor::create_fragment( '

      One

      Two

      Three

      Four' ); + + $this->assertTrue( $processor->next_tag( 'EM' ), 'Could not find first EM.' ); + $this->assertFalse( $processor->next_tag( 'EM' ), 'Should have aborted before finding second EM as it required reconstructing the first EM.' ); + } + + /** + * Ensure non-nesting tags do not nest. + * + * @ticket 60283 + * + * @covers WP_HTML_Processor::step_in_body + * @covers WP_HTML_Processor::is_void + * + * @dataProvider data_void_tags_not_ignored_in_body + * + * @param string $tag_name Name of void tag under test. + */ + public function test_cannot_nest_void_tags( $tag_name ) { + $processor = WP_HTML_Processor::create_fragment( "<{$tag_name}>

      " ); + + /* + * This HTML represents the same as the following HTML, + * assuming that it were provided `` as the tag: + * + * + * + * + *
      + * + * + */ + + $found_tag = $processor->next_tag(); + + $this->assertTrue( + $found_tag, + "Could not find first {$tag_name}." + ); + + $this->assertSame( + array( 'HTML', 'BODY', $tag_name ), + $processor->get_breadcrumbs(), + 'Found incorrect nesting of first element.' + ); + + $this->assertTrue( + $processor->next_tag(), + 'Should have found the DIV as the second tag.' + ); + + $this->assertSame( + array( 'HTML', 'BODY', 'DIV' ), + $processor->get_breadcrumbs(), + "DIV should have been a sibling of the {$tag_name}." + ); + } + + /** + * Ensure reporting that normal non-void HTML elements expect a closer. + * + * @ticket 61257 + */ + public function test_expects_closer_regular_tags() { + $processor = WP_HTML_Processor::create_fragment( '

      ' ); + + $tags = 0; + while ( $processor->next_tag() ) { + $this->assertTrue( + $processor->expects_closer(), + "Should have expected a closer for '{$processor->get_tag()}', but didn't." + ); + ++$tags; + } + + $this->assertSame( + 4, + $tags, + 'Did not find all the expected tags.' + ); + } + + /** + * Ensure reporting that non-tag HTML nodes expect a closer. + * + * @ticket 61257 + * + * @dataProvider data_self_contained_node_tokens + * + * @param string $self_contained_token String starting with HTML token that doesn't expect a closer, + * e.g. an HTML comment, text node, void tag, or special element. + */ + public function test_expects_closer_expects_no_closer_for_self_contained_tokens( $self_contained_token ) { + $processor = WP_HTML_Processor::create_fragment( $self_contained_token ); + $found_token = $processor->next_token(); + + $this->assertTrue( + $found_token, + "Failed to find any tokens in '{$self_contained_token}': check test data provider." + ); + + $this->assertFalse( + $processor->expects_closer(), + "Incorrectly expected a closer for node of type '{$processor->get_token_type()}'." + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_self_contained_node_tokens() { + $self_contained_nodes = array( + 'Normative comment' => array( '' ), + 'Comment with invalid closing' => array( '' ), + 'CDATA Section lookalike' => array( '' ), + 'Processing Instruction lookalike' => array( '' ), + 'Funky comment' => array( '' ), + 'Text node' => array( 'Trombone' ), + ); + + foreach ( self::data_void_tags_not_ignored_in_body() as $tag_name => $_name ) { + $self_contained_nodes[ "Void elements ({$tag_name})" ] = array( "<{$tag_name}>" ); + } + + foreach ( self::data_special_tags() as $tag_name => $_name ) { + $self_contained_nodes[ "Special atomic elements ({$tag_name})" ] = array( "<{$tag_name}>content" ); + } + + return $self_contained_nodes; + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_special_tags() { + return array( + 'IFRAME' => array( 'IFRAME' ), + 'NOEMBED' => array( 'NOEMBED' ), + 'NOFRAMES' => array( 'NOFRAMES' ), + 'SCRIPT' => array( 'SCRIPT' ), + 'STYLE' => array( 'STYLE' ), + 'TEXTAREA' => array( 'TEXTAREA' ), + 'TITLE' => array( 'TITLE' ), + 'XMP' => array( 'XMP' ), + ); + } + + /** + * Ensure non-nesting tags do not nest when processing tokens. + * + * @ticket 60382 + * + * @dataProvider data_void_tags_not_ignored_in_body + * + * @param string $tag_name Name of void tag under test. + */ + public function test_cannot_nest_void_tags_next_token( $tag_name ) { + $processor = WP_HTML_Processor::create_fragment( "<{$tag_name}>

      " ); + + /* + * This HTML represents the same as the following HTML, + * assuming that it were provided `` as the tag: + * + * + * + * + *
      + * + * + */ + + $found_tag = $processor->next_token(); + + $this->assertTrue( + $found_tag, + "Could not find first {$tag_name}." + ); + + $this->assertSame( + array( 'HTML', 'BODY', $tag_name ), + $processor->get_breadcrumbs(), + 'Found incorrect nesting of first element.' + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_void_tags() { + return array( + 'AREA' => array( 'AREA' ), + 'BASE' => array( 'BASE' ), + 'BR' => array( 'BR' ), + 'COL' => array( 'COL' ), + 'EMBED' => array( 'EMBED' ), + 'HR' => array( 'HR' ), + 'IMG' => array( 'IMG' ), + 'INPUT' => array( 'INPUT' ), + 'KEYGEN' => array( 'KEYGEN' ), + 'LINK' => array( 'LINK' ), + 'META' => array( 'META' ), + 'PARAM' => array( 'PARAM' ), + 'SOURCE' => array( 'SOURCE' ), + 'TRACK' => array( 'TRACK' ), + 'WBR' => array( 'WBR' ), + ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_void_tags_not_ignored_in_body() { + $all_void_tags = self::data_void_tags(); + unset( $all_void_tags['COL'] ); + + return $all_void_tags; + } + + /** + * Ensures that the HTML Processor properly reports the depth of a given element. + * + * @ticket 61255 + * + * @dataProvider data_html_with_target_element_and_depth_in_body + * + * @param string $html_with_target_element HTML containing element with `target` class. + * @param int $depth_at_element Depth into document at target node. + */ + public function test_reports_proper_element_depth_in_body( $html_with_target_element, $depth_at_element ) { + $processor = WP_HTML_Processor::create_fragment( $html_with_target_element ); + + $this->assertTrue( + $processor->next_tag( array( 'class_name' => 'target' ) ), + 'Failed to find target element: check test data provider.' + ); + + $this->assertSame( + $depth_at_element, + $processor->get_current_depth(), + 'HTML Processor reported the wrong depth at the matched element.' + ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_html_with_target_element_and_depth_in_body() { + return array( + 'Single element' => array( '
      ', 3 ), + 'Basic layout and formatting stack' => array( '

      ', 7 ), + 'Adjacent elements' => array( '

      ', 4 ), + ); + } + + /** + * Ensures that the HTML Processor properly reports the depth of a given non-element. + * + * @ticket 61255 + * + * @dataProvider data_html_with_target_element_and_depth_of_next_node_in_body + * + * @param string $html_with_target_element HTML containing element with `target` class. + * @param int $depth_after_element Depth into document immediately after target node. + */ + public function test_reports_proper_non_element_depth_in_body( $html_with_target_element, $depth_after_element ) { + $processor = WP_HTML_Processor::create_fragment( $html_with_target_element ); + + $this->assertTrue( + $processor->next_tag( array( 'class_name' => 'target' ) ), + 'Failed to find target element: check test data provider.' + ); + + $this->assertTrue( + $processor->next_token(), + 'Failed to find next node after target element: check tests data provider.' + ); + + $this->assertSame( + $depth_after_element, + $processor->get_current_depth(), + 'HTML Processor reported the wrong depth after the matched element.' + ); + } + + /** + * Data provider. + * + * @return array[]. + */ + public static function data_html_with_target_element_and_depth_of_next_node_in_body() { + return array( + 'Element then text' => array( '
      One Deeper', 4 ), + 'Basic layout and formatting stack' => array( '

      Formatted', 8 ), + 'Basic layout with text' => array( '

      ab

      cee', 8 ), + 'Adjacent elements' => array( '

      Here
      ', 5 ), + 'Adjacent text' => array( '

      BeforeAfter

      ', 4 ), + 'HTML comment' => array( '', 3 ), + 'HTML comment in DIV' => array( '
      ', 4 ), + 'Funky comment' => array( '

      What

      ', 5 ), + ); + } + + /** + * Ensures that elements which are unopened at the end of a document are implicitly closed. + * + * @ticket 61576 + */ + public function test_closes_unclosed_elements() { + $processor = WP_HTML_Processor::create_fragment( '

      ' ); + + $this->assertTrue( + $processor->next_tag( 'SPAN' ), + 'Could not find SPAN element: check test setup.' + ); + + // This is the end of the document, but there should be three closing events. + $processor->next_token(); + $this->assertSame( + 'SPAN', + $processor->get_tag(), + 'Should have found implicit SPAN closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'P', + $processor->get_tag(), + 'Should have found implicit P closing tag.' + ); + + $processor->next_token(); + $this->assertSame( + 'DIV', + $processor->get_tag(), + 'Should have found implicit DIV closing tag.' + ); + + $this->assertFalse( + $processor->next_token(), + "Should have failed to find any more tokens but found a '{$processor->get_token_name()}'" + ); + } + + /** + * Ensures that subclasses can be created from ::create_fragment method. + * + * @ticket 61374 + */ + public function test_subclass_create_fragment_creates_subclass() { + $processor = WP_HTML_Processor::create_fragment( '' ); + $this->assertInstanceOf( WP_HTML_Processor::class, $processor, '::create_fragment did not return class instance.' ); + + $subclass_instance = new class('') extends WP_HTML_Processor { + public function __construct( $html ) { + parent::__construct( $html, parent::CONSTRUCTOR_UNLOCK_CODE ); + } + }; + + $subclass_processor = call_user_func( array( get_class( $subclass_instance ), 'create_fragment' ), '' ); + $this->assertInstanceOf( get_class( $subclass_instance ), $subclass_processor, '::create_fragment did not return subclass instance.' ); + } + + /** + * Ensures that self-closing elements in foreign content properly report + * that they expect no closer. + * + * @ticket 61576 + */ + public function test_expects_closer_foreign_content_self_closing() { + $processor = WP_HTML_Processor::create_fragment( '' ); + + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'SVG', $processor->get_tag() ); + $this->assertFalse( $processor->expects_closer() ); + + $this->assertTrue( $processor->next_tag() ); + $this->assertSame( 'MATH', $processor->get_tag() ); + $this->assertTrue( $processor->expects_closer() ); + } + + /** + * Ensures that expects_closer works for void-like elements in foreign content. + * + * For example, `

      ' ); + + $this->assertTrue( $processor->next_tag( 'DIV' ) ); + $this->assertSame( array( 'HTML', 'BODY', 'TEMPLATE', 'SVG', 'TEMPLATE', 'FOREIGNOBJECT', 'DIV' ), $processor->get_breadcrumbs() ); + $this->assertTrue( $processor->next_tag( 'DIV' ) ); + $this->assertSame( array( 'HTML', 'BODY', 'DIV' ), $processor->get_breadcrumbs() ); + } + + /** + * Ensures that the tag processor is case sensitive when removing CSS classes in no-quirks mode. + * + * @ticket 61531 + * + * @covers ::remove_class + */ + public function test_remove_class_no_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $processor->remove_class( 'upper' ); + $this->assertSame( '', $processor->get_updated_html() ); + + $processor->remove_class( 'UPPER' ); + $this->assertSame( '', $processor->get_updated_html() ); + } + + /** + * Ensures that the tag processor is case sensitive when adding CSS classes in no-quirks mode. + * + * @ticket 61531 + * + * @covers ::add_class + */ + public function test_add_class_no_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $processor->add_class( 'UPPER' ); + $this->assertSame( '', $processor->get_updated_html() ); + + $processor->add_class( 'upper' ); + $this->assertSame( '', $processor->get_updated_html() ); + } + + /** + * Ensures that the tag processor is case sensitive when checking has CSS classes in no-quirks mode. + * + * @ticket 61531 + * + * @covers ::has_class + */ + public function test_has_class_no_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $this->assertFalse( $processor->has_class( 'upper' ) ); + $this->assertTrue( $processor->has_class( 'UPPER' ) ); + } + + /** + * Ensures that the tag processor lists unique CSS class names in no-quirks mode. + * + * @ticket 61531 + * + * @covers ::class_list + */ + public function test_class_list_no_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( + /* + * U+00C9 is LATIN CAPITAL LETTER E WITH ACUTE + * U+0045 is LATIN CAPITAL LETTER E + * U+0301 is COMBINING ACUTE ACCENT + * + * This tests not only that the class matching deduplicates the É, but also + * that it treats the same character in different normalization forms as + * distinct, since matching occurs on a byte-for-byte basis. + */ + "" + ); + $processor->next_tag( 'SPAN' ); + $class_list = iterator_to_array( $processor->class_list() ); + $this->assertSame( + array( 'A', 'a', 'B', 'b', 'É', "E\u{0301}", 'é' ), + $class_list + ); + } + + /** + * Ensures that the tag processor is case insensitive when removing CSS classes in quirks mode. + * + * @ticket 61531 + * + * @covers ::remove_class + */ + public function test_remove_class_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $processor->remove_class( 'upPer' ); + $this->assertSame( '', $processor->get_updated_html() ); + } + + /** + * Ensures that the tag processor is case insensitive when adding CSS classes in quirks mode. + * + * @ticket 61531 + * + * @covers ::add_class + */ + public function test_add_class_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $processor->add_class( 'upper' ); + + $this->assertSame( '', $processor->get_updated_html() ); + + $processor->add_class( 'ANOTHER-UPPER' ); + $this->assertSame( '', $processor->get_updated_html() ); + } + + /** + * Ensures that the tag processor is case sensitive when checking has CSS classes in quirks mode. + * + * @ticket 61531 + * + * @covers ::has_class + */ + public function test_has_class_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag( 'SPAN' ); + $this->assertTrue( $processor->has_class( 'upper' ) ); + $this->assertTrue( $processor->has_class( 'UPPER' ) ); + } + + /** + * Ensures that the tag processor lists unique CSS class names in quirks mode. + * + * @ticket 61531 + * + * @covers ::class_list + */ + public function test_class_list_quirks_mode() { + $processor = WP_HTML_Processor::create_full_parser( + /* + * U+00C9 is LATIN CAPITAL LETTER E WITH ACUTE + * U+0045 is LATIN CAPITAL LETTER E + * U+0065 is LATIN SMALL LETTER E + * U+0301 is COMBINING ACUTE ACCENT + * + * This tests not only that the class matching deduplicates the É, but also + * that it treats the same character in different normalization forms as + * distinct, since matching occurs on a byte-for-byte basis. + */ + "" + ); + $processor->next_tag( 'SPAN' ); + $class_list = iterator_to_array( $processor->class_list() ); + $this->assertSame( + array( 'a', 'b', 'É', "e\u{301}", 'é' ), + $class_list + ); + } + + /** + * Ensures that the processor correctly adjusts the namespace + * for elements inside HTML integration points. + * + * @ticket 61576 + */ + public function test_adjusts_for_html_integration_points_in_svg() { + $processor = WP_HTML_Processor::create_full_parser( + '' + ); + + // At the foreignObject, the processor is in the SVG namespace. + $this->assertTrue( + $processor->next_tag( 'foreignObject' ), + 'Failed to find "foreignObject" under test: check test setup.' + ); + + $this->assertSame( + 'svg', + $processor->get_namespace(), + 'Found the wrong namespace for the "foreignObject" element.' + ); + + /* + * The IMAGE tag should be handled according to HTML processing rules + * and transformted to an IMG tag because `foreignObject` is an HTML + * integration point. At this point, the processor is entering the HTML + * integration point. + */ + $this->assertTrue( + $processor->next_tag( 'IMG' ), + 'Failed to find expected "IMG" tag from "" source tag.' + ); + + $this->assertSame( + 'html', + $processor->get_namespace(), + 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.' + ); + + /* + * Again, the IMAGE tag should be handled according to HTML processing + * rules and transformted to an IMG tag because `foreignObject` is an + * HTML integration point. At this point, the processor is has entered + * SVG and is returning to an HTML integration point. + */ + $this->assertTrue( + $processor->next_tag( 'IMG' ), + 'Failed to find expected "IMG" tag from "" source tag.' + ); + + $this->assertSame( + 'html', + $processor->get_namespace(), + 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.' + ); + } + + /** + * Ensures that the processor correctly adjusts the namespace + * for elements inside MathML integration points. + * + * @ticket 61576 + */ + public function test_adjusts_for_mathml_integration_points() { + $processor = WP_HTML_Processor::create_fragment( + '' + ); + + // Advance token-by-token to ensure matching the right raw "" token. + $processor->next_token(); // Advance past the +MO. + $processor->next_token(); // Advance into the +IMG. + + $this->assertSame( + 'IMG', + $processor->get_tag(), + 'Failed to find expected "IMG" tag from "" source tag.' + ); + + $this->assertSame( + 'html', + $processor->get_namespace(), + 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.' + ); + + // Advance token-by-token to ensure matching the right raw "" token. + $processor->next_token(); // Advance past the -MO. + $processor->next_token(); // Advance past the +MATH. + $processor->next_token(); // Advance into the +IMAGE. + + $this->assertSame( + 'IMAGE', + $processor->get_tag(), + 'Failed to find the un-transformed "" tag.' + ); + + $this->assertSame( + 'math', + $processor->get_namespace(), + 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.' + ); + + $processor->next_token(); // Advance past the +MO. + $processor->next_token(); // Advance into the +IMG. + + $this->assertSame( + 'IMG', + $processor->get_tag(), + 'Failed to find expected "IMG" tag from "" source tag.' + ); + + $this->assertSame( + 'html', + $processor->get_namespace(), + 'Found the wrong namespace for the transformed "IMAGE"/"IMG" element.' + ); + } + + /** + * Ensures that the processor stops correctly on a FORM tag closer token. + * + * Form tag closers have complicated conditions. There was a bug where the processor + * would not stop correctly on a FORM tag closer token. Ensure this token is reachable. + * + * @ticket 61576 + */ + public function test_ensure_form_tag_closer_token_is_reachable() { + $processor = WP_HTML_Processor::create_fragment( '
      ' ); + + // Advance to . + $processor->next_token(); + $processor->next_token(); + + $this->assertSame( 'FORM', $processor->get_tag() ); + $this->assertTrue( $processor->is_tag_closer() ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_html_processor_with_extended_next_token() { + return array( + 'single_instance_per_tag' => array( + 'html' => ' + + + + Hello World + + +

      Hello World!

      + +

      Each tag should occur only once in this document. +

      The end.
      + + + ', + 'expected_token_counts' => array( + '+HTML' => 1, + '+HEAD' => 1, + '#text' => 14, + '+META' => 1, + '+TITLE' => 1, + '-HEAD' => 1, + '+BODY' => 1, + '+H1' => 1, + '-H1' => 1, + '+IMG' => 1, + '+P' => 1, + '#comment' => 1, + '-P' => 1, + '+FOOTER' => 1, + '-FOOTER' => 1, + '-BODY' => 1, + '-HTML' => 1, + '' => 1, + ), + ), + + 'multiple_tag_instances' => array( + 'html' => ' + + +

      Hello World!

      +

      First +

      Second +

      Third +

        +
      • 1 +
      • 2 +
      • 3 +
      + + + ', + 'expected_token_counts' => array( + '+HTML' => 1, + '+HEAD' => 1, + '-HEAD' => 1, + '+BODY' => 1, + '#text' => 13, + '+H1' => 1, + '-H1' => 1, + '+P' => 3, + '-P' => 3, + '+UL' => 1, + '+LI' => 3, + '-LI' => 3, + '-UL' => 1, + '-BODY' => 1, + '-HTML' => 1, + '' => 1, + ), + ), + + 'extreme_nested_formatting' => array( + 'html' => ' + + +

      + FORMAT +

      + + + ', + 'expected_token_counts' => array( + '+HTML' => 1, + '+HEAD' => 1, + '-HEAD' => 1, + '+BODY' => 1, + '#text' => 7, + '+P' => 1, + '+STRONG' => 1, + '+EM' => 1, + '+STRIKE' => 1, + '+I' => 1, + '+B' => 1, + '+U' => 1, + '-U' => 1, + '-B' => 1, + '-I' => 1, + '-STRIKE' => 1, + '-EM' => 1, + '-STRONG' => 1, + '-P' => 1, + '-BODY' => 1, + '-HTML' => 1, + '' => 1, + ), + ), + ); + } + + /** + * Ensures that subclasses to WP_HTML_Processor can do bookkeeping by extending the next_token() method. + * + * @ticket 62269 + * @dataProvider data_html_processor_with_extended_next_token + */ + public function test_ensure_next_token_method_extensibility( $html, $expected_token_counts ) { + require_once DIR_TESTDATA . '/html-api/token-counting-html-processor.php'; + + $processor = Token_Counting_HTML_Processor::create_full_parser( $html ); + while ( $processor->next_tag() ) { + continue; + } + + $this->assertEquals( $expected_token_counts, $processor->token_seen_count, 'Snapshot: ' . var_export( $processor->token_seen_count, true ) ); + } + + /** + * Ensure that lowercased tag_name query matches tags case-insensitively. + * + * @ticket 62427 + */ + public function test_next_tag_lowercase_tag_name() { + // The upper case
      is irrelevant but illustrates the case-insentivity. + $processor = WP_HTML_Processor::create_fragment( '
      ' ); + $this->assertTrue( $processor->next_tag( array( 'tag_name' => 'div' ) ) ); + + // The upper case is irrelevant but illustrates the case-insentivity. + $processor = WP_HTML_Processor::create_fragment( '' ); + $this->assertTrue( $processor->next_tag( array( 'tag_name' => 'rect' ) ) ); + } + + /** + * Ensure that the processor does not throw errors in cases of extreme HTML nesting. + * + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_deep_nesting_fails_process_without_error() { + $html = str_repeat( '', WP_HTML_Processor::MAX_BOOKMARKS * 2 ); + $processor = WP_HTML_Processor::create_fragment( $html ); + + while ( $processor->next_token() ) { + // Process tokens. + } + + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + } + + /** + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_deep_nesting_fails_processing_virtual_tokens_without_error() { + /* + * This test has some variability depending on how the virtual tokens align. + * In order to ensure that bookmarks are exhausted on a virtual token + * without throwing an error, 3 documents are parsed with different "offsets" + * to ensure that the bookmarks are exhaused on a virtual token in at least one of the runs. + * + * "

      …" produces: + * └─TABLE (real) + * └─TBODY (virtual) + * └─TR (virtual) + * └─TD (real) + * └─TABLE (real) + * └─TBODY (virtual) + * └─TR (virtual) + * └─TD (real) + * └─… + */ + $html_table_td = str_repeat( '\s*
      ', WP_HTML_Processor::MAX_BOOKMARKS * 2 ); + + // Offset 0 + $processor = WP_HTML_Processor::create_fragment( $html_table_td ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + + // Offset 1 + $processor = WP_HTML_Processor::create_fragment( "
      {$html_table_td}" ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + + // Offset 2 + $processor = WP_HTML_Processor::create_fragment( "
      {$html_table_td}" ); + while ( $processor->next_token() ) { + // Process tokens. + } + $this->assertSame( + WP_HTML_Processor::ERROR_EXCEEDED_MAX_BOOKMARKS, + $processor->get_last_error(), + 'Failed to report exceeded-max-bookmarks error.' + ); + } + + /** + * @ticket 64394 + * + * @expectedIncorrectUsage WP_HTML_Tag_Processor::set_bookmark + */ + public function test_prevents_unbounded_bookmarking() { + $processor = WP_HTML_Processor::create_full_parser( '' ); + $processor->next_tag(); + + // This might fail before the MAX_BOOKMARK limit, which is okay. + foreach ( range( 0, WP_HTML_Processor::MAX_BOOKMARKS ) as $n ) { + if ( ! $processor->set_bookmark( "{$n}" ) ) { + break; + } + } + + $this->assertFalse( + $processor->set_bookmark( 'beyond the limit' ) + ); + } +} diff --git a/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php new file mode 100644 index 0000000000000..911fa8b910b37 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpHtmlProcessorBreadcrumbs.php @@ -0,0 +1,583 @@ +assertTrue( $processor->next_token(), "Failed to step into supported {$tag_name} element." ); + $this->assertSame( $tag_name, $processor->get_tag(), "Misread {$tag_name} as a {$processor->get_tag()} element." ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_single_tag_of_supported_elements() { + $supported_elements = array( + 'A', + 'ABBR', + 'ACRONYM', // Neutralized. + 'ADDRESS', + 'APPLET', // Deprecated. + 'AREA', + 'ARTICLE', + 'ASIDE', + 'AUDIO', + 'B', + 'BASE', + 'BDI', + 'BDO', + 'BGSOUND', // Deprectated. + 'BIG', + 'BLINK', // Deprecated. + 'BR', + 'BUTTON', + 'CANVAS', + 'CENTER', // Neutralized. + 'CITE', + 'CODE', + 'DATA', + 'DD', + 'DATALIST', + 'DFN', + 'DEL', + 'DETAILS', + 'DIALOG', + 'DIR', + 'DIV', + 'DL', + 'DT', + 'EM', + 'EMBED', + 'FIELDSET', + 'FIGCAPTION', + 'FIGURE', + 'FONT', + 'FORM', + 'FOOTER', + 'H1', + 'H2', + 'H3', + 'H4', + 'H5', + 'H6', + 'HEADER', + 'HGROUP', + 'HR', + 'I', + 'IMG', + 'INS', + 'LI', + 'LINK', + 'ISINDEX', // Deprecated. + 'KBD', + 'KEYGEN', // Deprecated. + 'LABEL', + 'LEGEND', + 'LINK', + 'LISTING', // Deprecated. + 'MAIN', + 'MAP', + 'MARK', + 'MARQUEE', // Deprecated. + 'MENU', + 'META', + 'METER', + 'MULTICOL', // Deprecated. + 'NAV', + 'NEXTID', // Deprecated. + 'NOBR', // Neutralized. + 'NOEMBED', // Neutralized. + 'NOFRAMES', // Neutralized. + 'NOSCRIPT', + 'OBJECT', + 'OL', + 'OUTPUT', + 'P', + 'PICTURE', + 'PROGRESS', + 'Q', + 'RB', // Neutralized. + 'RP', + 'RT', + 'RTC', // Neutralized. + 'RUBY', + 'SAMP', + 'SCRIPT', + 'SEARCH', + 'SECTION', + 'SLOT', + 'SMALL', + 'SPACER', // Deprecated. + 'SPAN', + 'STRIKE', + 'STRONG', + 'STYLE', + 'SUB', + 'SUMMARY', + 'SUP', + 'TABLE', + 'TEXTAREA', + 'TIME', + 'TITLE', + 'TT', + 'U', + 'UL', + 'VAR', + 'VIDEO', + 'XMP', // Deprecated, use PRE instead. + ); + + $data = array(); + foreach ( $supported_elements as $tag_name ) { + $closer = in_array( $tag_name, array( 'NOEMBED', 'NOFRAMES', 'SCRIPT', 'STYLE', 'TEXTAREA', 'TITLE', 'XMP' ), true ) + ? "" + : ''; + + $data[ $tag_name ] = array( "<{$tag_name}>{$closer}", $tag_name ); + } + + $data['IMAGE (treated as an IMG)'] = array( '', 'IMG' ); + + return $data; + } + + /** + * @ticket 58517 + * + * @dataProvider data_unsupported_markup + * + * @param string $html HTML containing unsupported markup. + */ + public function test_fails_when_encountering_unsupported_markup( $html, $description ) { + $processor = WP_HTML_Processor::create_fragment( $html ); + + while ( $processor->next_token() && null === $processor->get_attribute( 'supported' ) ) { + continue; + } + + $this->assertNull( + $processor->get_last_error(), + 'Bailed on unsupported input before finding supported checkpoint: check test code.' + ); + + $this->assertTrue( $processor->get_attribute( 'supported' ), 'Did not find required supported element.' ); + $processor->next_token(); + $this->assertNotNull( $processor->get_last_error(), "Didn't properly reject unsupported markup: {$description}" ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_unsupported_markup() { + return array( + 'A with formatting following unclosed A' => array( + 'Click Here', + 'Unclosed formatting requires complicated reconstruction.', + ), + + 'A after unclosed A inside DIV' => array( + '
      ', + 'A is a formatting element, which requires more complicated reconstruction.', + ), + ); + } + + /** + * @ticket 58517 + * + * @covers WP_HTML_Processor::next_tag + * + * @dataProvider data_html_target_with_breadcrumbs + * + * @param string $html HTML string with tags in it, one of which contains the "target" attribute. + * @param array $breadcrumbs Breadcrumbs of element with "target" attribute set. + * @param int $n How many breadcrumb matches to scan through in order to find "target" element. + */ + public function test_finds_correct_tag_given_breadcrumbs( $html, $breadcrumbs, $n ) { + $processor = WP_HTML_Processor::create_fragment( $html ); + + $processor->next_tag( + array( + 'breadcrumbs' => $breadcrumbs, + 'match_offset' => $n, + ) + ); + + $this->assertNotNull( $processor->get_tag(), 'Failed to find target node.' ); + $this->assertTrue( $processor->get_attribute( 'target' ), "Found {$processor->get_tag()} element didn't contain the necessary 'target' attribute." ); + } + + /** + * @ticket 58517 + * + * @covers WP_HTML_Processor::get_breadcrumbs + * + * @dataProvider data_html_target_with_breadcrumbs + * + * @param string $html HTML string with tags in it, one of which contains the "target" attribute. + * @param array $breadcrumbs Breadcrumbs of element with "target" attribute set. + * @param int $ignored_n Not used in this test but provided in the dataset for other tests. + */ + public function test_reports_correct_breadcrumbs_for_html( $html, $breadcrumbs, $ignored_n ) { + $processor = WP_HTML_Processor::create_fragment( $html ); + + while ( $processor->next_tag() && null === $processor->get_attribute( 'target' ) ) { + continue; + } + + $this->assertNotNull( $processor->get_tag(), 'Failed to find the target node.' ); + $this->assertSame( $breadcrumbs, $processor->get_breadcrumbs(), 'Found the wrong path from the root of the HTML document to the target node.' ); + } + + /** + * Data provider. + * + * @return array[] + */ + public static function data_html_target_with_breadcrumbs() { + return array( + 'Simple IMG tag' => array( '', array( 'HTML', 'BODY', 'IMG' ), 1 ), + 'Two sibling IMG tags' => array( '', array( 'HTML', 'BODY', 'IMG' ), 2 ), + 'Three sibling IMG tags, an IMAGE in last place' => array( '', array( 'HTML', 'BODY', 'IMG' ), 3 ), + 'IMG inside a DIV' => array( '
      ', array( 'HTML', 'BODY', 'DIV', 'IMG' ), 1 ), + 'DIV inside a DIV' => array( '
      ', array( 'HTML', 'BODY', 'DIV', 'DIV' ), 1 ), + 'IMG inside many DIVS' => array( '
      ', array( 'HTML', 'BODY', 'DIV', 'DIV', 'DIV', 'DIV', 'IMG' ), 1 ), + 'DIV inside DIV after IMG' => array( '
      ', array( 'HTML', 'BODY', 'DIV', 'DIV' ), 1 ), + 'IMG after DIV' => array( '
      ', array( 'HTML', 'BODY', 'IMG' ), 1 ), + 'IMG after two DIVs' => array( '
      ', array( 'HTML', 'BODY', 'IMG' ), 1 ), + 'IMG after two DIVs with nesting' => array( '
      ', array( 'HTML', 'BODY', 'IMG' ), 1 ), + 'IMG after invalid DIV closer' => array( '
      ', array( 'HTML', 'BODY', 'IMG' ), 1 ), + 'EM inside DIV' => array( '
      The weather is beautiful.
      ', array( 'HTML', 'BODY', 'DIV', 'EM' ), 1 ), + 'EM after closed EM' => array( '', array( 'HTML', 'BODY', 'EM' ), 2 ), + 'EM after closed EMs' => array( '', array( 'HTML', 'BODY', 'EM' ), 5 ), + 'EM after unclosed EM' => array( '', array( 'HTML', 'BODY', 'EM', 'EM' ), 1 ), + 'EM after unclosed EM after DIV' => array( '
      ', array( 'HTML', 'BODY', 'EM', 'DIV', 'EM' ), 1 ), + // This should work for all formatting elements, but if two work, the others probably do too. + 'CODE after unclosed CODE after DIV' => array( '
      ', array( 'HTML', 'BODY', 'CODE', 'DIV', 'CODE' ), 1 ), + 'P after unclosed P' => array( '

      ', array( 'HTML', 'BODY', 'P' ), 2 ), + 'Unclosed EM inside P after unclosed P' => array( '

      ', array( 'HTML', 'BODY', 'EM', 'P', 'EM' ), 1 ), + 'P after closed P' => array( '

      something

      This one

      ', array( 'HTML', 'BODY', 'P' ), 2 ), + 'A after unclosed A' => array( '', array( 'HTML', 'BODY', 'A' ), 2 ), + 'A after unclosed A, after a P' => array( '

      ', array( 'HTML', 'BODY', 'P', 'A' ), 2 ), + // This one adds a test at a deep stack depth to ensure things work for situations beyond short test docs. + 'Large HTML document with deep P' => array( + '

      ', + array( 'HTML', 'BODY', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'DIV', 'STRONG', 'EM', 'CODE' ), + 2, + ), + 'MAIN inside MAIN inside SPAN' => array( '
      ', array( 'HTML', 'BODY', 'SPAN', 'MAIN', 'MAIN' ), 1 ), + 'MAIN next to unclosed P' => array( '

      ', array( 'HTML', 'BODY', 'MAIN' ), 1 ), + 'LI after unclosed LI' => array( '
    • one
    • two
    • three', array( 'HTML', 'BODY', 'LI' ), 3 ), + 'LI in UL in LI' => array( '
    • Field 1 Value', $actual ); $this->assertStringContainsString( 'Another Field 1 Value', $actual ); diff --git a/tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php b/tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php index 9653c79818a5b..5410f851f4b2a 100644 --- a/tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php +++ b/tests/phpunit/tests/privacy/wpPrivacyProcessPersonalDataExportPage.php @@ -5,15 +5,9 @@ * @package WordPress * @subpackage UnitTests * @since 5.2.0 - */ - -/** - * Tests_Privacy_wpPrivacyProcessPersonalDataExportPage class. * * @group privacy * @covers ::wp_privacy_process_personal_data_export_page - * - * @since 5.2.0 */ class Tests_Privacy_wpPrivacyProcessPersonalDataExportPage extends WP_UnitTestCase { /** @@ -140,6 +134,13 @@ class Tests_Privacy_wpPrivacyProcessPersonalDataExportPage extends WP_UnitTestCa */ public $_export_data_grouped_fetched_within_callback; + /** + * Original error level. + * + * @var int + */ + private $orig_error_level; + /** * Create user request fixtures shared by test methods. * @@ -209,8 +210,8 @@ public function set_up() { add_filter( 'wp_die_ajax_handler', array( $this, 'get_wp_die_handler' ), 1, 1 ); // Suppress warnings from "Cannot modify header information - headers already sent by". - $this->_error_level = error_reporting(); - error_reporting( $this->_error_level & ~E_WARNING ); + $this->orig_error_level = error_reporting(); + error_reporting( $this->orig_error_level & ~E_WARNING ); } /** @@ -219,7 +220,7 @@ public function set_up() { * @since 5.2.0 */ public function tear_down() { - error_reporting( $this->_error_level ); + error_reporting( $this->orig_error_level ); parent::tear_down(); } @@ -625,12 +626,12 @@ public function test_request_status_transitions_correctly( $expected_status, $re * * @return array { * @type array { - * @string string $expected_status The expected post status after calling the function. - * @string string $response_page The exporter page to pass. Options are 'first' and 'last'. Default 'first'. - * @string string $exporter_index The exporter index to pass. Options are 'first' and 'last'. Default 'first'. - * @string string $page_index The page index to pass. Options are 'first' and 'last'. Default 'first'. - * @bool bool $send_as_email If the response should be sent as an email. - * @string string $exporter_key The slug (key) of the exporter to pass. + * @type string $expected_status The expected post status after calling the function. + * @type string $response_page The exporter page to pass. Options are 'first' and 'last'. Default 'first'. + * @type string $exporter_index The exporter index to pass. Options are 'first' and 'last'. Default 'first'. + * @type string $page_index The page index to pass. Options are 'first' and 'last'. Default 'first'. + * @type bool $send_as_email If the response should be sent as an email. + * @type string $exporter_key The slug (key) of the exporter to pass. * } * } */ diff --git a/tests/phpunit/tests/privacy/wpPrivacySendErasureFulfillmentNotification.php b/tests/phpunit/tests/privacy/wpPrivacySendErasureFulfillmentNotification.php index 96b4e0ecd9651..bb6195912e233 100644 --- a/tests/phpunit/tests/privacy/wpPrivacySendErasureFulfillmentNotification.php +++ b/tests/phpunit/tests/privacy/wpPrivacySendErasureFulfillmentNotification.php @@ -5,15 +5,9 @@ * @package WordPress * @subpackage UnitTests * @since 5.1.0 - */ - -/** - * Tests_Privacy_wpPrivacySendErasureFulfillmentNotification class. * * @group privacy * @covers ::_wp_privacy_send_erasure_fulfillment_notification - * - * @since 5.1.0 */ class Tests_Privacy_wpPrivacySendErasureFulfillmentNotification extends WP_UnitTestCase { /** @@ -104,6 +98,58 @@ public function tear_down() { parent::tear_down(); } + /** + * The function should not send an email when the request ID does not exist. + * + * @ticket 44234 + */ + public function test_should_not_send_email_when_not_a_valid_request_id() { + _wp_privacy_send_erasure_fulfillment_notification( 1234567890 ); + + $mailer = tests_retrieve_phpmailer_instance(); + + $this->assertEmpty( $mailer->mock_sent ); + } + + /** + * The function should not send an email when the ID passed does not correspond to a user request. + * + * @ticket 44234 + */ + public function test_should_not_send_email_when_not_a_user_request() { + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', // Should be 'user_request'. + ) + ); + + _wp_privacy_send_erasure_fulfillment_notification( $post_id ); + $mailer = tests_retrieve_phpmailer_instance(); + + $this->assertEmpty( $mailer->mock_sent ); + } + + /** + * The function should not send an email when the request is not completed. + * + * @ticket 44234 + */ + public function test_should_not_send_email_when_request_not_completed() { + wp_update_post( + array( + 'ID' => self::$request_id, + 'post_status' => 'request-confirmed', // Should be 'request-completed'. + ) + ); + + _wp_privacy_send_erasure_fulfillment_notification( self::$request_id ); + + $mailer = tests_retrieve_phpmailer_instance(); + + $this->assertEmpty( $mailer->mock_sent ); + $this->assertFalse( metadata_exists( 'post', self::$request_id, '_wp_user_notified' ) ); + } + /** * The function should send an email when a valid request ID is passed. * @@ -131,7 +177,7 @@ public function test_should_send_email_no_privacy_policy() { * @ticket 44234 */ public function test_should_send_email_with_privacy_policy() { - $privacy_policy = $this->factory->post->create( + $privacy_policy = self::factory()->post->create( array( 'post_type' => 'page', 'title' => 'Site Privacy Policy', @@ -288,58 +334,6 @@ public function modify_email_headers( $headers ) { return $headers; } - /** - * The function should not send an email when the request ID does not exist. - * - * @ticket 44234 - */ - public function test_should_not_send_email_when_passed_invalid_request_id() { - _wp_privacy_send_erasure_fulfillment_notification( 1234567890 ); - - $mailer = tests_retrieve_phpmailer_instance(); - - $this->assertEmpty( $mailer->mock_sent ); - } - - /** - * The function should not send an email when the ID passed does not correspond to a user request. - * - * @ticket 44234 - */ - public function test_should_not_send_email_when_not_user_request() { - $post_id = $this->factory->post->create( - array( - 'post_type' => 'post', // Should be 'user_request'. - ) - ); - - _wp_privacy_send_erasure_fulfillment_notification( $post_id ); - $mailer = tests_retrieve_phpmailer_instance(); - - $this->assertEmpty( $mailer->mock_sent ); - } - - /** - * The function should not send an email when the request is not completed. - * - * @ticket 44234 - */ - public function test_should_not_send_email_when_request_not_completed() { - wp_update_post( - array( - 'ID' => self::$request_id, - 'post_status' => 'request-confirmed', // Should be 'request-completed'. - ) - ); - - _wp_privacy_send_erasure_fulfillment_notification( self::$request_id ); - - $mailer = tests_retrieve_phpmailer_instance(); - - $this->assertEmpty( $mailer->mock_sent ); - $this->assertFalse( metadata_exists( 'post', self::$request_id, '_wp_user_notified' ) ); - } - /** * The function should respect the user locale settings when the site uses the default locale. * diff --git a/tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php b/tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php index 5c9336ad97571..37e2e4c282bd5 100644 --- a/tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php +++ b/tests/phpunit/tests/privacy/wpPrivacySendPersonalDataExportEmail.php @@ -5,15 +5,9 @@ * @package WordPress * @subpackage UnitTests * @since 4.9.6 - */ - -/** - * Tests_Privacy_wpPrivacySendPersonalDataExportEmail class. * * @group privacy * @covers ::wp_privacy_send_personal_data_export_email - * - * @since 4.9.6 */ class Tests_Privacy_wpPrivacySendPersonalDataExportEmail extends WP_UnitTestCase { /** @@ -52,27 +46,6 @@ class Tests_Privacy_wpPrivacySendPersonalDataExportEmail extends WP_UnitTestCase */ protected static $admin_user; - /** - * Reset the mocked phpmailer instance before each test method. - * - * @since 4.9.6 - */ - public function set_up() { - parent::set_up(); - reset_phpmailer_instance(); - } - - /** - * Reset the mocked phpmailer instance after each test method. - * - * @since 4.9.6 - */ - public function tear_down() { - reset_phpmailer_instance(); - restore_previous_locale(); - parent::tear_down(); - } - /** * Create user request fixtures shared by test methods. * @@ -101,31 +74,32 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } /** - * The function should send an export link to the requester when the user request is confirmed. + * Reset the mocked phpmailer instance before each test method. + * + * @since 4.9.6 */ - public function test_function_should_send_export_link_to_requester() { - $exports_url = wp_privacy_exports_url(); - $export_file_name = 'wp-personal-data-file-Wv0RfMnGIkl4CFEDEEkSeIdfLmaUrLsl.zip'; - $export_file_url = $exports_url . $export_file_name; - update_post_meta( self::$request_id, '_export_file_name', $export_file_name ); - - $email_sent = wp_privacy_send_personal_data_export_email( self::$request_id ); - $mailer = tests_retrieve_phpmailer_instance(); + public function set_up() { + parent::set_up(); + reset_phpmailer_instance(); + } - $this->assertSame( 'request-confirmed', get_post_status( self::$request_id ) ); - $this->assertSame( self::$requester_email, $mailer->get_recipient( 'to' )->address ); - $this->assertStringContainsString( 'Personal Data Export', $mailer->get_sent()->subject ); - $this->assertStringContainsString( $export_file_url, $mailer->get_sent()->body ); - $this->assertStringContainsString( 'please download it', $mailer->get_sent()->body ); - $this->assertTrue( $email_sent ); + /** + * Reset the mocked phpmailer instance after each test method. + * + * @since 4.9.6 + */ + public function tear_down() { + reset_phpmailer_instance(); + restore_previous_locale(); + parent::tear_down(); } /** - * The function should error when the request ID is invalid. + * The function should error when the request ID does not exist. * * @since 4.9.6 */ - public function test_function_should_error_when_request_id_invalid() { + public function test_should_return_wp_error_when_not_a_valid_request_id() { $request_id = 0; $email_sent = wp_privacy_send_personal_data_export_email( $request_id ); $this->assertWPError( $email_sent ); @@ -137,12 +111,30 @@ public function test_function_should_error_when_request_id_invalid() { $this->assertSame( 'invalid_request', $email_sent->get_error_code() ); } + /** + * The function should error when the ID passed does not correspond to a user request. + * + * @since 6.7.0 + * @ticket 46560 + */ + public function test_should_return_wp_error_when_not_a_user_request() { + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', // Should be 'user_request'. + ) + ); + + $email_sent = wp_privacy_send_personal_data_export_email( $post_id ); + $this->assertWPError( $email_sent ); + $this->assertSame( 'invalid_request', $email_sent->get_error_code() ); + } + /** * The function should error when the email was not sent. * * @since 4.9.6 */ - public function test_return_wp_error_when_send_fails() { + public function test_should_return_wp_error_when_sending_fails() { add_filter( 'wp_mail_from', '__return_empty_string' ); // Cause `wp_mail()` to return false. $email_sent = wp_privacy_send_personal_data_export_email( self::$request_id ); @@ -150,6 +142,26 @@ public function test_return_wp_error_when_send_fails() { $this->assertSame( 'privacy_email_error', $email_sent->get_error_code() ); } + /** + * The function should send an export link to the requester when the user request is confirmed. + */ + public function test_should_send_export_link_to_requester() { + $exports_url = wp_privacy_exports_url(); + $export_file_name = 'wp-personal-data-file-Wv0RfMnGIkl4CFEDEEkSeIdfLmaUrLsl.zip'; + $export_file_url = $exports_url . $export_file_name; + update_post_meta( self::$request_id, '_export_file_name', $export_file_name ); + + $email_sent = wp_privacy_send_personal_data_export_email( self::$request_id ); + $mailer = tests_retrieve_phpmailer_instance(); + + $this->assertSame( 'request-confirmed', get_post_status( self::$request_id ) ); + $this->assertSame( self::$requester_email, $mailer->get_recipient( 'to' )->address ); + $this->assertStringContainsString( 'Personal Data Export', $mailer->get_sent()->subject ); + $this->assertStringContainsString( $export_file_url, $mailer->get_sent()->body ); + $this->assertStringContainsString( 'please download it', $mailer->get_sent()->body ); + $this->assertTrue( $email_sent ); + } + /** * The export expiration should be filterable. * diff --git a/tests/phpunit/tests/privacy/wpPrivacySendRequestConfirmationNotification.php b/tests/phpunit/tests/privacy/wpPrivacySendRequestConfirmationNotification.php index f2933bdda0cfe..59d56c0642f35 100644 --- a/tests/phpunit/tests/privacy/wpPrivacySendRequestConfirmationNotification.php +++ b/tests/phpunit/tests/privacy/wpPrivacySendRequestConfirmationNotification.php @@ -5,16 +5,10 @@ * @package WordPress * @subpackage UnitTests * @since 4.9.8 - */ - -/** - * Tests_Privacy_wpPrivacySendRequestConfirmationNotification class. * * @group privacy * @group user * @covers ::_wp_privacy_send_request_confirmation_notification - * - * @since 4.9.8 */ class Tests_Privacy_wpPrivacySendRequestConfirmationNotification extends WP_UnitTestCase { /** @@ -38,11 +32,11 @@ public function tear_down() { } /** - * The function should not send emails when the request ID does not exist. + * The function should not send an email when the request ID does not exist. * * @ticket 43967 */ - public function test_function_should_not_send_email_when_not_a_valid_request_id() { + public function test_should_not_send_email_when_not_a_valid_request_id() { _wp_privacy_send_request_confirmation_notification( 1234567890 ); $mailer = tests_retrieve_phpmailer_instance(); @@ -50,14 +44,14 @@ public function test_function_should_not_send_email_when_not_a_valid_request_id( } /** - * The function should not send emails when the ID passed is not a WP_User_Request. + * The function should not send an email when the ID passed does not correspond to a user request. * * @ticket 43967 */ - public function test_function_should_not_send_email_when_not_a_wp_user_request() { - $post_id = $this->factory->post->create( + public function test_should_not_send_email_when_not_a_user_request() { + $post_id = self::factory()->post->create( array( - 'post_type' => 'post', + 'post_type' => 'post', // Should be 'user_request'. ) ); @@ -72,7 +66,7 @@ public function test_function_should_not_send_email_when_not_a_wp_user_request() * * @ticket 43967 */ - public function test_function_should_send_email_to_site_admin_when_user_request_confirmed() { + public function test_should_send_email_to_site_admin_when_user_request_confirmed() { $email = 'export.request.from.unregistered.user@example.com'; $request_id = wp_create_user_request( $email, 'export_personal_data' ); @@ -95,7 +89,7 @@ public function test_function_should_send_email_to_site_admin_when_user_request_ * * @ticket 43967 */ - public function test_function_should_only_send_email_to_site_admin_when_user_request_is_confirmed() { + public function test_should_only_send_email_to_site_admin_when_user_request_is_confirmed() { $email = 'export.request.from.unregistered.user@example.com'; $request_id = wp_create_user_request( $email, 'export_personal_data' ); @@ -115,7 +109,7 @@ public function test_function_should_only_send_email_to_site_admin_when_user_req * * @ticket 43967 */ - public function test_function_should_only_send_email_once_to_admin_when_user_request_is_confirmed() { + public function test_should_only_send_email_once_to_admin_when_user_request_is_confirmed() { $email = 'export.request.from.unregistered.user@example.com'; $request_id = wp_create_user_request( $email, 'export_personal_data' ); @@ -246,5 +240,4 @@ public function modify_email_headers( $headers ) { return $headers; } - } diff --git a/tests/phpunit/tests/query.php b/tests/phpunit/tests/query.php index 8dfd3fb200366..40d23816d1ab6 100644 --- a/tests/phpunit/tests/query.php +++ b/tests/phpunit/tests/query.php @@ -17,6 +17,9 @@ public function test_nested_loop_reset_postdata() { $nested_post_id = self::factory()->post->create(); $first_query = new WP_Query( array( 'post__in' => array( $post_id ) ) ); + + $this->assertTrue( $first_query->have_posts() ); + while ( $first_query->have_posts() ) { $first_query->the_post(); $second_query = new WP_Query( array( 'post__in' => array( $nested_post_id ) ) ); @@ -33,7 +36,7 @@ public function test_nested_loop_reset_postdata() { * @ticket 16471 */ public function test_default_query_var() { - $query = new WP_Query; + $query = new WP_Query(); $this->assertSame( '', $query->get( 'nonexistent' ) ); $this->assertFalse( $query->get( 'nonexistent', false ) ); $this->assertTrue( $query->get( 'nonexistent', true ) ); @@ -644,19 +647,19 @@ public function test_get_queried_object_should_return_first_of_multiple_terms() register_taxonomy( 'tax1', 'post' ); register_taxonomy( 'tax2', 'post' ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'taxonomy' => 'tax1', 'name' => 'term1', ) ); - $term2 = $this->factory->term->create( + $term2 = self::factory()->term->create( array( 'taxonomy' => 'tax2', 'name' => 'term2', ) ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_object_terms( $post_id, 'term1', 'tax1' ); wp_set_object_terms( $post_id, 'term2', 'tax2' ); @@ -674,19 +677,19 @@ public function test_query_vars_should_match_first_of_multiple_terms() { register_taxonomy( 'tax1', 'post' ); register_taxonomy( 'tax2', 'post' ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'taxonomy' => 'tax1', 'name' => 'term1', ) ); - $term2 = $this->factory->term->create( + $term2 = self::factory()->term->create( array( 'taxonomy' => 'tax2', 'name' => 'term2', ) ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_object_terms( $post_id, 'term1', 'tax1' ); wp_set_object_terms( $post_id, 'term2', 'tax2' ); @@ -696,4 +699,272 @@ public function test_query_vars_should_match_first_of_multiple_terms() { $this->assertSame( 'tax1', get_query_var( 'taxonomy' ) ); $this->assertSame( 'term1', get_query_var( 'term' ) ); } + + /** + * @ticket 55100 + */ + public function test_get_queried_object_should_work_for_author_name_before_get_posts() { + $user_id = self::factory()->user->create(); + $user = get_user_by( 'ID', $user_id ); + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + ) + ); + + $this->go_to( home_url( '?author=' . $user_id ) ); + + $this->assertInstanceOf( 'WP_User', get_queried_object() ); + $this->assertSame( get_queried_object_id(), $user_id ); + + $this->go_to( home_url( '?author_name=' . $user->user_nicename ) ); + + $this->assertInstanceOf( 'WP_User', get_queried_object() ); + $this->assertSame( get_queried_object_id(), $user_id ); + } + + /** + * Tests that the `posts_clauses` filter receives an array of clauses + * with the other `posts_*` filters applied, e.g. `posts_join_paged`. + * + * @ticket 55699 + * @covers WP_Query::get_posts + */ + public function test_posts_clauses_filter_should_receive_filtered_clauses() { + add_filter( + 'posts_join_paged', + static function () { + return '/* posts_join_paged */'; + } + ); + + $filter = new MockAction(); + add_filter( 'posts_clauses', array( $filter, 'filter' ), 10, 2 ); + $this->go_to( '/' ); + $filter_args = $filter->get_args(); + $posts_clauses = $filter_args[0][0]; + + $this->assertArrayHasKey( 'join', $posts_clauses ); + $this->assertSame( '/* posts_join_paged */', $posts_clauses['join'] ); + } + + /** + * Tests that the `posts_clauses_request` filter receives an array of clauses + * with the other `posts_*_request` filters applied, e.g. `posts_join_request`. + * + * @ticket 55699 + * @covers WP_Query::get_posts + */ + public function test_posts_clauses_request_filter_should_receive_filtered_clauses() { + add_filter( + 'posts_join_request', + static function () { + return '/* posts_join_request */'; + } + ); + + $filter = new MockAction(); + add_filter( 'posts_clauses_request', array( $filter, 'filter' ), 10, 2 ); + $this->go_to( '/' ); + $filter_args = $filter->get_args(); + $posts_clauses_request = $filter_args[0][0]; + + $this->assertArrayHasKey( 'join', $posts_clauses_request ); + $this->assertSame( '/* posts_join_request */', $posts_clauses_request['join'] ); + } + + /** + * Tests that is_post_type_archive() returns false for an undefined post type. + * + * @ticket 56287 + * + * @covers ::is_post_type_archive + */ + public function test_is_post_type_archive_should_return_false_for_an_undefined_post_type() { + global $wp_query; + + $post_type = '56287-post-type'; + + // Force the request to be a post type archive. + $wp_query->is_post_type_archive = true; + $wp_query->set( 'post_type', $post_type ); + + $this->assertFalse( is_post_type_archive( $post_type ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_singular_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'pagename' => 'non-existent-page', + ) + ); + + $this->assertSame( 0, $q->post_count ); + $this->assertFalse( $q->is_single() ); + + $this->assertTrue( $q->is_singular() ); + $this->assertFalse( $q->is_singular( 'page' ) ); + + $this->assertTrue( $q->is_page() ); + $this->assertFalse( $q->is_page( 'non-existent-page' ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_single_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'name' => 'non-existent-post', + ) + ); + + $this->assertSame( 0, $q->post_count ); + $this->assertFalse( $q->is_page() ); + + $this->assertTrue( $q->is_singular() ); + $this->assertFalse( $q->is_singular( 'post' ) ); + + $this->assertTrue( $q->is_single() ); + $this->assertFalse( $q->is_single( 'non-existent-post' ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_attachment_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'attachment' => 'non-existent-attachment', + ) + ); + + $this->assertSame( 0, $q->post_count ); + + $this->assertTrue( $q->is_singular() ); + $this->assertFalse( $q->is_singular( 'attachment' ) ); + + $this->assertTrue( $q->is_attachment() ); + $this->assertFalse( $q->is_attachment( 'non-existent-attachment' ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_author_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'author_name' => 'non-existent-author', + ) + ); + + $this->assertSame( 0, $q->post_count ); + + $this->assertTrue( $q->is_author() ); + $this->assertFalse( $q->is_author( 'non-existent-author' ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_category_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'category_name' => 'non-existent-category', + ) + ); + + $this->assertSame( 0, $q->post_count ); + + $this->assertTrue( $q->is_category() ); + $this->assertFalse( $q->is_tax() ); + $this->assertFalse( $q->is_category( 'non-existent-category' ) ); + } + + /** + * @ticket 29660 + */ + public function test_query_tag_404_does_not_throw_warning() { + $q = new WP_Query( + array( + 'tag' => 'non-existent-tag', + ) + ); + + $this->assertSame( 0, $q->post_count ); + + $this->assertTrue( $q->is_tag() ); + $this->assertFalse( $q->is_tax() ); + $this->assertFalse( $q->is_tag( 'non-existent-tag' ) ); + } + + /** + * Test if $before_loop is true before loop. + * + * @ticket 58211 + */ + public function test_before_loop_value_set_true_before_the_loop() { + // Get a new query with 3 posts. + $query = $this->get_new_wp_query_with_posts( 3 ); + + $this->assertTrue( $query->before_loop ); + } + + /** + * Test $before_loop value is set to false when the loop starts. + * + * @ticket 58211 + * + * @covers WP_Query::the_post + */ + public function test_before_loop_value_set_to_false_in_loop_with_post() { + // Get a new query with 2 posts. + $query = $this->get_new_wp_query_with_posts( 2 ); + + while ( $query->have_posts() ) { + // $before_loop should be set false as soon as the_post is called for the first time. + $query->the_post(); + + $this->assertFalse( $query->before_loop ); + break; + } + } + + /** + * Test $before_loop value is set to false when there is no post in the loop. + * + * @ticket 58211 + * + * @covers WP_Query::have_posts + */ + public function test_before_loop_set_false_after_loop_with_no_post() { + // New query without any posts in the result. + $query = new WP_Query( + array( + 'category_name' => 'non-existent-category', + ) + ); + + // There will not be any posts, so the loop will never actually enter. + while ( $query->have_posts() ) { + $query->the_post(); + } + + // Still, this should be false as there are no results and entering the loop was attempted. + $this->assertFalse( $query->before_loop ); + } + + /** + * Get a new query with a given number of posts. + * + * @param int $no_of_posts Number of posts to be added in the query. + */ + public function get_new_wp_query_with_posts( $no_of_posts ) { + $post_ids = self::factory()->post->create_many( $no_of_posts ); + $query = new WP_Query( array( 'post__in' => $post_ids ) ); + return $query; + } } diff --git a/tests/phpunit/tests/query/cacheResults.php b/tests/phpunit/tests/query/cacheResults.php new file mode 100644 index 0000000000000..02bb702a2b6aa --- /dev/null +++ b/tests/phpunit/tests/query/cacheResults.php @@ -0,0 +1,2071 @@ +post->create_many( 5 ); + self::$pages = $factory->post->create_many( 5, array( 'post_type' => 'page' ) ); + + self::$t1 = $factory->term->create( + array( + 'taxonomy' => 'category', + 'slug' => 'foo', + 'name' => 'Foo', + ) + ); + + wp_set_post_terms( self::$posts[0], self::$t1, 'category' ); + add_post_meta( self::$posts[0], 'color', '#000000' ); + + // Make a user. + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + } + + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @dataProvider data_query_cache + */ + public function test_generate_cache_key( $args ) { + global $wpdb; + $query1 = new WP_Query(); + $query1->query( $args ); + + $query_vars = $query1->query_vars; + $request = $query1->request; + $request_no_placeholder = $wpdb->remove_placeholder_escape( $request ); + + $this->assertStringNotContainsString( $wpdb->placeholder_escape(), $request_no_placeholder, 'Placeholder escape should be removed from the modified request.' ); + + if ( str_contains( $request, $wpdb->placeholder_escape() ) ) { + self::$sql_placeholder_cache_key_tested = true; + } + + if ( str_contains( serialize( $query_vars ), $wpdb->placeholder_escape() ) ) { + self::$wp_query_placeholder_cache_key_tested = true; + } + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request ); + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request_no_placeholder ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); + } + + /** + * Ensure cache keys tests include WPDB placeholder in SQL Query. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @depends test_generate_cache_key + */ + public function test_sql_placeholder_cache_key_tested() { + $this->assertTrue( self::$sql_placeholder_cache_key_tested, 'Cache key containing WPDB placeholder in SQL query was not tested.' ); + } + + /** + * Ensure cache keys tests include WPDB placeholder in WP_Query arguments. + * + * This test mainly covers the search query which generates the `search_orderby_title` + * query_var in WP_Query. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + * + * @depends test_generate_cache_key + */ + public function test_wp_query_placeholder_cache_key_tested() { + $this->assertTrue( self::$wp_query_placeholder_cache_key_tested, 'Cache key containing WPDB placeholder in WP_Query arguments was not tested.' ); + } + + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 56802 + * + * @covers WP_Query::generate_cache_key + */ + public function test_generate_cache_key_placeholder() { + global $wpdb; + $query1 = new WP_Query(); + $query1->query( array() ); + + $query_vars = $query1->query_vars; + $request = $query1->request; + $query_vars['test']['nest'] = '%'; + $query_vars['test2']['nest']['nest']['nest'] = '%'; + $this->assertStringNotContainsString( $wpdb->placeholder_escape(), serialize( $query_vars ), 'Query vars should not contain the wpdb placeholder.' ); + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request ); + + $query_vars['test']['nest'] = $wpdb->placeholder_escape(); + $query_vars['test2']['nest']['nest']['nest'] = $wpdb->placeholder_escape(); + $this->assertStringContainsString( $wpdb->placeholder_escape(), serialize( $query_vars ), 'Query vars should not contain the wpdb placeholder.' ); + + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); + } + + /** + * @covers WP_Query::generate_cache_key + * @ticket 59442 + */ + public function test_generate_cache_key_unregister_post_type() { + global $wpdb; + register_post_type( + 'wptests_pt', + array( + 'exclude_from_search' => false, + ) + ); + $query_vars = array( + 'post_type' => 'any', + ); + $fields = "{$wpdb->posts}.ID"; + $query1 = new WP_Query( $query_vars ); + $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request ); + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request1 ); + unregister_post_type( 'wptests_pt' ); + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request1 ); + + $this->assertNotSame( $cache_key_1, $cache_key_2, 'Cache key should differ after unregistering post type.' ); + } + + /** + * @ticket 59516 + * + * @covers WP_Query::generate_cache_key + */ + public function test_post_in_order_by_clauses_are_not_normalized() { + global $wpdb; + + $post_ids = self::$posts; + + $query_vars1 = array( + 'post__in' => $post_ids, + 'orderby' => 'post__in', + ); + $query_vars2 = array( + 'post__in' => array_reverse( $post_ids ), + 'orderby' => 'post__in', + ); + + $fields = "{$wpdb->posts}.ID"; + $query1 = new WP_Query( $query_vars1 ); + $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request ); + + $query2 = new WP_Query( $query_vars2 ); + $request2 = str_replace( $fields, "{$wpdb->posts}.*", $query2->request ); + + $reflection_q1 = new ReflectionProperty( $query1, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q1->setAccessible( true ); + } + + $reflection_q2 = new ReflectionProperty( $query2, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q2->setAccessible( true ); + } + + $this->assertNotSame( $request1, $request2, 'Queries should not match' ); + + $cache_key_1 = $reflection_q1->getValue( $query1 ); + $cache_key_2 = $reflection_q2->getValue( $query2 ); + + $this->assertNotSame( $cache_key_1, $cache_key_2, 'Cache key should differ.' ); + $this->assertNotEmpty( $cache_key_1, 'Cache key for query one should not be empty.' ); + $this->assertNotEmpty( $cache_key_2, 'Cache key for query two should not be empty.' ); + + // Test the posts are returned different orders. + $this->assertNotSame( wp_list_pluck( $query1->posts, 'ID' ), wp_list_pluck( $query2->posts, 'ID' ), 'Query one posts should not match the order of query two posts.' ); + // Test the posts are the same sets. + $this->assertSameSets( wp_list_pluck( $query1->posts, 'ID' ), wp_list_pluck( $query2->posts, 'ID' ), 'Query one posts should match the set of query two posts.' ); + } + + /** + * @ticket 59516 + * + * @covers WP_Query::generate_cache_key + */ + public function test_post_parent_in_order_by_clauses_are_not_normalized() { + global $wpdb; + + $parent_pages = self::$pages; + $post_names = array( 'doctor-dillamond', 'elphaba', 'fiyero', 'glinda', 'the-wizard-of-oz' ); + $child_pages = array(); + foreach ( $parent_pages as $key => $parent_page ) { + $child_pages[] = self::factory()->post->create( + array( + 'post_parent' => $parent_page, + 'post_type' => 'page', + 'post_name' => $post_names[ $key ], + ) + ); + } + + $query_vars1 = array( + 'post_parent__in' => $parent_pages, + 'post_type' => 'page', + 'orderby' => 'post_parent__in', + ); + + $query_vars2 = array( + 'post_parent__in' => array_reverse( $parent_pages ), + 'post_type' => 'page', + 'orderby' => 'post_parent__in', + ); + + $fields = "{$wpdb->posts}.ID"; + $query1 = new WP_Query( $query_vars1 ); + $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request ); + + $query2 = new WP_Query( $query_vars2 ); + $request2 = str_replace( $fields, "{$wpdb->posts}.*", $query2->request ); + + $reflection_q1 = new ReflectionProperty( $query1, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q1->setAccessible( true ); + } + + $reflection_q2 = new ReflectionProperty( $query2, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q2->setAccessible( true ); + } + + $this->assertNotSame( $request1, $request2, 'Queries should not match' ); + + $cache_key_1 = $reflection_q1->getValue( $query1 ); + $cache_key_2 = $reflection_q2->getValue( $query2 ); + + $this->assertNotSame( $cache_key_1, $cache_key_2, 'Cache key should differ.' ); + $this->assertNotEmpty( $cache_key_1, 'Cache key for query one should not be empty.' ); + $this->assertNotEmpty( $cache_key_2, 'Cache key for query two should not be empty.' ); + + // Test the posts are returned in the correct order. + $this->assertSame( array( 'doctor-dillamond', 'elphaba', 'fiyero', 'glinda', 'the-wizard-of-oz' ), wp_list_pluck( $query1->posts, 'post_name' ), 'Query one posts should be in alphabetical order' ); + $this->assertSame( array( 'the-wizard-of-oz', 'glinda', 'fiyero', 'elphaba', 'doctor-dillamond' ), wp_list_pluck( $query2->posts, 'post_name' ), 'Query two posts should be in reverse alphabetical order.' ); + // Test the posts are the same sets. + $this->assertSameSets( wp_list_pluck( $query1->posts, 'ID' ), wp_list_pluck( $query2->posts, 'ID' ), 'Query one posts should match the set of query two posts.' ); + } + + /** + * @ticket 59516 + * + * @covers WP_Query::generate_cache_key + */ + public function test_post_name_in_order_by_clauses_are_not_normalized() { + global $wpdb; + $post_names = array( 'doctor-dillamond', 'elphaba', 'glinda', 'the-wizard-of-oz' ); + $posts = array(); + + foreach ( $post_names as $post_name ) { + $posts[] = self::factory()->post->create( + array( + 'post_name' => $post_name, + ) + ); + } + + $query_vars1 = array( + 'post_name__in' => $post_names, + 'orderby' => 'post_name__in', + ); + + $query_vars2 = array( + 'post_name__in' => array_reverse( $post_names ), + 'orderby' => 'post_name__in', + ); + + $fields = "{$wpdb->posts}.ID"; + $query1 = new WP_Query( $query_vars1 ); + $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request ); + + $query2 = new WP_Query( $query_vars2 ); + $request2 = str_replace( $fields, "{$wpdb->posts}.*", $query2->request ); + + $reflection_q1 = new ReflectionProperty( $query1, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q1->setAccessible( true ); + } + + $reflection_q2 = new ReflectionProperty( $query2, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q2->setAccessible( true ); + } + + $this->assertNotSame( $request1, $request2, 'Queries should not match' ); + + $cache_key_1 = $reflection_q1->getValue( $query1 ); + $cache_key_2 = $reflection_q2->getValue( $query2 ); + + $this->assertNotSame( $cache_key_1, $cache_key_2, 'Cache key should differ.' ); + $this->assertNotEmpty( $cache_key_1, 'Cache key for query one should not be empty.' ); + $this->assertNotEmpty( $cache_key_2, 'Cache key for query two should not be empty.' ); + + // Test the posts are returned in the correct order. + $this->assertSame( array( 'doctor-dillamond', 'elphaba', 'glinda', 'the-wizard-of-oz' ), wp_list_pluck( $query1->posts, 'post_name' ), 'Query one posts should be in alphabetical order' ); + $this->assertSame( array( 'the-wizard-of-oz', 'glinda', 'elphaba', 'doctor-dillamond' ), wp_list_pluck( $query2->posts, 'post_name' ), 'Query two posts should be in reverse alphabetical order.' ); + // Test the posts are the same sets. + $this->assertSameSets( wp_list_pluck( $query1->posts, 'ID' ), wp_list_pluck( $query2->posts, 'ID' ), 'Query one posts should match the set of query two posts.' ); + } + + /** + * @ticket 59442 + * @ticket 59516 + * + * @covers WP_Query::generate_cache_key + * + * @dataProvider data_query_cache_duplicate + */ + public function test_generate_cache_key_normalize( $query_vars1, $query_vars2 ) { + global $wpdb; + + $fields = "{$wpdb->posts}.ID"; + $query1 = new WP_Query( $query_vars1 ); + $request1 = str_replace( $fields, "{$wpdb->posts}.*", $query1->request ); + + $query2 = new WP_Query( $query_vars2 ); + $request2 = str_replace( $fields, "{$wpdb->posts}.*", $query2->request ); + + $reflection_q1 = new ReflectionProperty( $query1, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q1->setAccessible( true ); + } + + $reflection_q2 = new ReflectionProperty( $query2, 'query_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_q2->setAccessible( true ); + } + + $this->assertSame( $request1, $request2, 'Queries should match' ); + + $cache_key_1 = $reflection_q1->getValue( $query1 ); + $cache_key_2 = $reflection_q2->getValue( $query2 ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs the same effective parameters.' ); + $this->assertNotEmpty( $cache_key_1, 'Cache key for query one should not be empty.' ); + $this->assertNotEmpty( $cache_key_2, 'Cache key for query two should not be empty.' ); + } + + /** + * @dataProvider data_query_cache + * @ticket 22176 + */ + public function test_query_cache( $args ) { + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + $queries_after = get_num_queries(); + + add_filter( 'split_the_query', '__return_false' ); + $split_query = new WP_Query(); + $split_posts = $split_query->query( $args ); + remove_filter( 'split_the_query', '__return_false' ); + + if ( isset( $args['fields'] ) ) { + if ( 'all' !== $args['fields'] ) { + $this->assertSameSets( $posts1, $posts2, 'Second query produces different set of posts to first.' ); + $this->assertSameSets( $posts1, $split_posts, 'Split query produces different set of posts to first.' ); + } + if ( 'id=>parent' !== $args['fields'] ) { + $this->assertSame( $queries_after, $queries_before, 'Second query produces unexpected DB queries.' ); + } + } else { + $this->assertSame( $queries_after, $queries_before, 'Second query produces unexpected DB queries.' ); + } + $this->assertSame( $query1->found_posts, $query2->found_posts, 'Second query has a different number of found posts to first.' ); + $this->assertSame( $query1->found_posts, $split_query->found_posts, 'Split query has a different number of found posts to first.' ); + $this->assertSame( $query1->max_num_pages, $query2->max_num_pages, 'Second query has a different number of total to first.' ); + $this->assertSame( $query1->max_num_pages, $split_query->max_num_pages, 'Split query has a different number of total to first.' ); + + if ( ! $query1->query_vars['no_found_rows'] ) { + wp_delete_post( self::$posts[0], true ); + wp_delete_post( self::$pages[0], true ); + $query3 = new WP_Query(); + $query3->query( $args ); + + $this->assertNotSame( $query1->found_posts, $query3->found_posts ); + $this->assertNotSame( $queries_after, get_num_queries() ); + } + } + + /** + * Data provider for test_generate_cache_key_normalize(). + * + * @return array[] + */ + public function data_query_cache_duplicate() { + return array( + 'post type empty' => array( + 'query_vars1' => array( 'post_type' => '' ), + 'query_vars2' => array( 'post_type' => 'post' ), + ), + 'post type array' => array( + 'query_vars1' => array( 'post_type' => array( 'page' ) ), + 'query_vars2' => array( 'post_type' => 'page' ), + ), + 'orderby empty' => array( + 'query_vars1' => array( 'orderby' => null ), + 'query_vars2' => array( 'orderby' => 'date' ), + ), + 'different order parameter' => array( + 'query_vars1' => array( + 'post_type' => 'post', + 'posts_per_page' => 15, + ), + 'query_vars2' => array( + 'posts_per_page' => 15, + 'post_type' => 'post', + ), + ), + 'same args' => array( + 'query_vars1' => array( 'post_type' => 'post' ), + 'query_vars2' => array( 'post_type' => 'post' ), + ), + 'same args any' => array( + 'query_vars1' => array( 'post_type' => 'any' ), + 'query_vars2' => array( 'post_type' => 'any' ), + ), + 'any and post types' => array( + 'query_vars1' => array( 'post_type' => 'any' ), + 'query_vars2' => array( 'post_type' => array( 'post', 'page', 'attachment' ) ), + ), + 'different order post type' => array( + 'query_vars1' => array( 'post_type' => array( 'post', 'page' ) ), + 'query_vars2' => array( 'post_type' => array( 'page', 'post' ) ), + ), + 'non-unique post type' => array( + 'query_vars1' => array( 'post_type' => array( 'post', 'page' ) ), + 'query_vars2' => array( 'post_type' => array( 'page', 'post', 'page' ) ), + ), + 'post status array' => array( + 'query_vars1' => array( 'post_status' => 'publish' ), + 'query_vars2' => array( 'post_status' => array( 'publish' ) ), + ), + 'post status order' => array( + 'query_vars1' => array( 'post_status' => array( 'draft', 'publish' ) ), + 'query_vars2' => array( 'post_status' => array( 'publish', 'draft' ) ), + ), + 'non-unique post status' => array( + 'query_vars1' => array( 'post_status' => array( 'draft', 'publish' ) ), + 'query_vars2' => array( 'post_status' => array( 'draft', 'publish', 'draft' ) ), + ), + 'post id int vs string' => array( + 'query_vars1' => array( 'p' => '1' ), + 'query_vars2' => array( 'p' => 1 ), + ), + 'page id int vs string' => array( + 'query_vars1' => array( 'page_id' => '2' ), + 'query_vars2' => array( 'page_id' => 2 ), + ), + 'attachment id int vs string' => array( + 'query_vars1' => array( 'attachment_id' => '3' ), + 'query_vars2' => array( 'attachment_id' => 3 ), + ), + 'date and time values int vs string' => array( + 'query_vars1' => array( + 'year' => '2013', + 'monthnum' => '12', + 'day' => '12', + 'hour' => '12', + 'minute' => '12', + 'second' => '12', + ), + 'query_vars2' => array( + 'year' => 2013, + 'monthnum' => 12, + 'day' => 12, + 'hour' => 12, + 'minute' => 12, + 'second' => 12, + ), + ), + 'offset value int vs string' => array( + 'query_vars1' => array( 'offset' => '5' ), + 'query_vars2' => array( 'offset' => 5 ), + ), + 'posts per page value int vs string' => array( + 'query_vars1' => array( 'posts_per_page' => '5' ), + 'query_vars2' => array( 'posts_per_page' => 5 ), + ), + 'paged value int vs string' => array( + 'query_vars1' => array( 'paged' => '2' ), + 'query_vars2' => array( 'paged' => 2 ), + ), + 'menu_order value int vs string' => array( + 'query_vars1' => array( 'menu_order' => '2' ), + 'query_vars2' => array( 'menu_order' => 2 ), + ), + 'post__in different order' => array( + 'query_vars1' => array( 'post__in' => array( 1, 2, 3, 4, 5 ) ), + 'query_vars2' => array( 'post__in' => array( 5, 4, 3, 2, 1 ) ), + ), + 'post__in non-unique' => array( + 'query_vars1' => array( 'post__in' => array( 1, 2, 3, 4, 5 ) ), + 'query_vars2' => array( 'post__in' => array( 1, 2, 3, 4, 5, 1, 2, 3 ) ), + ), + 'post_parent__in different order' => array( + 'query_vars1' => array( 'post_parent__in' => array( 1, 2, 3, 4, 5 ) ), + 'query_vars2' => array( 'post_parent__in' => array( 5, 4, 3, 2, 1 ) ), + ), + 'post_parent__in non-unique' => array( + 'query_vars1' => array( 'post_parent__in' => array( 1, 2, 3, 4, 5 ) ), + 'query_vars2' => array( 'post_parent__in' => array( 1, 2, 3, 4, 5, 1, 2, 3 ) ), + ), + 'post_name__in different order' => array( + 'query_vars1' => array( 'post_name__in' => array( 'elphaba', 'glinda', 'the-wizard-of-oz', 'doctor-dillamond' ) ), + 'query_vars2' => array( 'post_name__in' => array( 'doctor-dillamond', 'elphaba', 'the-wizard-of-oz', 'glinda' ) ), + ), + 'post_name__in non-unique' => array( + 'query_vars1' => array( 'post_name__in' => array( 'elphaba', 'glinda', 'the-wizard-of-oz', 'doctor-dillamond' ) ), + 'query_vars2' => array( 'post_name__in' => array( 'elphaba', 'glinda', 'elphaba', 'glinda', 'the-wizard-of-oz', 'doctor-dillamond' ) ), + ), + 'cat different order (array)' => array( + 'query_vars_1' => array( 'cat' => array( '1', '2' ) ), + 'query_vars_2' => array( 'cat' => array( '2', '1' ) ), + ), + 'cat different order (string)' => array( + 'query_vars_1' => array( 'cat' => '2,1' ), + 'query_vars_2' => array( 'cat' => '1,2' ), + ), + 'cat queries int vs string' => array( + 'query_vars_1' => array( 'cat' => '2' ), + 'query_vars_2' => array( 'cat' => 2 ), + ), + 'category__in queries different order (array)' => array( + 'query_vars_1' => array( 'category__in' => array( '1', '2' ) ), + 'query_vars_2' => array( 'category__in' => array( '2', '1' ) ), + ), + 'category__in queries with non-unique array' => array( + 'query_vars_1' => array( 'category__in' => array( '1', '1' ) ), + 'query_vars_2' => array( 'category__in' => array( '1' ) ), + ), + 'category__in queries string vs array (array)' => array( + 'query_vars_1' => array( 'category__in' => array( '1' ) ), + 'query_vars_2' => array( 'category__in' => array( 1 ) ), + ), + 'category__not_in different order (array)' => array( + 'query_vars_1' => array( 'category__not_in' => array( '1', '2' ) ), + 'query_vars_2' => array( 'category__not_in' => array( '2', '1' ) ), + ), + 'category__not_in with non-unique array' => array( + 'query_vars_1' => array( 'category__not_in' => array( '1', '1' ) ), + 'query_vars_2' => array( 'category__not_in' => array( '1' ) ), + ), + 'category__not_in queries string vs array (array)' => array( + 'query_vars_1' => array( 'category__not_in' => array( '1' ) ), + 'query_vars_2' => array( 'category__not_in' => array( 1 ) ), + ), + 'category__and queries width different order (array)' => array( + 'query_vars_1' => array( 'category__and' => array( '1', '2' ) ), + 'query_vars_2' => array( 'category__and' => array( '2', '1' ) ), + ), + 'category__and with non-unique array' => array( + 'query_vars_1' => array( 'category__and' => array( '1', '1', '2' ) ), + 'query_vars_2' => array( 'category__and' => array( '1', '2' ) ), + ), + 'category__and queries string vs array (array)' => array( + 'query_vars_1' => array( 'category__and' => array( '1', '2' ) ), + 'query_vars_2' => array( 'category__and' => array( 1, 2 ) ), + ), + 'author queries different order (string)' => array( + 'query_vars_1' => array( 'author' => '1,2' ), + 'query_vars_2' => array( 'author' => '2,1' ), + ), + 'author with non-unique string' => array( + 'query_vars_1' => array( 'author' => '1,1' ), + 'query_vars_2' => array( 'author' => '1' ), + ), + 'author queries int vs string (string)' => array( + 'query_vars_1' => array( 'author' => 1 ), + 'query_vars_2' => array( 'author' => '1' ), + ), + 'author queries int vs string (array)' => array( + 'query_vars_1' => array( 'author' => array( 1 ) ), + 'query_vars_2' => array( 'author' => array( '1' ) ), + ), + 'author__in different order' => array( + 'query_vars_1' => array( 'author__in' => array( 1, 2 ) ), + 'query_vars_2' => array( 'author__in' => array( 2, 1 ) ), + ), + 'author__in with non-unique array' => array( + 'query_vars_1' => array( 'author__in' => array( 1, 1, 2 ) ), + 'query_vars_2' => array( 'author__in' => array( 1, 2 ) ), + ), + 'author__in queries int vs string (array)' => array( + 'query_vars_1' => array( 'author__in' => array( 1 ) ), + 'query_vars_2' => array( 'author__in' => array( '1' ) ), + ), + 'author__not_in different order (array)' => array( + 'query_vars_1' => array( 'author__not_in' => array( 1, 2 ) ), + 'query_vars_2' => array( 'author__not_in' => array( 2, 1 ) ), + ), + 'author__not_in queries int vs string (array)' => array( + 'query_vars_1' => array( 'author__not_in' => array( 1 ) ), + 'query_vars_2' => array( 'author__not_in' => array( '1' ) ), + ), + 'tag_slug__in order' => array( + 'query_vars_1' => array( 'tag_slug__in' => array( 'foo', 'bar' ) ), + 'query_vars_2' => array( 'tag_slug__in' => array( 'bar', 'foo' ) ), + ), + 'tag_slug__in non-unique vs unique' => array( + 'query_vars_1' => array( 'tag_slug__in' => array( 'foo', 'bar', 'bar' ) ), + 'query_vars_2' => array( 'tag_slug__in' => array( 'foo', 'bar' ) ), + ), + 'tag_slug__and order' => array( + 'query_vars_1' => array( 'tag_slug__and' => array( 'foo', 'bar' ) ), + 'query_vars_2' => array( 'tag_slug__and' => array( 'bar', 'foo' ) ), + ), + 'tag_slug__and non-unique' => array( + 'query_vars_1' => array( 'tag_slug__and' => array( 'foo', 'bar', 'foo' ) ), + 'query_vars_2' => array( 'tag_slug__and' => array( 'bar', 'foo' ) ), + ), + 'tag__in queries different order (array)' => array( + 'query_vars_1' => array( 'tag__in' => array( 1, 2 ) ), + 'query_vars_2' => array( 'tag__in' => array( 2, 1 ) ), + ), + 'tag__in queries non-unique array' => array( + 'query_vars_1' => array( 'tag__in' => array( 1, 2, 1 ) ), + 'query_vars_2' => array( 'tag__in' => array( 2, 1 ) ), + ), + 'tag__in queries int vs string' => array( + 'query_vars_1' => array( 'tag__in' => array( 2, 1 ) ), + 'query_vars_2' => array( 'tag__in' => array( '2', '1' ) ), + ), + 'tag__and queries different order (array)' => array( + 'query_vars_1' => array( 'tag__and' => array( 1, 2 ) ), + 'query_vars_2' => array( 'tag__and' => array( 2, 1 ) ), + ), + 'tag__and queries non-unique array' => array( + 'query_vars_1' => array( 'tag__and' => array( 1, 2, 2 ) ), + 'query_vars_2' => array( 'tag__and' => array( 2, 1 ) ), + ), + 'tag__not_in queries different order (array)' => array( + 'query_vars_1' => array( 'tag__not_in' => array( 1, 2 ) ), + 'query_vars_2' => array( 'tag__not_in' => array( 2, 1 ) ), + ), + 'tag__not_in queries non-unique array' => array( + 'query_vars_1' => array( 'tag__not_in' => array( 1, 2, 2 ) ), + 'query_vars_2' => array( 'tag__not_in' => array( 1, 2 ) ), + ), + 'tag__not_in queries int vs string (array)' => array( + 'query_vars_1' => array( 'tag__not_in' => array( '1' ) ), + 'query_vars_2' => array( 'tag__not_in' => array( 1 ) ), + ), + 'cache parameters' => array( + 'query_vars1' => array( + 'update_post_meta_cache' => true, + 'update_post_term_cache' => true, + 'update_menu_item_cache' => true, + ), + 'query_vars2' => array( + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'update_menu_item_cache' => false, + ), + ), + ); + } + + /** + * Data provider. + * + * @return array[] Test parameters. + */ + public function data_query_cache() { + return array( + 'cache true' => array( + 'args' => array( + 'cache_results' => true, + ), + ), + 'cache true and pagination' => array( + 'args' => array( + 'cache_results' => true, + 'posts_per_page' => 3, + 'page' => 2, + ), + ), + 'cache true and no pagination' => array( + 'args' => array( + 'cache_results' => true, + 'nopaging' => true, + ), + ), + 'cache true and post type any' => array( + 'args' => array( + 'cache_results' => true, + 'nopaging' => true, + 'post_type' => 'any', + ), + ), + 'cache true and get all' => array( + 'args' => array( + 'cache_results' => true, + 'fields' => 'all', + 'posts_per_page' => -1, + 'post_status' => 'any', + 'post_type' => 'any', + ), + ), + 'cache true and page' => array( + 'args' => array( + 'cache_results' => true, + 'post_type' => 'page', + ), + ), + 'cache true and empty post type' => array( + 'args' => array( + 'cache_results' => true, + 'post_type' => '', + ), + ), + 'cache true and orderby null' => array( + 'args' => array( + 'cache_results' => true, + 'orderby' => null, + ), + ), + 'cache true and ids' => array( + 'args' => array( + 'cache_results' => true, + 'fields' => 'ids', + ), + ), + 'cache true and id=>parent and no found rows' => array( + 'args' => array( + 'cache_results' => true, + 'fields' => 'id=>parent', + ), + ), + 'cache true and ids and no found rows' => array( + 'args' => array( + 'no_found_rows' => true, + 'cache_results' => true, + 'fields' => 'ids', + ), + ), + 'cache true and id=>parent' => array( + 'args' => array( + 'no_found_rows' => true, + 'cache_results' => true, + 'fields' => 'id=>parent', + ), + ), + 'cache and ignore_sticky_posts' => array( + 'args' => array( + 'cache_results' => true, + 'ignore_sticky_posts' => true, + ), + ), + 'cache meta query' => array( + 'args' => array( + 'cache_results' => true, + 'meta_query' => array( + array( + 'key' => 'color', + ), + ), + ), + ), + 'cache meta query search' => array( + 'args' => array( + 'cache_results' => true, + 'meta_query' => array( + array( + 'key' => 'color', + 'value' => '00', + 'compare' => 'LIKE', + ), + ), + ), + ), + 'cache nested meta query search' => array( + 'args' => array( + 'cache_results' => true, + 'meta_query' => array( + 'relation' => 'AND', + array( + 'key' => 'color', + 'value' => '00', + 'compare' => 'LIKE', + ), + array( + 'relation' => 'OR', + array( + 'key' => 'color', + 'value' => '00', + 'compare' => 'LIKE', + ), + array( + 'relation' => 'AND', + array( + 'key' => 'wp_test_suite', + 'value' => '56802', + 'compare' => 'LIKE', + ), + array( + 'key' => 'wp_test_suite_too', + 'value' => '56802', + 'compare' => 'LIKE', + ), + ), + ), + ), + ), + ), + 'cache meta query not search' => array( + 'args' => array( + 'cache_results' => true, + 'meta_query' => array( + array( + 'key' => 'color', + 'value' => 'ff', + 'compare' => 'NOT LIKE', + ), + ), + ), + ), + 'cache comment_count' => array( + 'args' => array( + 'cache_results' => true, + 'comment_count' => 0, + ), + ), + 'cache term query' => array( + 'args' => array( + 'cache_results' => true, + 'tax_query' => array( + array( + 'taxonomy' => 'category', + 'terms' => array( 'foo' ), + 'field' => 'slug', + ), + ), + ), + ), + 'cache search query' => array( + 'args' => array( + 'cache_results' => true, + 's' => 'title', + ), + ), + 'cache search query multiple terms' => array( + 'args' => array( + 'cache_results' => true, + 's' => 'Post title', + ), + ), + ); + } + + /** + * @ticket 22176 + */ + public function test_seeded_random_queries_only_cache_post_objects() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'orderby' => 'rand(6)', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + $queries_before = get_num_queries(); + + $query2 = new WP_Query(); + $query2->query( $args ); + + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after ); + } + + /** + * @ticket 22176 + */ + public function test_unseeded_random_queries_only_cache_post_objects() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'orderby' => 'rand', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + $queries_before = get_num_queries(); + + $query2 = new WP_Query(); + $query2->query( $args ); + + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_filter_request() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + $queries_before = get_num_queries(); + + add_filter( 'posts_request', array( $this, 'filter_posts_request' ) ); + + $query2 = new WP_Query(); + $query2->query( $args ); + + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_no_caching() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + $queries_before = get_num_queries(); + + $query2 = new WP_Query(); + $args['cache_results'] = false; + $query2->query( $args ); + + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after ); + } + + public function filter_posts_request( $request ) { + return $request . ' -- Add comment'; + } + + /** + * @ticket 22176 + */ + public function test_query_cache_new_post() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + $p1 = self::factory()->post->create(); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_main_query_sticky_posts_change() { + add_action( 'parse_query', array( $this, 'set_cache_results' ) ); + update_option( 'posts_per_page', 5 ); + + $old_date = date_create( '-25 hours' ); + $old_post = self::factory()->post->create( array( 'post_date' => $old_date->format( 'Y-m-d H:i:s' ) ) ); + + // Post is unstuck. + $this->go_to( '/' ); + $unstuck = $GLOBALS['wp_query']->posts; + $unstuck_ids = wp_list_pluck( $unstuck, 'ID' ); + + $expected = array_reverse( self::$posts ); + $this->assertSame( $expected, $unstuck_ids ); + + // Stick the post. + stick_post( $old_post ); + + $this->go_to( '/' ); + $stuck = $GLOBALS['wp_query']->posts; + $stuck_ids = wp_list_pluck( $stuck, 'ID' ); + + $expected = array_reverse( self::$posts ); + array_unshift( $expected, $old_post ); + + $this->assertSame( $expected, $stuck_ids ); + } + + /** + * @ticket 22176 + */ + public function test_main_query_in_query_sticky_posts_change() { + add_action( 'parse_query', array( $this, 'set_cache_results' ) ); + update_option( 'posts_per_page', 5 ); + + $middle_post = self::$posts[2]; + + // Post is unstuck. + $this->go_to( '/' ); + $unstuck = $GLOBALS['wp_query']->posts; + $unstuck_ids = wp_list_pluck( $unstuck, 'ID' ); + + $expected = array_reverse( self::$posts ); + $this->assertSame( $expected, $unstuck_ids ); + + // Stick the post. + stick_post( $middle_post ); + + $this->go_to( '/' ); + $stuck = $GLOBALS['wp_query']->posts; + $stuck_ids = wp_list_pluck( $stuck, 'ID' ); + + $expected = array_diff( array_reverse( self::$posts ), array( $middle_post ) ); + array_unshift( $expected, $middle_post ); + + $this->assertSame( $expected, $stuck_ids ); + } + + /** + * @ticket 22176 + */ + public function test_query_sticky_posts_change() { + add_action( 'parse_query', array( $this, 'set_cache_results' ) ); + + $old_date = date_create( '-25 hours' ); + $old_post = self::factory()->post->create( array( 'post_date' => $old_date->format( 'Y-m-d H:i:s' ) ) ); + + // Post is unstuck. + $unstuck = new WP_Query( array( 'posts_per_page' => 5 ) ); + $unstuck_ids = wp_list_pluck( $unstuck->posts, 'ID' ); + + $expected = array_reverse( self::$posts ); + + $this->assertSame( $expected, $unstuck_ids ); + + // Stick the post. + stick_post( $old_post ); + + $stuck = new WP_Query( array( 'posts_per_page' => 5 ) ); + $stuck_ids = wp_list_pluck( $stuck->posts, 'ID' ); + + $expected = array_reverse( self::$posts ); + array_unshift( $expected, $old_post ); + + $this->assertSame( $expected, $stuck_ids ); + + // Ignore sticky posts. + $ignore_stuck = new WP_Query( + array( + 'posts_per_page' => 5, + 'ignore_sticky_posts' => true, + ) + ); + $ignore_stuck_ids = wp_list_pluck( $ignore_stuck->posts, 'ID' ); + + $expected = array_reverse( self::$posts ); + + $this->assertSame( $expected, $ignore_stuck_ids ); + + // Just to make sure everything has changed. + $this->assertNotSame( $unstuck, $stuck ); + } + + /** + * @ticket 22176 + */ + public function test_query_in_query_sticky_posts_change() { + add_action( 'parse_query', array( $this, 'set_cache_results' ) ); + + $middle_post = self::$posts[2]; + + // Post is unstuck. + $unstuck = new WP_Query( array( 'posts_per_page' => 5 ) ); + $unstuck_ids = wp_list_pluck( $unstuck->posts, 'ID' ); + + $expected = array_reverse( self::$posts ); + + $this->assertSame( $expected, $unstuck_ids ); + + // Stick the post. + stick_post( $middle_post ); + + $stuck = new WP_Query( array( 'posts_per_page' => 5 ) ); + $stuck_ids = wp_list_pluck( $stuck->posts, 'ID' ); + + $expected = array_diff( array_reverse( self::$posts ), array( $middle_post ) ); + array_unshift( $expected, $middle_post ); + + $this->assertSame( $expected, $stuck_ids ); + + // Ignore sticky posts. + $ignore_stuck = new WP_Query( + array( + 'posts_per_page' => 5, + 'ignore_sticky_posts' => true, + ) + ); + $ignore_stuck_ids = wp_list_pluck( $ignore_stuck->posts, 'ID' ); + + $expected = array_reverse( self::$posts ); + + $this->assertSame( $expected, $ignore_stuck_ids ); + + // Just to make sure everything has changed. + $this->assertNotSame( $unstuck, $stuck ); + } + + public function set_cache_results( $q ) { + $q->set( 'cache_results', true ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_different_args() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'suppress_filters' => true, + 'cache_results' => true, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'lazy_load_term_meta' => false, + ); + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + $queries_after = get_num_queries(); + + $this->assertSame( $queries_before, $queries_after ); + $this->assertSame( $posts1, $posts2 ); + $this->assertSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_different_fields() { + $args = array( + 'cache_results' => true, + 'fields' => 'all', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + + $args = array( + 'cache_results' => true, + 'fields' => 'id=>parent', + ); + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $query2->query( $args ); + $queries_after = get_num_queries(); + + $this->assertSame( 1, $queries_after - $queries_before ); + $this->assertCount( 5, $query1->posts ); + $this->assertCount( 5, $query2->posts ); + $this->assertSame( $query1->found_posts, $query2->found_posts ); + + /* + * Make sure the returned post objects differ due to the field argument. + * + * This uses assertNotEquals rather than assertNotSame as the former is + * agnostic to the instance ID of objects, whereas the latter will take + * it in to account. The test needs to discard the instance ID when + * confirming inequality. + */ + $this->assertNotEquals( $query1->posts, $query2->posts ); + } + + + /** + * @ticket 59188 + */ + public function test_query_cache_unprimed_parents() { + $args = array( + 'cache_results' => true, + 'fields' => 'id=>parent', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + + $post_ids = wp_list_pluck( $query1->posts, 'ID' ); + $cache_keys = array_map( + function ( $post_id ) { + return "post_parent:{$post_id}"; + }, + $post_ids + ); + + wp_cache_delete_multiple( $cache_keys, 'posts' ); + + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $query2->query( $args ); + $queries_after = get_num_queries(); + + $this->assertSame( 1, $queries_after - $queries_before, 'There should be only one query to prime parents' ); + $this->assertCount( 5, $query1->posts, 'There should be only 5 posts returned on first query' ); + $this->assertCount( 5, $query2->posts, 'There should be only 5 posts returned on second query' ); + $this->assertSame( $query1->found_posts, $query2->found_posts, 'Found posts should match on second query' ); + } + + /** + * @ticket 59188 + */ + public function test_query_cache_update_parent() { + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_parent' => self::$pages[0], + ) + ); + $args = array( + 'cache_results' => true, + 'post_type' => 'page', + 'fields' => 'id=>parent', + 'post__in' => array( + $page_id, + ), + ); + $query1 = new WP_Query(); + $query1->query( $args ); + + wp_update_post( + array( + 'ID' => $page_id, + 'post_parent' => self::$pages[1], + ) + ); + + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $query2->query( $args ); + $queries_after = get_num_queries(); + + $this->assertSame( self::$pages[0], $query1->posts[0]->post_parent, 'Check post parent on first query' ); + $this->assertSame( self::$pages[1], $query2->posts[0]->post_parent, 'Check post parent on second query' ); + $this->assertSame( 2, $queries_after - $queries_before, 'There should be 2 queries, one for id=>parent' ); + $this->assertSame( $query1->found_posts, $query2->found_posts, 'Found posts should match on second query' ); + } + + /** + * @ticket 59188 + */ + public function test_query_cache_delete_parent() { + $parent_page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + ) + ); + $page_id = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_parent' => $parent_page_id, + ) + ); + $args = array( + 'cache_results' => true, + 'post_type' => 'page', + 'fields' => 'id=>parent', + 'post__in' => array( + $page_id, + ), + ); + $query1 = new WP_Query(); + $query1->query( $args ); + + wp_delete_post( $parent_page_id, true ); + + $queries_before = get_num_queries(); + $query2 = new WP_Query(); + $query2->query( $args ); + $queries_after = get_num_queries(); + + $this->assertSame( $parent_page_id, $query1->posts[0]->post_parent, 'Check post parent on first query' ); + $this->assertSame( 0, $query2->posts[0]->post_parent, 'Check post parent on second query' ); + $this->assertSame( 2, $queries_after - $queries_before, 'There should be 2 queries, one for id=>parent' ); + $this->assertSame( $query1->found_posts, $query2->found_posts, 'Found posts should match on second query' ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_logged_in() { + $user_id = self::$author_id; + + self::factory()->post->create( + array( + 'post_status' => 'private', + 'post_author' => $user_id, + ) + ); + + $args = array( + 'cache_results' => true, + 'author' => $user_id, + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_set_current_user( $user_id ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + $this->assertEmpty( $posts1 ); + $this->assertNotSame( $posts1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_logged_in_password() { + $user_id = self::$author_id; + self::factory()->post->create( + array( + 'post_title' => 'foo', + 'post_password' => 'password', + 'post_author' => $user_id, + ) + ); + + $args = array( + 'cache_results' => true, + 's' => 'foo', + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_set_current_user( $user_id ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + $this->assertEmpty( $posts1 ); + $this->assertNotSame( $posts1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_new_comment() { + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'comment_count' => 1, + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + self::factory()->comment->create( array( 'comment_post_ID' => self::$posts[0] ) ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( self::$posts[0], $posts2 ); + $this->assertNotEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_main_comments_feed_includes_attachment_comments() { + $attachment_id = self::factory()->post->create( array( 'post_type' => 'attachment' ) ); + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $attachment_id, + 'comment_approved' => '1', + ) + ); + + $args = array( + 'cache_results' => true, + 'withcomments' => 1, + 'feed' => 'feed', + ); + $query1 = new WP_Query(); + $query1->query( $args ); + + $query2 = new WP_Query(); + $query2->query( $args ); + + $this->assertTrue( $query1->have_comments() ); + $this->assertTrue( $query2->have_comments() ); + + $feed_comment = $query1->next_comment(); + $this->assertEquals( $comment_id, $feed_comment->comment_ID ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_delete_comment() { + $comment_id = self::factory()->comment->create( array( 'comment_post_ID' => self::$posts[0] ) ); + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'comment_count' => 1, + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_delete_comment( $comment_id, true ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_update_post() { + $p1 = self::$posts[0]; + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_update_post( + array( + 'ID' => $p1, + 'post_status' => 'draft', + ) + ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts1 ); + $this->assertNotContains( $p1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_new_meta() { + $p1 = self::$posts[1]; // Post 0 already has a color meta value. + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'color', + ), + ), + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + add_post_meta( $p1, 'color', 'black' ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_update_meta() { + // Posts[0] already has a color meta value set to #000000. + $p1 = self::$posts[0]; + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'color', + 'value' => '#000000', + ), + ), + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + update_post_meta( $p1, 'color', 'blue' ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts1 ); + $this->assertEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + + /** + * @ticket 22176 + */ + public function test_query_cache_delete_attachment() { + $p1 = self::factory()->post->create( + array( + 'post_type' => 'attachment', + 'post_status' => 'inherit', + ) + ); + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'post_type' => 'attachment', + 'post_status' => 'inherit', + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_delete_attachment( $p1 ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts1 ); + $this->assertEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_delete_meta() { + // Post 0 already has a color meta value. + $p1 = self::$posts[1]; + add_post_meta( $p1, 'color', 'black' ); + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'meta_query' => array( + array( + 'key' => 'color', + ), + ), + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + delete_post_meta( $p1, 'color' ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts1 ); + $this->assertNotEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_new_term() { + // Post 0 already has the category foo. + $p1 = self::$posts[1]; + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'category', + 'terms' => array( 'foo' ), + 'field' => 'slug', + ), + ), + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_set_post_terms( $p1, array( self::$t1 ), 'category' ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_delete_term() { + // Post 0 already has the category foo. + $p1 = self::$posts[1]; + register_taxonomy( 'wptests_tax1', 'post' ); + + $t1 = self::factory()->term->create( array( 'taxonomy' => 'wptests_tax1' ) ); + + wp_set_object_terms( $p1, array( $t1 ), 'wptests_tax1' ); + + $args = array( + 'cache_results' => true, + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax1', + 'terms' => array( $t1 ), + 'field' => 'term_id', + ), + ), + ); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + + wp_delete_term( $t1, 'wptests_tax1' ); + + $query2 = new WP_Query(); + $posts2 = $query2->query( $args ); + + $this->assertNotSame( $posts1, $posts2 ); + $this->assertContains( $p1, $posts1 ); + $this->assertEmpty( $posts2 ); + $this->assertNotSame( $query1->found_posts, $query2->found_posts ); + } + + /** + * @ticket 58599 + */ + public function test_query_posts_fields_request() { + global $wpdb; + + $args = array( + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'no_found_rows' => true, + ); + + add_filter( 'posts_fields_request', array( $this, 'filter_posts_fields_request' ) ); + + $before = get_num_queries(); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + $after = get_num_queries(); + + foreach ( $posts1 as $_post ) { + $this->assertNotSame( get_post( $_post->ID )->post_content, $_post->post_content ); + } + + $this->assertSame( 2, $after - $before, 'There should only be 2 queries run, one for request and one prime post objects.' ); + + $this->assertStringContainsString( + "SELECT $wpdb->posts.*", + $wpdb->last_query, + 'Check that _prime_post_caches is called.' + ); + } + + public function filter_posts_fields_request( $fields ) { + global $wpdb; + return "{$wpdb->posts}.ID"; + } + + /** + * @ticket 58599 + * @dataProvider data_query_filter_posts_results + */ + public function test_query_filter_posts_results( $filter ) { + global $wpdb; + + $args = array( + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'no_found_rows' => true, + ); + + add_filter( $filter, array( $this, 'filter_posts_results' ) ); + + $before = get_num_queries(); + $query1 = new WP_Query(); + $posts1 = $query1->query( $args ); + $after = get_num_queries(); + + $this->assertCount( 1, $posts1 ); + + $this->assertSame( 2, $after - $before, 'There should only be 2 queries run, one for request and one prime post objects.' ); + + $this->assertStringContainsString( + "SELECT $wpdb->posts.*", + $wpdb->last_query, + 'Check that _prime_post_caches is called.' + ); + } + + public function filter_posts_results() { + return array( get_post( self::$posts[0] ) ); + } + + public function data_query_filter_posts_results() { + return array( + array( 'posts_results' ), + array( 'the_posts' ), + ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_should_exclude_post_with_excluded_term() { + $term_id = self::$t1; + // Post 0 has the term applied. + $post_id = self::$posts[0]; + + $args = array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'category', + 'terms' => array( $term_id ), + 'operator' => 'NOT IN', + ), + ), + ); + + $post_ids_q1 = get_posts( $args ); + $this->assertNotContains( $post_id, $post_ids_q1, 'First query includes the post ID.' ); + + $num_queries = get_num_queries(); + $post_ids_q2 = get_posts( $args ); + $this->assertNotContains( $post_id, $post_ids_q2, 'Second query includes the post ID.' ); + + $this->assertSame( $num_queries, get_num_queries(), 'Second query is not cached.' ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_should_exclude_post_when_excluded_term_is_added_after_caching() { + $term_id = self::$t1; + // Post 1 does not have the term applied. + $post_id = self::$posts[1]; + + $args = array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'category', + 'terms' => array( $term_id ), + 'operator' => 'NOT IN', + ), + ), + ); + + $post_ids_q1 = get_posts( $args ); + $this->assertContains( $post_id, $post_ids_q1, 'First query does not include the post ID.' ); + + wp_set_object_terms( $post_id, array( $term_id ), 'category' ); + + $num_queries = get_num_queries(); + $post_ids_q2 = get_posts( $args ); + $this->assertNotContains( $post_id, $post_ids_q2, 'Second query includes the post ID.' ); + $this->assertNotSame( $num_queries, get_num_queries(), 'Applying term does not invalidate previous cache.' ); + } + + /** + * @ticket 22176 + */ + public function test_query_cache_should_not_exclude_post_when_excluded_term_is_removed_after_caching() { + $term_id = self::$t1; + // Post 0 has the term applied. + $post_id = self::$posts[0]; + + $args = array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'category', + 'terms' => array( $term_id ), + 'operator' => 'NOT IN', + ), + ), + ); + + $post_ids_q1 = get_posts( $args ); + $this->assertNotContains( $post_id, $post_ids_q1, 'First query includes the post ID.' ); + + // Clear the post of terms. + wp_set_object_terms( $post_id, array(), 'category' ); + + $num_queries = get_num_queries(); + $post_ids_q2 = get_posts( $args ); + $this->assertContains( $post_id, $post_ids_q2, 'Second query does not include the post ID.' ); + $this->assertNotSame( $num_queries, get_num_queries(), 'Removing term does not invalidate previous cache.' ); + } + + /** + * @ticket 22176 + * @dataProvider data_query_cache_with_empty_result_set + */ + public function test_query_cache_with_empty_result_set( $fields_q1, $fields_q2 ) { + _delete_all_posts(); + + $args_q1 = array( + 'fields' => $fields_q1, + ); + + $query_1 = new WP_Query(); + $posts_q1 = $query_1->query( $args_q1 ); + $this->assertEmpty( $posts_q1, 'First query does not return an empty result set.' ); + + $args_q2 = array( + 'fields' => $fields_q2, + ); + + $num_queries = get_num_queries(); + $query_2 = new WP_Query(); + $posts_q2 = $query_2->query( $args_q2 ); + $this->assertEmpty( $posts_q2, 'Second query does not return an empty result set.' ); + $this->assertSame( $num_queries, get_num_queries(), 'Second query is not cached.' ); + } + + public function data_query_cache_with_empty_result_set() { + return array( + array( '', '' ), + array( '', 'ids' ), + array( '', 'id=>parent' ), + + array( 'ids', '' ), + array( 'ids', 'ids' ), + array( 'ids', 'id=>parent' ), + + array( 'id=>parent', '' ), + array( 'id=>parent', 'ids' ), + array( 'id=>parent', 'id=>parent' ), + ); + } + + /** + * Ensure starting the loop warms the author cache. + * + * @since 6.1.1 + * @ticket 56948 + * @ticket 56992 + * + * @covers WP_Query::the_post + * + * @dataProvider data_author_cache_warmed_by_the_loop + * + * @param string $fields Query fields. + */ + public function test_author_cache_warmed_by_the_loop( $fields ) { + // Update post author for the parent post. + self::factory()->post->update_object( self::$pages[0], array( 'post_author' => self::$author_id ) ); + + self::factory()->post->create( + array( + 'post_author' => self::$author_id, + 'post_parent' => self::$pages[0], + 'post_type' => 'page', + ) + ); + + $query_1 = new WP_Query( + array( + 'post_type' => 'page', + 'fields' => $fields, + 'author' => self::$author_id, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + ) + ); + + // Start the loop. + $start_loop_queries = get_num_queries(); + $query_1->the_post(); + $num_loop_queries = get_num_queries() - $start_loop_queries; + /* + * Two expected queries: + * 1: User meta data, + * 2: User data. + */ + $this->assertSame( 2, $num_loop_queries, 'Unexpected number of queries while initializing the loop.' ); + + $start_author_queries = get_num_queries(); + get_user_by( 'ID', self::$author_id ); + $num_author_queries = get_num_queries() - $start_author_queries; + $this->assertSame( 0, $num_author_queries, 'Author cache is not warmed by the loop.' ); + } + + /** + * Data provider for test_author_cache_warmed_by_the_loop + * + * @return array[] + */ + public function data_author_cache_warmed_by_the_loop() { + return array( + 'fields: empty' => array( '' ), + 'fields: all' => array( 'all' ), + 'fields: ids' => array( 'ids' ), + 'fields: id=>parent' => array( 'id=>parent' ), + ); + } + + /** + * Ensure lazy loading term meta queries all term meta in a single query. + * + * @since 6.2.0 + * + * @ticket 57163 + * @ticket 22176 + */ + public function test_get_post_meta_lazy_loads_all_term_meta_data() { + $query = new WP_Query(); + + $t2 = $this->factory()->term->create( + array( + 'taxonomy' => 'category', + 'slug' => 'bar', + 'name' => 'Bar', + ) + ); + + wp_set_post_terms( self::$posts[0], $t2, 'category', true ); + // Clean data added to cache by factory and setting terms. + clean_term_cache( array( self::$t1, $t2 ), 'category' ); + clean_post_cache( self::$posts[0] ); + + $num_queries_start = get_num_queries(); + $query_posts = $query->query( + array( + 'lazy_load_term_meta' => true, + 'no_found_rows' => true, + ) + ); + $num_queries = get_num_queries() - $num_queries_start; + + /* + * Four expected queries: + * 1: Post IDs + * 2: Post data + * 3: Post meta data. + * 4: Post term data. + */ + $this->assertSame( 4, $num_queries, 'Unexpected number of queries while querying posts.' ); + $this->assertNotEmpty( $query_posts, 'Query posts is empty.' ); + + $num_queries_start = get_num_queries(); + get_term_meta( self::$t1 ); + $num_queries = get_num_queries() - $num_queries_start; + + /* + * One expected query: + * 1: Term meta data. + */ + $this->assertSame( 1, $num_queries, 'Unexpected number of queries during first query of term meta.' ); + + $num_queries_start = get_num_queries(); + get_term_meta( $t2 ); + $num_queries = get_num_queries() - $num_queries_start; + + // No additional queries expected. + $this->assertSame( 0, $num_queries, 'Unexpected number of queries during second query of term meta.' ); + } +} diff --git a/tests/phpunit/tests/query/commentCount.php b/tests/phpunit/tests/query/commentCount.php index 5c8bd2df26ac6..d995c4b4223cf 100644 --- a/tests/phpunit/tests/query/commentCount.php +++ b/tests/phpunit/tests/query/commentCount.php @@ -21,7 +21,7 @@ public function tear_down() { public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { $post_id = $factory->post->create( array( - 'post_content' => 1 . rand_str() . ' about', + 'post_content' => '1 about', 'post_type' => self::$post_type, ) ); @@ -30,7 +30,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { $post_id = $factory->post->create( array( - 'post_content' => 1 . rand_str() . ' about', + 'post_content' => '2 about', 'post_type' => self::$post_type, ) ); @@ -41,7 +41,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { $post_id = $factory->post->create( array( - 'post_content' => 1 . rand_str() . ' about', + 'post_content' => '3 about', 'post_type' => self::$post_type, ) ); @@ -52,7 +52,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { $post_id = $factory->post->create( array( - 'post_content' => 1 . rand_str() . ' about', + 'post_content' => '4 about', 'post_type' => self::$post_type, ) ); @@ -81,7 +81,7 @@ public function test_operator_equals() { $expected = self::$post_ids[4]; - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_greater_than() { @@ -99,7 +99,7 @@ public function test_operator_greater_than() { $expected = self::$post_ids[5]; - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_greater_than_no_results() { @@ -117,7 +117,7 @@ public function test_operator_greater_than_no_results() { $expected = array(); - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_less_than() { $args = array( @@ -143,7 +143,7 @@ public function test_operator_less_than() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_less_than_no_results() { @@ -161,7 +161,7 @@ public function test_operator_less_than_no_results() { $expected = array(); - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } @@ -189,8 +189,7 @@ public function test_operator_not_equal() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); - + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_equal_or_greater_than() { $args = array( @@ -213,7 +212,7 @@ public function test_operator_equal_or_greater_than() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_equal_or_greater_than_no_results() { @@ -231,7 +230,7 @@ public function test_operator_equal_or_greater_than_no_results() { $expected = array(); - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_equal_or_less_than() { @@ -255,7 +254,7 @@ public function test_operator_equal_or_less_than() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_operator_equal_or_less_than_no_results() { @@ -273,7 +272,7 @@ public function test_operator_equal_or_less_than_no_results() { $expected = array(); - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_invalid_operator_should_fall_back_on_equals() { @@ -294,7 +293,7 @@ public function test_invalid_operator_should_fall_back_on_equals() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_wrong_count_no_results() { @@ -312,7 +311,7 @@ public function test_wrong_count_no_results() { $expected = array(); - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_no_operator_no_results() { @@ -329,7 +328,7 @@ public function test_no_operator_no_results() { $expected = self::$post_ids[5]; - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_empty_non_numeric_string_should_be_ignored() { @@ -353,7 +352,7 @@ public function test_empty_non_numeric_string_should_be_ignored() { $expected[] = $expected_id; } - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } public function test_simple_count() { @@ -368,7 +367,6 @@ public function test_simple_count() { $expected = self::$post_ids[5]; - $this->assertSameSets( $found_post_ids, $expected ); + $this->assertSameSets( $expected, $found_post_ids ); } } - diff --git a/tests/phpunit/tests/query/commentFeed.php b/tests/phpunit/tests/query/commentFeed.php new file mode 100644 index 0000000000000..23a37f2b40f4c --- /dev/null +++ b/tests/phpunit/tests/query/commentFeed.php @@ -0,0 +1,112 @@ +post->create_many( + 3, + array( + 'post_type' => self::$post_type, + 'post_status' => 'publish', + ) + ); + foreach ( self::$post_ids as $post_id ) { + $factory->comment->create_post_comments( $post_id, 5 ); + } + + update_option( 'posts_per_rss', 100 ); + } + + /** + * @ticket 36904 + */ + public function test_archive_comment_feed() { + add_filter( 'split_the_query', '__return_false' ); + $q1 = new WP_Query(); + $args = array( + 'withcomments' => 1, + 'feed' => 'comments-rss', + 'post_type' => self::$post_type, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'ignore_sticky_posts' => false, + 'no_found_rows' => true, + 'cache_results' => false, + ); + $q1->query( $args ); + $num_queries = get_num_queries(); + $q2 = new WP_Query(); + $q2->query( $args ); + $this->assertTrue( $q2->is_comment_feed() ); + $this->assertFalse( $q2->is_singular() ); + $this->assertSame( $num_queries + 1, get_num_queries() ); + } + + /** + * @ticket 36904 + */ + public function test_archive_comment_feed_invalid_cache() { + $q1 = new WP_Query(); + $args = array( + 'withcomments' => 1, + 'feed' => 'comments-rss', + 'post_type' => self::$post_type, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'ignore_sticky_posts' => false, + ); + $q1->query( $args ); + $comment_count = $q1->comment_count; + $this->assertSame( 15, $comment_count ); + + $post = self::factory()->post->create_and_get( + array( + 'post_type' => self::$post_type, + 'post_status' => 'publish', + ) + ); + self::factory()->comment->create_post_comments( $post->ID, 5 ); + $q2 = new WP_Query(); + $q2->query( $args ); + $this->assertTrue( $q2->is_comment_feed() ); + $this->assertFalse( $q2->is_singular() ); + + $comment_count = $q2->comment_count; + $this->assertSame( 20, $comment_count ); + } + + /** + * @ticket 36904 + */ + public function test_single_comment_feed() { + $post = get_post( self::$post_ids[0] ); + + $q1 = new WP_Query(); + $args = array( + 'withcomments' => 1, + 'feed' => 'comments-rss', + 'post_type' => $post->post_type, + 'name' => $post->post_name, + 'update_post_meta_cache' => false, + 'update_post_term_cache' => false, + 'ignore_sticky_posts' => false, + 'cache_results' => false, + ); + + $q1->query( $args ); + $num_queries = get_num_queries(); + $q2 = new WP_Query(); + $q2->query( $args ); + + $this->assertTrue( $q2->is_comment_feed() ); + $this->assertTrue( $q2->is_singular() ); + $this->assertSame( $num_queries + 1, get_num_queries() ); + } +} diff --git a/tests/phpunit/tests/query/conditionals.php b/tests/phpunit/tests/query/conditionals.php index 4ef661b21a3eb..4b473178897ae 100644 --- a/tests/phpunit/tests/query/conditionals.php +++ b/tests/phpunit/tests/query/conditionals.php @@ -14,6 +14,22 @@ class Tests_Query_Conditionals extends WP_UnitTestCase { protected $page_ids; protected $post_ids; + /** + * ID of the user-a. + * + * @var int + */ + public static $user_a_id; + + /** + * Set up the shared fixture. + * + * @param WP_UnitTest_Factory $factory Factory instance. + */ + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$user_a_id = $factory->user->create( array( 'user_login' => 'user-a' ) ); + } + public function set_up() { parent::set_up(); @@ -395,7 +411,6 @@ public function test_main_feed_2() { $this->go_to( "/{$feed}/" ); $this->assertQueryTrue( 'is_feed' ); } - } public function test_main_feed() { @@ -440,7 +455,6 @@ public function test_main_comments_feed() { $this->go_to( "/comments/{$type}" ); $this->assertQueryTrue( 'is_feed', 'is_comment_feed' ); } - } // 'search/(.+)/feed/(feed|rdf|rss|rss2|atom)/?$' => 'index.php?s=$matches[1]&feed=$matches[2]', @@ -590,7 +604,6 @@ public function test_tag() { // 'author/([^/]+)/feed/(feed|rdf|rss|rss2|atom)/?$' => 'index.php?author_name=$matches[1]&feed=$matches[2]', // 'author/([^/]+)/(feed|rdf|rss|rss2|atom)/?$' => 'index.php?author_name=$matches[1]&feed=$matches[2]', public function test_author_feed() { - self::factory()->user->create( array( 'user_login' => 'user-a' ) ); // Check the long form. $types = array( 'feed', 'rdf', 'rss', 'rss2', 'atom' ); foreach ( $types as $type ) { @@ -609,7 +622,7 @@ public function test_author_feed() { // 'author/([^/]+)/page/?([0-9]{1,})/?$' => 'index.php?author_name=$matches[1]&paged=$matches[2]', public function test_author_paged() { update_option( 'posts_per_page', 2 ); - $user_id = self::factory()->user->create( array( 'user_login' => 'user-a' ) ); + $user_id = self::$user_a_id; self::factory()->post->create_many( 3, array( 'post_author' => $user_id ) ); $this->go_to( '/author/user-a/page/2/' ); $this->assertQueryTrue( 'is_archive', 'is_author', 'is_paged' ); @@ -617,14 +630,13 @@ public function test_author_paged() { // 'author/([^/]+)/?$' => 'index.php?author_name=$matches[1]', public function test_author() { - $user_id = self::factory()->user->create( array( 'user_login' => 'user-a' ) ); + $user_id = self::$user_a_id; self::factory()->post->create( array( 'post_author' => $user_id ) ); $this->go_to( '/author/user-a/' ); $this->assertQueryTrue( 'is_archive', 'is_author' ); } public function test_author_with_no_posts() { - $user_id = self::factory()->user->create( array( 'user_login' => 'user-a' ) ); $this->go_to( '/author/user-a/' ); $this->assertQueryTrue( 'is_archive', 'is_author' ); } @@ -771,7 +783,6 @@ public function test_post_paged_short() { $this->go_to( get_permalink( $post_id ) . '2/' ); // Should is_paged be true also? $this->assertQueryTrue( 'is_single', 'is_singular' ); - } // '[0-9]{4}/[0-9]{1,2}/[0-9]{1,2}/[^/]+/([^/]+)/?$' => 'index.php?attachment=$matches[1]', @@ -978,13 +989,13 @@ public function test_is_single_with_slug_that_begins_with_a_number_that_clashes_ public function test_is_single_with_slug_that_clashes_with_attachment() { $this->set_permalink_structure( '/%postname%/' ); - $attachment_id = $this->factory->post->create( + $attachment_id = self::factory()->post->create( array( 'post_type' => 'attachment', ) ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_title' => get_post( $attachment_id )->post_title, ) @@ -1125,7 +1136,7 @@ public function test_is_attachment_with_slug_that_begins_with_a_number_that_clas * @ticket 24674 */ public function test_is_author_with_nicename_that_begins_with_a_number_that_clashes_with_another_author_id() { - $u1 = self::factory()->user->create(); + $u1 = self::$user_a_id; $u2_name = $u1 . '_user'; $u2 = self::factory()->user->create( @@ -1282,7 +1293,7 @@ public function test_is_page_template_not_singular() { global $wpdb; // We need a non-post that shares an ID with a post assigned a template. - $user_id = self::factory()->user->create(); + $user_id = self::$user_a_id; if ( ! get_post( $user_id ) ) { $post_id = self::factory()->post->create( array( 'post_type' => 'post' ) ); $wpdb->update( $wpdb->posts, array( 'ID' => $user_id ), array( 'ID' => $post_id ), array( '%d' ) ); @@ -1355,12 +1366,7 @@ public function test_is_attachment_should_not_match_numeric_id_to_post_name_begi * @ticket 35902 */ public function test_is_author_should_not_match_numeric_id_to_nickname_beginning_with_id() { - $u1 = self::factory()->user->create( - array( - 'nickname' => 'Foo', - 'user_nicename' => 'foo', - ) - ); + $u1 = self::$user_a_id; $u2 = self::factory()->user->create( array( 'nickname' => "$u1 Foo", @@ -1378,12 +1384,7 @@ public function test_is_author_should_not_match_numeric_id_to_nickname_beginning * @ticket 35902 */ public function test_is_author_should_not_match_numeric_id_to_user_nicename_beginning_with_id() { - $u1 = self::factory()->user->create( - array( - 'nickname' => 'Foo', - 'user_nicename' => 'foo', - ) - ); + $u1 = self::$user_a_id; $u2 = self::factory()->user->create( array( 'nickname' => 'Foo', @@ -1616,4 +1617,80 @@ public function test_is_privacy_policy() { $this->assertQueryTrue( 'is_page', 'is_singular', 'is_privacy_policy' ); } + /** + * @ticket 55104 + * + * @dataProvider data_conditional_tags_trigger_doing_it_wrong_and_return_false_if_wp_query_is_not_set + * + * @param string $function_name The name of the function to test. + */ + public function test_conditional_tags_trigger_doing_it_wrong_and_return_false_if_wp_query_is_not_set( $function_name ) { + unset( $GLOBALS['wp_query'] ); + + if ( 'is_comments_popup' === $function_name ) { + // `is_comments_popup()` is deprecated as of WP 4.5. + $this->setExpectedDeprecated( $function_name ); + } else { + // All the other functions should throw a `_doing_it_wrong()` notice. + $this->setExpectedIncorrectUsage( $function_name ); + } + + $this->assertFalse( call_user_func( $function_name ) ); + } + + /** + * Data provider. + */ + public function data_conditional_tags_trigger_doing_it_wrong_and_return_false_if_wp_query_is_not_set() { + // Get the list of `is_*()` conditional tags. + $functions = array_filter( + get_class_methods( 'WP_Query' ), + static function ( $function_name ) { + return str_starts_with( $function_name, 'is_' ); + } + ); + + // Wrap each function name in an array. + $functions = array_map( + static function ( $function_name ) { + return array( $function_name ); + }, + $functions + ); + + return $functions; + } + + /** + * @ticket 55722 + * + * @dataProvider data_loop_functions_do_not_trigger_a_fatal_error_if_wp_query_is_not_set + * + * @param string $function_name The name of the function to test. + * @param false|null $expected Expected return value. + */ + public function test_loop_functions_do_not_trigger_a_fatal_error_if_wp_query_is_not_set( $function_name, $expected ) { + unset( $GLOBALS['wp_query'] ); + + $this->assertSame( $expected, call_user_func( $function_name ) ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type string $function_name The name of the function to test. + * @type false|null $expected Expected return value. + * } + */ + public function data_loop_functions_do_not_trigger_a_fatal_error_if_wp_query_is_not_set() { + return array( + array( 'have_posts', false ), + array( 'in_the_loop', false ), + array( 'rewind_posts', null ), + array( 'the_post', null ), + array( 'have_comments', false ), + array( 'the_comment', null ), + ); + } } diff --git a/tests/phpunit/tests/query/date.php b/tests/phpunit/tests/query/date.php index d54f8af77c1f5..978368674a96d 100644 --- a/tests/phpunit/tests/query/date.php +++ b/tests/phpunit/tests/query/date.php @@ -298,7 +298,7 @@ public function test_non_scalar_m_should_be_discarded() { ) ); - $this->assertEquals( $expected, $posts ); + $this->assertEqualSets( $expected, $posts ); } public function test_simple_monthnum_expecting_results() { diff --git a/tests/phpunit/tests/query/fieldsClause.php b/tests/phpunit/tests/query/fieldsClause.php new file mode 100644 index 0000000000000..c5febb973353b --- /dev/null +++ b/tests/phpunit/tests/query/fieldsClause.php @@ -0,0 +1,234 @@ +post->create_many( 5, array( 'post_type' => 'wptests_pt' ) ); + } + + public function set_up() { + parent::set_up(); + /* + * Re-register the CPT for use within each test. + * + * Custom post types are deregistered by the default tear_down method + * so need to be re-registered for each test as WP_Query calls + * get_post_types(). + */ + register_post_type( 'wptests_pt' ); + } + + /** + * Tests limiting the WP_Query fields to the ID and parent sub-set. + * + * @ticket 57012 + */ + public function test_should_limit_fields_to_id_and_parent_subset() { + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'id=>parent', + ); + + $q = new WP_Query( $query_args ); + + $expected = array(); + foreach ( self::$post_ids as $post_id ) { + $expected[] = (object) array( + 'ID' => $post_id, + 'post_parent' => 0, + ); + } + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Tests limiting the WP_Query fields to the IDs only. + * + * @ticket 57012 + */ + public function test_should_limit_fields_to_ids() { + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'ids', + ); + + $q = new WP_Query( $query_args ); + + $expected = self::$post_ids; + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Tests querying all fields via WP_Query. + * + * @ticket 57012 + */ + public function test_should_query_all_fields() { + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'all', + ); + + $q = new WP_Query( $query_args ); + + $expected = array_map( 'get_post', self::$post_ids ); + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Tests adding fields to WP_Query via filters when requesting the ID and parent sub-set. + * + * @ticket 57012 + */ + public function test_should_include_filtered_values_in_addition_to_id_and_parent_subset() { + add_filter( 'posts_fields', array( $this, 'filter_posts_fields' ) ); + add_filter( 'posts_clauses', array( $this, 'filter_posts_clauses' ) ); + + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'id=>parent', + ); + + $q = new WP_Query( $query_args ); + + $expected = array(); + foreach ( self::$post_ids as $post_id ) { + $expected[] = (object) array( + 'ID' => $post_id, + 'post_parent' => 0, + 'test_post_fields' => '1', + 'test_post_clauses' => '2', + ); + } + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Tests adding fields to WP_Query via filters when requesting the ID field. + * + * @ticket 57012 + */ + public function test_should_include_filtered_values_in_addition_to_id() { + add_filter( 'posts_fields', array( $this, 'filter_posts_fields' ) ); + add_filter( 'posts_clauses', array( $this, 'filter_posts_clauses' ) ); + + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'ids', + ); + + $q = new WP_Query( $query_args ); + + // `fields => ids` does not include the additional fields. + $expected = self::$post_ids; + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Tests adding fields to WP_Query via filters when requesting all fields. + * + * @ticket 57012 + */ + public function test_should_include_filtered_values() { + add_filter( 'posts_fields', array( $this, 'filter_posts_fields' ) ); + add_filter( 'posts_clauses', array( $this, 'filter_posts_clauses' ) ); + + $query_args = array( + 'post_type' => 'wptests_pt', + 'fields' => 'all', + ); + + $q = new WP_Query( $query_args ); + + $expected = array_map( 'get_post', self::$post_ids ); + foreach ( $expected as $post ) { + $post->test_post_fields = '1'; + $post->test_post_clauses = '2'; + } + + $this->assertEqualSets( $expected, $q->posts, 'Posts property for first query is not of expected form.' ); + $this->assertSame( 5, $q->found_posts, 'Number of found posts is not five.' ); + $this->assertSame( 1, $q->max_num_pages, 'Number of found pages is not one.' ); + + // Test the second query's results match. + $q2 = new WP_Query( $query_args ); + $this->assertEqualSets( $expected, $q2->posts, 'Posts property for second query is not in the expected form.' ); + } + + /** + * Filters the posts fields. + * + * @param string $fields The fields to SELECT. + * @return string The filtered fields. + */ + public function filter_posts_fields( $fields ) { + return "$fields, 1 as test_post_fields"; + } + + /** + * Filters the posts clauses. + * + * @param array $clauses The WP_Query database clauses. + * @return array The filtered database clauses. + */ + public function filter_posts_clauses( $clauses ) { + $clauses['fields'] .= ', 2 as test_post_clauses'; + return $clauses; + } +} diff --git a/tests/phpunit/tests/query/generatePostdata.php b/tests/phpunit/tests/query/generatePostdata.php index c223e4b59859d..5d39dfb5d4ba5 100644 --- a/tests/phpunit/tests/query/generatePostdata.php +++ b/tests/phpunit/tests/query/generatePostdata.php @@ -19,7 +19,7 @@ public function test_setup_by_id() { * @ticket 42814 */ public function test_setup_by_fake_post() { - $fake = new stdClass; + $fake = new stdClass(); $fake->ID = 98765; $data = generate_postdata( $fake->ID ); diff --git a/tests/phpunit/tests/query/invalidQueries.php b/tests/phpunit/tests/query/invalidQueries.php index ea18617b286a3..cd0b87e9960f2 100644 --- a/tests/phpunit/tests/query/invalidQueries.php +++ b/tests/phpunit/tests/query/invalidQueries.php @@ -91,15 +91,14 @@ public function test_unregistered_post_type_wp_query() { global $wpdb; $query = new WP_Query( array( 'post_type' => 'unregistered_cpt' ) ); - $posts = $query->get_posts(); $this->assertStringContainsString( "{$wpdb->posts}.post_type = 'unregistered_cpt'", self::$last_posts_request ); $this->assertStringContainsString( "{$wpdb->posts}.post_status = 'publish'", self::$last_posts_request ); - $this->assertCount( 0, $posts ); + $this->assertCount( 0, $query->posts ); } /** - * Test WP Query with an invalid post type in a mutiple post type query. + * Test WP Query with an invalid post type in a multiple post type query. * * @ticket 48556 */ @@ -111,10 +110,9 @@ public function test_unregistered_post_type_wp_query_multiple_post_types() { 'post_type' => array( 'unregistered_cpt', 'page' ), ) ); - $posts = $query->get_posts(); $this->assertStringContainsString( "{$wpdb->posts}.post_type = 'unregistered_cpt'", self::$last_posts_request ); - $this->assertCount( 1, $posts, 'the valid `page` post type should still return one post' ); + $this->assertCount( 1, $query->posts, 'the valid `page` post type should still return one post' ); } /** @@ -143,10 +141,9 @@ public function test_deprecated_parameters_have_no_effect_on_page() { 'post_type' => 'page', ) ); - $posts = $query->get_posts(); // Only the published page should be returned. - $this->assertCount( 1, $posts ); + $this->assertCount( 1, $query->posts ); } /** @@ -158,9 +155,24 @@ public function test_deprecated_parameters_have_no_effect_on_post() { 'static' => 'a', ) ); - $posts = $query->get_posts(); // Only the published post should be returned. - $this->assertCount( 1, $posts ); + $this->assertCount( 1, $query->posts ); + } + + /** + * Ensure a non-scalar page parameter does not throw a fatal error for trim(). + * + * @ticket 56558 + * @covers WP_Query::get_posts + */ + public function test_non_scalar_page_value() { + $query = new WP_Query( + array( + 'page' => array( 1, 2, 3 ), + ) + ); + + $this->assertSame( 0, $query->query_vars['page'] ); } } diff --git a/tests/phpunit/tests/query/isTerm.php b/tests/phpunit/tests/query/isTerm.php index 0527201ce2f18..8eb6d05d6b7bb 100644 --- a/tests/phpunit/tests/query/isTerm.php +++ b/tests/phpunit/tests/query/isTerm.php @@ -25,9 +25,6 @@ class Tests_Query_IsTerm extends WP_UnitTestCase { public function set_up() { parent::set_up(); - $GLOBALS['wp_the_query'] = new WP_Query(); - $GLOBALS['wp_query'] = $GLOBALS['wp_the_query']; - $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); create_initial_taxonomies(); diff --git a/tests/phpunit/tests/query/lazyLoadCommentMeta.php b/tests/phpunit/tests/query/lazyLoadCommentMeta.php new file mode 100644 index 0000000000000..420fd45b23ce3 --- /dev/null +++ b/tests/phpunit/tests/query/lazyLoadCommentMeta.php @@ -0,0 +1,70 @@ +post->create(); + self::$comment_ids = $factory->comment->create_post_comments( self::$post_id, 11 ); + } + + /** + * @ticket 57901 + * + * @covers ::wp_queue_comments_for_comment_meta_lazyload + * + * @expectedDeprecated wp_queue_comments_for_comment_meta_lazyload + */ + public function test_wp_queue_comments_for_comment_meta_lazyload() { + $filter = new MockAction(); + add_filter( 'update_comment_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + $comments = array_map( 'get_comment', self::$comment_ids ); + $comment_id = reset( self::$comment_ids ); + wp_queue_comments_for_comment_meta_lazyload( $comments ); + get_comment_meta( $comment_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $comment_meta_ids = end( $first ); + $this->assertSameSets( self::$comment_ids, $comment_meta_ids ); + } + + /** + * @ticket 57901 + * + * @covers ::wp_queue_comments_for_comment_meta_lazyload + * + * @expectedDeprecated wp_queue_comments_for_comment_meta_lazyload + */ + public function test_wp_queue_comments_for_comment_meta_lazyload_new_comment() { + $filter = new MockAction(); + add_filter( 'update_comment_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + $comments = array_map( 'get_comment', self::$comment_ids ); + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => self::$post_id, + ) + ); + wp_queue_comments_for_comment_meta_lazyload( $comments ); + get_comment_meta( $comment_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $comment_meta_ids = end( $first ); + $this->assertContains( $comment_id, $comment_meta_ids ); + } +} diff --git a/tests/phpunit/tests/query/lazyLoadTermMeta.php b/tests/phpunit/tests/query/lazyLoadTermMeta.php new file mode 100644 index 0000000000000..fc34f84bb313c --- /dev/null +++ b/tests/phpunit/tests/query/lazyLoadTermMeta.php @@ -0,0 +1,169 @@ +post->create_many( + 3, + array( + 'post_type' => $post_type, + 'post_status' => 'publish', + ) + ); + $taxonomies = get_object_taxonomies( $post_type, 'object' ); + foreach ( self::$post_ids as $post_id ) { + foreach ( $taxonomies as $taxonomy ) { + if ( ! $taxonomy->_builtin ) { + continue; + } + $terms = $factory->term->create_many( 3, array( 'taxonomy' => $taxonomy->name ) ); + self::$term_ids = array_merge( self::$term_ids, $terms ); + foreach ( $terms as $term ) { + add_term_meta( $term, wp_rand(), 'test' ); + } + wp_set_object_terms( $post_id, $terms, $taxonomy->name ); + } + } + } + + /** + * @ticket 57150 + * @covers ::wp_queue_posts_for_term_meta_lazyload + */ + public function test_wp_queue_posts_for_term_meta_lazyload() { + $this->reset_lazyload_queue(); + $filter = new MockAction(); + add_filter( 'update_term_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + new WP_Query( + array( + 'post__in' => self::$post_ids, + 'lazy_load_term_meta' => true, + ) + ); + + get_term_meta( end( self::$term_ids ) ); + + $args = $filter->get_args(); + $first = reset( $args ); + $term_ids = end( $first ); + $this->assertSameSets( $term_ids, self::$term_ids ); + } + + /** + * @ticket 57150 + * @covers ::wp_queue_posts_for_term_meta_lazyload + */ + public function test_wp_queue_posts_for_term_meta_lazyload_update_post_term_cache() { + $filter = new MockAction(); + add_filter( 'update_term_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + new WP_Query( + array( + 'post__in' => self::$post_ids, + 'lazy_load_term_meta' => true, + 'update_post_term_cache' => false, + ) + ); + + get_term_meta( end( self::$term_ids ) ); + + $args = $filter->get_args(); + $first = reset( $args ); + $term_ids = end( $first ); + $this->assertSameSets( $term_ids, self::$term_ids ); + } + + /** + * @ticket 57150 + * @covers ::wp_queue_posts_for_term_meta_lazyload + */ + public function test_wp_queue_posts_for_term_meta_lazyload_false() { + $filter = new MockAction(); + add_filter( 'update_term_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + new WP_Query( + array( + 'post__in' => self::$post_ids, + 'lazy_load_term_meta' => false, + ) + ); + + $term_id = end( self::$term_ids ); + get_term_meta( $term_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $term_ids = end( $first ); + $this->assertSameSets( $term_ids, array( $term_id ) ); + } + + + /** + * @ticket 57901 + * + * @covers ::wp_queue_posts_for_term_meta_lazyload + */ + public function test_wp_queue_posts_for_term_meta_lazyload_insert_term() { + $filter = new MockAction(); + add_filter( 'update_term_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + + register_taxonomy( 'wptests_tax', 'post' ); + + $t1 = wp_insert_term( 'Foo', 'wptests_tax' ); + $term_id = $t1['term_id']; + + new WP_Query( + array( + 'post__in' => self::$post_ids, + 'lazy_load_term_meta' => true, + ) + ); + + get_term_meta( $term_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $term_ids = end( $first ); + $this->assertContains( $term_id, $term_ids ); + } + + /** + * @ticket 57150 + * @covers ::wp_queue_posts_for_term_meta_lazyload + */ + public function test_wp_queue_posts_for_term_meta_lazyload_delete_term() { + $filter = new MockAction(); + add_filter( 'update_term_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + + $remove_term_id = end( self::$term_ids ); + $term = get_term( $remove_term_id ); + wp_delete_term( $remove_term_id, $term->taxonomy ); + + new WP_Query( + array( + 'post__in' => self::$post_ids, + 'lazy_load_term_meta' => true, + ) + ); + + $term_id = end( self::$term_ids ); + get_term_meta( $term_id ); + + $args = $filter->get_args(); + $first = reset( $args ); + $term_ids = end( $first ); + $this->assertContains( $remove_term_id, $term_ids ); + } +} diff --git a/tests/phpunit/tests/query/metaQuery.php b/tests/phpunit/tests/query/metaQuery.php index c9e73ec8a509c..e930ef3266654 100644 --- a/tests/phpunit/tests/query/metaQuery.php +++ b/tests/phpunit/tests/query/metaQuery.php @@ -1920,7 +1920,6 @@ public function test_compare_key_like_with_not_exists_compare() { ); $this->assertSameSets( array( $posts[0] ), $q->posts ); - } /** diff --git a/tests/phpunit/tests/query/noFoundRows.php b/tests/phpunit/tests/query/noFoundRows.php index 457da449942a4..7b4f66a416030 100644 --- a/tests/phpunit/tests/query/noFoundRows.php +++ b/tests/phpunit/tests/query/noFoundRows.php @@ -73,7 +73,7 @@ public function test_no_found_rows_non_bool_cast_to_true() { * @ticket 29552 */ public function test_no_found_rows_default_with_nopaging_true() { - $p = $this->factory->post->create(); + $p = self::factory()->post->create(); $q = new WP_Query( array( @@ -90,7 +90,7 @@ public function test_no_found_rows_default_with_nopaging_true() { * @ticket 29552 */ public function test_no_found_rows_default_with_postsperpage_minus1() { - $p = $this->factory->post->create(); + $p = self::factory()->post->create(); $q = new WP_Query( array( diff --git a/tests/phpunit/tests/query/parseQuery.php b/tests/phpunit/tests/query/parseQuery.php index 5b3625ed0f74f..7830b6723dfa5 100644 --- a/tests/phpunit/tests/query/parseQuery.php +++ b/tests/phpunit/tests/query/parseQuery.php @@ -104,4 +104,164 @@ public function test_parse_query_p_object() { $this->assertSame( '404', $q->query_vars['error'] ); } + /** + * Ensure an array of authors is rejected. + * + * @ticket 17737 + */ + public function test_parse_query_author_array() { + $q = new WP_Query(); + $q->parse_query( + array( + 'author' => array( 1, 2, 3 ), + ) + ); + + $this->assertEmpty( $q->query_vars['author'] ); + } + + /** + * Ensure a non-scalar (non-numeric) author value is rejected. + * + * @ticket 17737 + */ + public function test_parse_query_author_string() { + $q = new WP_Query(); + $q->parse_query( + array( + 'author' => 'admin', + ) + ); + + $this->assertEmpty( $q->query_vars['author'] ); + } + + /** + * Ensure nonscalar 'cat' array values are rejected. + * + * Note the returned 'cat' query_var value is a string. + * + * @ticket 17737 + */ + public function test_parse_query_cat_array_mixed() { + $q = new WP_Query(); + $q->parse_query( + array( + 'cat' => array( 1, 'uncategorized', '-1' ), + ) + ); + + $this->assertSame( '-1,1', $q->query_vars['cat'] ); + } + + /** + * Ensure a nonscalar menu_order value is rejected. + * + * @ticket 17737 + */ + public function test_parse_query_menu_order_nonscalar() { + $q = new WP_Query(); + $q->parse_query( + array( + 'menu_order' => array( 1 ), + ) + ); + + $this->assertEmpty( $q->query_vars['menu_order'] ); + } + + /** + * Ensure numeric 'subpost' gets assigned to 'attachment'. + * + * @ticket 17737 + */ + public function test_parse_query_subpost_scalar() { + $q = new WP_Query(); + $q->parse_query( + array( + 'subpost' => 1, + ) + ); + + $this->assertSame( 1, $q->query_vars['attachment'] ); + } + + /** + * Ensure non-scalar 'subpost' does not get assigned to 'attachment'. + * + * @ticket 17737 + */ + public function test_parse_query_subpost_nonscalar() { + $q = new WP_Query(); + $q->parse_query( + array( + 'subpost' => array( 1 ), + ) + ); + + $this->assertEmpty( $q->query_vars['attachment'] ); + } + + /** + * Ensure numeric 'attachment_id' value is assigned. + * + * @ticket 17737 + */ + public function test_parse_query_attachment_id() { + $q = new WP_Query(); + $q->parse_query( + array( + 'attachment_id' => 1, + ) + ); + + $this->assertSame( 1, $q->query_vars['attachment_id'] ); + } + + /** + * Ensure non-scalar 'attachment_id' value is rejected. + * + * @ticket 17737 + */ + public function test_parse_query_attachment_id_nonscalar() { + $q = new WP_Query(); + $q->parse_query( + array( + 'attachment_id' => array( 1 ), + ) + ); + + $this->assertEmpty( $q->query_vars['attachment_id'] ); + } + + /** + * Tests that a fatal error is not thrown when a hierarchical taxonomy query var + * passed to wp_basename() in ::parse_tax_query() is an array instead of a string. + * + * The message that we should not see: + * `TypeError: urldecode(): Argument #1 ($string) must be of type string, array given`. + * + * @ticket 64870 + */ + public function test_parse_query_hierarchical_taxonomy_query_var_array() { + register_taxonomy( + 'wptests_tax', + 'post', + array( + 'query_var' => 'wptests_tax', + 'rewrite' => array( 'hierarchical' => true ), + 'public' => true, + ) + ); + + $q = new WP_Query( + array( + 'wptests_tax' => array( 'term-a', 'term-b' ), + ) + ); + + unregister_taxonomy( 'wptests_tax' ); + + $this->assertIsArray( $q->posts ); + } } diff --git a/tests/phpunit/tests/query/postStatus.php b/tests/phpunit/tests/query/postStatus.php index 1ab8aa849d1b8..95e7968951c19 100644 --- a/tests/phpunit/tests/query/postStatus.php +++ b/tests/phpunit/tests/query/postStatus.php @@ -87,7 +87,7 @@ public function set_up() { * Register custom post types and statuses used in multiple tests. * * CPTs and CPSs are reset between each test run so need to be registered - * in both the wpSetUpBeforeClass() and setUp() methods. + * in both the wpSetUpBeforeClass() and set_up() methods. */ public static function register_custom_post_objects() { register_post_type( @@ -240,7 +240,11 @@ public function test_all_public_post_stati_should_be_included_when_no_post_statu ) ); - foreach ( get_post_stati( array( 'public' => true ) ) as $status ) { + $stati = get_post_stati( array( 'public' => true ) ); + + $this->assertNotEmpty( $stati ); + + foreach ( $stati as $status ) { $this->assertStringContainsString( "post_status = '$status'", $q->request ); } } diff --git a/tests/phpunit/tests/query/results.php b/tests/phpunit/tests/query/results.php index bf653d378a857..412d02fe83be6 100644 --- a/tests/phpunit/tests/query/results.php +++ b/tests/phpunit/tests/query/results.php @@ -156,7 +156,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$post_ids[] = $factory->post->create( array( 'post_title' => 'no-comments', - 'post_date' => '2009-10-01 00:00:00', + 'post_date' => '2009-10-15 00:00:00', ) ); self::$post_ids[] = $factory->post->create( @@ -768,7 +768,6 @@ public function test_query_fields_integers() { $this->assertIsInt( $post->ID ); $this->assertIsInt( $post->post_parent ); } - } /** diff --git a/tests/phpunit/tests/query/search.php b/tests/phpunit/tests/query/search.php index 2beda65d2f66c..7bfbdec31c87d 100644 --- a/tests/phpunit/tests/query/search.php +++ b/tests/phpunit/tests/query/search.php @@ -10,7 +10,7 @@ class Tests_Query_Search extends WP_UnitTestCase { public function set_up() { parent::set_up(); - $this->post_type = rand_str( 12 ); + $this->post_type = 'foo1'; register_post_type( $this->post_type ); $this->q = new WP_Query(); @@ -36,7 +36,7 @@ public function test_search_order_title_relevance() { foreach ( range( 1, 7 ) as $i ) { self::factory()->post->create( array( - 'post_content' => $i . rand_str() . ' about', + 'post_content' => "{$i} about", 'post_type' => $this->post_type, ) ); @@ -266,7 +266,7 @@ public function test_search_orderby_should_be_empty_when_search_string_is_longer * @ticket 31025 */ public function test_s_zero() { - $p1 = $this->factory->post->create( + $p1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_title' => '1', @@ -275,12 +275,12 @@ public function test_s_zero() { ) ); - $p2 = $this->factory->post->create( + $p2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_title' => '0', 'post_content' => 'this post contains zeroes', - 'post_excerpt' => 'this post containts zeroes', + 'post_excerpt' => 'this post contains zeroes', ) ); @@ -408,7 +408,7 @@ public function test_search_order_title_before_excerpt_and_content() { } /** - * Unfiltered search queries for attachment post types should not inlcude + * Unfiltered search queries for attachment post types should not include * filenames to ensure the postmeta JOINs don't happen on the front end. * * @ticket 22744 @@ -454,7 +454,7 @@ public function test_include_file_names_in_attachment_search_as_string() { ); add_post_meta( $attachment, '_wp_attached_file', 'some-image1.png', true ); - add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); // Pass post_type a string value. $q = new WP_Query( @@ -484,7 +484,7 @@ public function test_include_file_names_in_attachment_search_as_array() { ); add_post_meta( $attachment, '_wp_attached_file', 'some-image2.png', true ); - add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); // Pass post_type an array value. $q = new WP_Query( @@ -543,7 +543,7 @@ public function test_include_file_names_in_attachment_search_with_meta_query() { add_post_meta( $attachment, '_wp_attached_file', 'some-image4.png', true ); add_post_meta( $attachment, '_test_meta_key', 'value', true ); - add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); // Pass post_type a string value. $q = new WP_Query( @@ -583,7 +583,7 @@ public function test_include_file_names_in_attachment_search_with_tax_query() { wp_set_post_terms( $attachment, 'test', 'post_tag' ); add_post_meta( $attachment, '_wp_attached_file', 'some-image5.png', true ); - add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); // Pass post_type a string value. $q = new WP_Query( @@ -608,25 +608,37 @@ public function test_include_file_names_in_attachment_search_with_tax_query() { /** * @ticket 22744 */ - public function test_filter_query_attachment_filenames_unhooks_itself() { - add_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + public function test_wp_query_removes_filter_wp_allow_query_attachment_by_filename() { + $attachment = self::factory()->post->create( + array( + 'post_type' => 'attachment', + 'post_status' => 'publish', + 'post_title' => 'bar foo', + 'post_content' => 'foo bar', + 'post_excerpt' => 'This post has foo', + ) + ); + + add_post_meta( $attachment, '_wp_attached_file', 'some-image1.png', true ); + add_filter( 'wp_allow_query_attachment_by_filename', '__return_true' ); - apply_filters( - 'posts_clauses', + $q = new WP_Query( array( - 'where' => '', - 'groupby' => '', - 'join' => '', - 'orderby' => '', - 'distinct' => '', - 'fields' => '', - 'limit' => '', + 's' => 'image1', + 'fields' => 'ids', + 'post_type' => 'attachment', + 'post_status' => 'inherit', ) ); - $result = has_filter( 'posts_clauses', '_filter_query_attachment_filenames' ); + $this->assertSame( array( $attachment ), $q->posts ); - $this->assertFalse( $result ); + /* + * WP_Query should have removed the wp_allow_query_attachment_by_filename filter + * and thus not match the attachment created above. + */ + $q->get_posts(); + $this->assertEmpty( $q->posts ); } public function filter_posts_search( $sql ) { diff --git a/tests/phpunit/tests/query/searchColumns.php b/tests/phpunit/tests/query/searchColumns.php new file mode 100644 index 0000000000000..9ef30c2113920 --- /dev/null +++ b/tests/phpunit/tests/query/searchColumns.php @@ -0,0 +1,413 @@ +post->create( + array( + 'post_status' => 'publish', + 'post_title' => 'foo title', + 'post_excerpt' => 'foo excerpt', + 'post_content' => 'foo content', + ) + ); + self::$pid2 = $factory->post->create( + array( + 'post_status' => 'publish', + 'post_title' => 'bar title', + 'post_excerpt' => 'foo bar excerpt', + 'post_content' => 'foo bar content', + ) + ); + + self::$pid3 = $factory->post->create( + array( + 'post_status' => 'publish', + 'post_title' => 'baz title', + 'post_excerpt' => 'baz bar excerpt', + 'post_content' => 'baz bar foo content', + ) + ); + } + + /** + * Tests that search uses default search columns when search columns are empty. + * + * @ticket 43867 + */ + public function test_s_should_use_default_search_columns_when_empty_search_columns() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array(), + 'fields' => 'ids', + ) + ); + + $this->assertStringContainsString( 'post_title', $q->request, 'SQL request should contain post_title string.' ); + $this->assertStringContainsString( 'post_excerpt', $q->request, 'SQL request should contain post_excerpt string.' ); + $this->assertStringContainsString( 'post_content', $q->request, 'SQL request should contain post_content string.' ); + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts, 'Query results should be equal to the set.' ); + } + + /** + * Tests that search supports the `post_title` search column. + * + * @ticket 43867 + */ + public function test_s_should_support_post_title_search_column() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_title' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1 ), $q->posts ); + } + + /** + * Tests that search supports the `post_excerpt` search column. + * + * @ticket 43867 + */ + public function test_s_should_support_post_excerpt_search_column() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_excerpt' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1, self::$pid2 ), $q->posts ); + } + + /** + * Tests that search supports the `post_content` search column. + * + * @ticket 43867 + */ + public function test_s_should_support_post_content_search_column() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_content' ), + 'fields' => 'ids', + ) + ); + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports the `post_title` and `post_excerpt` search columns together. + * + * @ticket 43867 + */ + public function test_s_should_support_post_title_and_post_excerpt_search_columns() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_title', 'post_excerpt' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1, self::$pid2 ), $q->posts ); + } + + /** + * Tests that search supports the `post_title` and `post_content` search columns together. + * + * @ticket 43867 + */ + public function test_s_should_support_post_title_and_post_content_search_columns() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_title', 'post_content' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports the `post_excerpt` and `post_content` search columns together. + * + * @ticket 43867 + */ + public function test_s_should_support_post_excerpt_and_post_content_search_columns() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_excerpt', 'post_content' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports the `post_title`, `post_excerpt` and `post_content` search columns together. + * + * @ticket 43867 + */ + public function test_s_should_support_post_title_and_post_excerpt_and_post_content_search_columns() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_title', 'post_excerpt', 'post_content' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search uses default search columns when using a non-existing search column. + * + * @ticket 43867 + */ + public function test_s_should_use_default_search_columns_when_using_non_existing_search_column() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_non_existing_column' ), + 'fields' => 'ids', + ) + ); + + $this->assertStringContainsString( 'post_title', $q->request, 'SQL request should contain post_title string.' ); + $this->assertStringContainsString( 'post_excerpt', $q->request, 'SQL request should contain post_excerpt string.' ); + $this->assertStringContainsString( 'post_content', $q->request, 'SQL request should contain post_content string.' ); + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts, 'Query results should be equal to the set.' ); + } + + /** + * Tests that search ignores a non-existing search column when used together with a supported one. + * + * @ticket 43867 + */ + public function test_s_should_ignore_non_existing_search_column_when_used_with_supported_one() { + $q = new WP_Query( + array( + 's' => 'foo', + 'search_columns' => array( 'post_title', 'post_non_existing_column' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1 ), $q->posts ); + } + + /** + * Tests that search supports search columns when searching multiple terms. + * + * @ticket 43867 + */ + public function test_s_should_support_search_columns_when_searching_multiple_terms() { + $q = new WP_Query( + array( + 's' => 'foo bar', + 'search_columns' => array( 'post_content' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports search columns when searching for a sentence. + * + * @ticket 43867 + */ + public function test_s_should_support_search_columns_when_sentence_true() { + $q = new WP_Query( + array( + 's' => 'bar foo', + 'search_columns' => array( 'post_content' ), + 'sentence' => true, + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports search columns when searching for a sentence. + * + * @ticket 43867 + */ + public function test_s_should_support_search_columns_when_sentence_false() { + $q = new WP_Query( + array( + 's' => 'bar foo', + 'search_columns' => array( 'post_content' ), + 'sentence' => false, + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid2, self::$pid3 ), $q->posts ); + } + + /** + * Tests that search supports search columns when using term exclusion. + * + * @ticket 43867 + */ + public function test_s_should_support_search_columns_when_searching_with_term_exclusion() { + $q = new WP_Query( + array( + 's' => 'bar -baz', + 'search_columns' => array( 'post_excerpt', 'post_content' ), + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid2 ), $q->posts ); + } + + /** + * Tests that search columns is filterable with the `post_search_columns` filter. + * + * @ticket 43867 + */ + public function test_search_columns_should_be_filterable() { + add_filter( 'post_search_columns', array( $this, 'post_supported_search_column' ), 10, 3 ); + $q = new WP_Query( + array( + 's' => 'foo', + 'fields' => 'ids', + ) + ); + + $this->assertSameSets( array( self::$pid1 ), $q->posts ); + } + + /** + * Filter callback that sets a supported search column. + * + * @param string[] $search_columns Array of column names to be searched. + * @param string $search Text being searched. + * @param WP_Query $wp_query The current WP_Query instance. + * @return string[] $search_columns Array of column names to be searched. + */ + public function post_supported_search_column( $search_columns, $search, $wp_query ) { + $search_columns = array( 'post_title' ); + return $search_columns; + } + + /** + * Tests that search columns ignores non-supported search columns from the `post_search_columns` filter. + * + * @ticket 43867 + */ + public function test_search_columns_should_not_be_filterable_with_non_supported_search_columns() { + add_filter( 'post_search_columns', array( $this, 'post_non_supported_search_column' ), 10, 3 ); + $q = new WP_Query( + array( + 's' => 'foo', + 'fields' => 'ids', + ) + ); + + $this->assertStringNotContainsString( 'post_name', $q->request, "SQL request shouldn't contain post_name string." ); + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts, 'Query results should be equal to the set.' ); + } + + /** + * Filter callback that sets an existing but non-supported search column. + * + * @param string[] $search_columns Array of column names to be searched. + * @param string $search Text being searched. + * @param WP_Query $wp_query The current WP_Query instance. + * @return string[] $search_columns Array of column names to be searched. + */ + public function post_non_supported_search_column( $search_columns, $search, $wp_query ) { + $search_columns = array( 'post_name' ); + return $search_columns; + } + + /** + * Tests that search columns ignores non-existing search columns from the `post_search_columns` filter. + * + * @ticket 43867 + */ + public function test_search_columns_should_not_be_filterable_with_non_existing_search_column() { + add_filter( 'post_search_columns', array( $this, 'post_non_existing_search_column' ), 10, 3 ); + $q = new WP_Query( + array( + 's' => 'foo', + 'fields' => 'ids', + ) + ); + + $this->assertStringNotContainsString( 'post_non_existing_column', $q->request, "SQL request shouldn't contain post_non_existing_column string." ); + $this->assertSameSets( array( self::$pid1, self::$pid2, self::$pid3 ), $q->posts, 'Query results should be equal to the set.' ); + } + + /** + * Filter callback that sets a non-existing search column. + * + * @param string[] $search_columns Array of column names to be searched. + * @param string $search Text being searched. + * @param WP_Query $wp_query The current WP_Query instance. + * @return string[] $search_columns Array of column names to be searched. + */ + public function post_non_existing_search_column( $search_columns, $search, $wp_query ) { + $search_columns = array( 'post_non_existing_column' ); + return $search_columns; + } +} diff --git a/tests/phpunit/tests/query/setupPostdata.php b/tests/phpunit/tests/query/setupPostdata.php index cabfba7c5b590..ef6b3479d8e33 100644 --- a/tests/phpunit/tests/query/setupPostdata.php +++ b/tests/phpunit/tests/query/setupPostdata.php @@ -33,7 +33,7 @@ public function test_setup_by_id() { * @ticket 30970 */ public function test_setup_by_fake_post() { - $fake = new stdClass; + $fake = new stdClass(); $fake->ID = 98765; setup_postdata( $fake->ID ); @@ -121,6 +121,9 @@ public function test_secondary_query_post_vars() { 'posts_per_page' => 1, ) ); + + $this->assertTrue( $q->have_posts() ); + if ( $q->have_posts() ) { while ( $q->have_posts() ) { $q->the_post(); @@ -221,6 +224,9 @@ public function test_secondary_query_nextpage() { 'post__in' => array( $post2 ), ) ); + + $this->assertTrue( $q->have_posts() ); + if ( $q->have_posts() ) { while ( $q->have_posts() ) { $q->the_post(); @@ -281,6 +287,9 @@ public function test_secondary_query_page() { 'posts_per_page' => 1, ) ); + + $this->assertTrue( $q->have_posts() ); + if ( $q->have_posts() ) { while ( $q->have_posts() ) { $q->the_post(); @@ -367,6 +376,9 @@ public function test_secondary_query_more() { 'posts_per_page' => 1, ) ); + + $this->assertTrue( $q->have_posts() ); + if ( $q->have_posts() ) { while ( $q->have_posts() ) { $q->the_post(); diff --git a/tests/phpunit/tests/query/stickies.php b/tests/phpunit/tests/query/stickies.php index e6d9d41f407bf..81a991c6d729d 100644 --- a/tests/phpunit/tests/query/stickies.php +++ b/tests/phpunit/tests/query/stickies.php @@ -4,6 +4,7 @@ * Tests related to sticky functionality in WP_Query. * * @group query + * @covers WP_Query::get_posts */ class Tests_Query_Stickies extends WP_UnitTestCase { public static $posts = array(); @@ -104,4 +105,48 @@ public function set_ignore_sticky_posts( $q ) { public function set_post__not_in( $q ) { $q->set( 'post__not_in', array( self::$posts[8] ) ); } + + /** + * @ticket 36907 + */ + public function test_stickies_should_obey_parameters_from_the_main_query() { + $filter = new MockAction(); + add_filter( 'posts_pre_query', array( $filter, 'filter' ), 10, 2 ); + $this->go_to( '/' ); + $filter_args = $filter->get_args(); + $query_vars = $filter_args[0][1]->query_vars; + $sticky_query_vars = $filter_args[1][1]->query_vars; + + $this->assertNotEmpty( $sticky_query_vars['posts_per_page'] ); + $this->assertSame( $query_vars['suppress_filters'], $sticky_query_vars['suppress_filters'] ); + $this->assertSame( $query_vars['cache_results'], $sticky_query_vars['cache_results'] ); + $this->assertSame( $query_vars['update_post_meta_cache'], $sticky_query_vars['update_post_meta_cache'] ); + $this->assertSame( $query_vars['update_post_term_cache'], $sticky_query_vars['update_post_term_cache'] ); + $this->assertSame( $query_vars['lazy_load_term_meta'], $sticky_query_vars['lazy_load_term_meta'] ); + $this->assertTrue( $sticky_query_vars['ignore_sticky_posts'] ); + $this->assertTrue( $sticky_query_vars['no_found_rows'] ); + } + + /** + * @ticket 36907 + */ + public function test_stickies_should_limit_query() { + $sticky_count = 6; + $post_date = gmdate( 'Y-m-d H:i:s', time() - 10000 ); + $post_ids = self::factory()->post->create_many( $sticky_count, array( 'post_date' => $post_date ) ); + add_filter( + 'pre_option_sticky_posts', + static function () use ( $post_ids ) { + return $post_ids; + } + ); + + $filter = new MockAction(); + add_filter( 'posts_pre_query', array( $filter, 'filter' ), 10, 2 ); + $this->go_to( '/' ); + $filter_args = $filter->get_args(); + $sticky_query_vars = $filter_args[1][1]->query_vars; + + $this->assertSame( $sticky_query_vars['posts_per_page'], $sticky_count ); + } } diff --git a/tests/phpunit/tests/query/taxQuery.php b/tests/phpunit/tests/query/taxQuery.php index 2ca00b605b5b5..f517e4bb84134 100644 --- a/tests/phpunit/tests/query/taxQuery.php +++ b/tests/phpunit/tests/query/taxQuery.php @@ -1024,8 +1024,7 @@ public function test_tax_query_relation_or_both_clauses_empty_terms() { ) ); - $posts = $query->get_posts(); - $this->assertCount( 0, $posts ); + $this->assertCount( 0, $query->posts ); } /** @@ -1059,8 +1058,7 @@ public function test_tax_query_relation_or_one_clause_empty_terms() { ) ); - $posts = $query->get_posts(); - $this->assertCount( 0, $posts ); + $this->assertCount( 0, $query->posts ); } public function test_tax_query_include_children() { @@ -1209,8 +1207,8 @@ public function test_tax_query_taxonomy_with_attachments() { register_taxonomy_for_object_type( 'post_tag', 'attachment:image' ); $tag_id = self::factory()->term->create( array( - 'slug' => rand_str(), - 'name' => rand_str(), + 'slug' => 'foo-bar', + 'name' => 'Foo Bar', ) ); $image_id = self::factory()->attachment->create_object( @@ -1623,4 +1621,127 @@ public function test_tax_terms_should_not_be_double_escaped() { $this->assertSameSets( array( $p ), $q->posts ); } + + /** + * @ticket 55360 + * + * @covers WP_Tax_Query::transform_query + */ + public function test_tax_terms_should_limit_query() { + register_taxonomy( 'wptests_tax', 'post' ); + $name = 'foobar'; + $t = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_tax', + 'name' => $name, + ) + ); + + $p = self::factory()->post->create(); + wp_set_object_terms( $p, array( $t ), 'wptests_tax' ); + + $filter = new MockAction(); + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $q = new WP_Query( + array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax', + 'field' => 'name', + 'terms' => $name, + ), + ), + ) + ); + + $filter_args = $filter->get_args(); + $query = $filter_args[0][1]->request; + + $this->assertSameSets( array( $p ), $q->posts ); + $this->assertStringContainsString( 'LIMIT 1', $query ); + } + + /** + * @ticket 55360 + * + * @covers WP_Tax_Query::transform_query + */ + public function test_tax_terms_should_limit_query_to_one() { + register_taxonomy( 'wptests_tax', 'post' ); + $name = 'foobar'; + $t = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_tax', + 'name' => $name, + ) + ); + + $p = self::factory()->post->create(); + wp_set_object_terms( $p, array( $t ), 'wptests_tax' ); + + $filter = new MockAction(); + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $q = new WP_Query( + array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax', + 'field' => 'term_id', + 'terms' => array( $t, $t, $t ), + ), + ), + ) + ); + + $filter_args = $filter->get_args(); + $query = $filter_args[0][1]->request; + + $this->assertSameSets( array( $p ), $q->posts ); + $this->assertStringContainsString( 'LIMIT 1', $query ); + } + + /** + * @ticket 55360 + * + * @covers WP_Tax_Query::transform_query + */ + public function test_hierarchical_taxonomies_do_not_limit_query() { + register_taxonomy( 'wptests_tax', 'post', array( 'hierarchical' => true ) ); + $name = 'foobar'; + $t = self::factory()->term->create( + array( + 'taxonomy' => 'wptests_tax', + 'name' => $name, + ) + ); + + $p = self::factory()->post->create(); + wp_set_object_terms( $p, array( $t ), 'wptests_tax' ); + + $filter = new MockAction(); + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $q = new WP_Query( + array( + 'fields' => 'ids', + 'tax_query' => array( + array( + 'taxonomy' => 'wptests_tax', + 'field' => 'name', + 'terms' => $name, + ), + ), + ) + ); + + $filter_args = $filter->get_args(); + $query = $filter_args[0][1]->request; + + $this->assertSameSets( array( $p ), $q->posts ); + $this->assertStringNotContainsString( 'LIMIT 1', $query ); + } } diff --git a/tests/phpunit/tests/query/thePost.php b/tests/phpunit/tests/query/thePost.php new file mode 100644 index 0000000000000..6032a01dbeb1b --- /dev/null +++ b/tests/phpunit/tests/query/thePost.php @@ -0,0 +1,411 @@ +user->create_many( 5, array( 'role' => 'author' ) ); + self::$page_parent_id = $factory->post->create( array( 'post_type' => 'page' ) ); + + // Create child pages. + foreach ( self::$author_ids as $author_id ) { + self::$page_child_ids[] = $factory->post->create( + array( + 'post_type' => 'page', + 'post_parent' => self::$page_parent_id, + 'post_author' => $author_id, + ) + ); + } + } + + /** + * Ensure custom 'fields' values are respected. + * + * @ticket 56992 + */ + public function test_wp_query_respects_custom_fields_values() { + global $wpdb; + add_filter( + 'posts_fields', + function ( $fields, $query ) { + global $wpdb; + + if ( $query->get( 'fields' ) === 'custom' ) { + $fields = "$wpdb->posts.ID,$wpdb->posts.post_author"; + } + + return $fields; + }, + 10, + 2 + ); + + $query = new WP_Query( + array( + 'fields' => 'custom', + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + ) + ); + + $this->assertNotEmpty( $query->posts, 'The query is expected to return results' ); + $this->assertSame( $query->get( 'fields' ), 'custom', 'The WP_Query class is expected to use the custom fields value' ); + $this->assertStringContainsString( "$wpdb->posts.ID,$wpdb->posts.post_author", $query->request, 'The database query is expected to use the custom fields value' ); + } + + /** + * Ensure custom 'fields' populates the global post in the loop. + * + * @ticket 56992 + */ + public function test_wp_query_with_custom_fields_value_populates_the_global_post() { + global $wpdb; + add_filter( + 'posts_fields', + function ( $fields, $query ) { + global $wpdb; + + if ( $query->get( 'fields' ) === 'custom' ) { + $fields = "$wpdb->posts.ID,$wpdb->posts.post_author"; + } + + return $fields; + }, + 10, + 2 + ); + + $query = new WP_Query( + array( + 'fields' => 'custom', + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + 'orderby' => 'id', + 'order' => 'ASC', + ) + ); + + $query->the_post(); + + // Get the global post and specific post. + $global_post = get_post(); + $specific_post = get_post( self::$page_child_ids[0], ARRAY_A ); + + $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' ); + + $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' ); + $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' ); + $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' ); + } + + /** + * Ensure that a secondary loop populates the global post completely regardless of the fields parameter. + * + * @ticket 56992 + * + * @dataProvider data_the_loop_fields + * + * @param string $fields Fields parameter for use in the query. + */ + public function test_the_loop_populates_the_global_post_completely( $fields ) { + $query = new WP_Query( + array( + 'fields' => $fields, + 'post_type' => 'page', + 'page_id' => self::$page_child_ids[0], + ) + ); + + $this->assertNotEmpty( $query->posts, 'The query is expected to return results' ); + + // Start the loop. + $query->the_post(); + + // Get the global post and specific post. + $global_post = get_post(); + $specific_post = get_post( self::$page_child_ids[0], ARRAY_A ); + + $this->assertSameSetsWithIndex( $specific_post, $global_post->to_array(), 'The global post is expected to be fully populated.' ); + + $this->assertNotEmpty( get_the_title(), 'The title is expected to be populated.' ); + $this->assertNotEmpty( get_the_content(), 'The content is expected to be populated.' ); + $this->assertNotEmpty( get_the_excerpt(), 'The excerpt is expected to be populated.' ); + } + + /** + * Ensure that a secondary loop primes the post cache completely regardless of the fields parameter. + * + * @ticket 56992 + * + * @dataProvider data_the_loop_fields + * + * @param string $fields Fields parameter for use in the query. + * @param int $expected_queries Expected number of queries when starting the loop. + */ + public function test_the_loop_primes_the_post_cache( $fields, $expected_queries ) { + $query = new WP_Query( + array( + 'fields' => $fields, + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + ) + ); + + // Start the loop. + $start_queries = get_num_queries(); + $query->the_post(); + $end_queries = get_num_queries(); + /* + * Querying complete posts: 2 queries. + * 1. User meta data. + * 2. User data. + * + * Querying partial posts: 4 queries. + * 1. Post objects + * 2. Post meta data. + * 3. User meta data. + * 4. User data. + */ + $this->assertSame( $expected_queries, $end_queries - $start_queries, "Starting the loop should make $expected_queries db queries." ); + + // Complete the loop. + $start_queries = get_num_queries(); + while ( $query->have_posts() ) { + $query->the_post(); + } + $end_queries = get_num_queries(); + + $this->assertSame( 0, $end_queries - $start_queries, 'The cache is expected to be primed by the loop.' ); + } + + /** + * Ensure that a secondary loop primes the author cache completely regardless of the fields parameter. + * + * @ticket 56992 + * + * @dataProvider data_the_loop_fields + * + * @param string $fields Fields parameter for use in the query. + * @param int $expected_queries Expected number of queries when starting the loop. + */ + public function test_the_loop_primes_the_author_cache( $fields, $expected_queries ) { + $query = new WP_Query( + array( + 'fields' => $fields, + 'post_type' => 'page', + 'post__in' => self::$page_child_ids, + ) + ); + + // Start the loop. + $start_queries = get_num_queries(); + $query->the_post(); + $end_queries = get_num_queries(); + /* + * Querying complete posts: 2 queries. + * 1. User meta data. + * 2. User data. + * + * Querying partial posts: 4 queries. + * 1. Post objects + * 2. Post meta data. + * 3. User meta data. + * 4. User data. + */ + $this->assertSame( $expected_queries, $end_queries - $start_queries, "Starting the loop should make $expected_queries db queries." ); + + // Complete the loop. + $start_queries = get_num_queries(); + while ( $query->have_posts() ) { + $query->the_post(); + get_the_author(); + } + $end_queries = get_num_queries(); + + $this->assertSame( 0, $end_queries - $start_queries, 'The cache is expected to be primed by the loop.' ); + } + + /** + * Data provider for: + * - test_the_loop_populates_the_global_post_completely, + * - test_the_loop_primes_the_post_cache, and, + * - test_the_loop_primes_the_author_cache. + * + * @return array[] + */ + public function data_the_loop_fields() { + return array( + 'all fields' => array( 'all', 2 ), + 'all fields (empty fields)' => array( '', 2 ), + 'post IDs' => array( 'ids', 4 ), + 'post ids and parent' => array( 'id=>parent', 4 ), + ); + } + + /** + * Ensure draft content is shown for post previews and permalinks for logged in users. + * + * @ticket 56992 + */ + public function test_post_preview_links_draft_posts() { + $user_id = self::$author_ids[0]; + wp_set_current_user( $user_id ); + $draft_post = $this->factory()->post->create( + array( + 'post_status' => 'draft', + 'post_author' => $user_id, + 'post_content' => 'ticket 56992', + ) + ); + + // Ensure the global post is populated with the draft content for the preview link. + $this->go_to( get_preview_post_link( $draft_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992', get_the_content(), 'Preview link should show draft content to logged in user' ); + + // Ensure the global post is populated with the draft content for the permalink. + $this->go_to( get_permalink( $draft_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992', get_the_content(), 'Permalink should show draft content to logged in user' ); + + // Ensure the global post is not populated with the draft content for the preview link when logged out. + wp_set_current_user( 0 ); + $this->go_to( get_preview_post_link( $draft_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertEmpty( get_the_content(), 'Preview link should not show draft content to logged out users' ); + + // Ensure the global post is not populated with the draft content for the permalink when logged out. + $this->go_to( get_permalink( $draft_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertEmpty( get_the_content(), 'Permalink should not show draft content to logged out users' ); + } + + /** + * Ensure autosave content is shown for post previews. + * + * @ticket 56992 + */ + public function test_post_preview_links_autosaves() { + $user_id = self::$author_ids[0]; + wp_set_current_user( $user_id ); + $published_post = $this->factory()->post->create( + array( + 'post_status' => 'publish', + 'post_author' => $user_id, + 'post_content' => 'ticket 56992', + ) + ); + + // Create an autosave for the published post. + $autosave = get_post( $published_post, ARRAY_A ); + $autosave['post_ID'] = $published_post; + $autosave['post_content'] = 'ticket 56992 edited'; + wp_create_post_autosave( $autosave ); + + // Set up the preview $_GET parameters. + $nonce = wp_create_nonce( 'post_preview_' . $published_post ); + $query_args['preview_id'] = $published_post; + $query_args['preview_nonce'] = $nonce; + $post_preview_link = get_preview_post_link( $published_post, $query_args ); + + /* + * Set up the GET parameters for the preview link. + * + * _show_post_preview() checks the $_GET super global for preview + * and nonce parameters. It needs to run prior to the global query + * being set up in WP_Query (via $this->go_to()), so the preview + * parameters are created here to ensure _show_post_preview() + * runs correctly. + */ + $_GET['preview_id'] = $published_post; + $_GET['preview_nonce'] = $nonce; + _show_post_preview(); + + // Ensure the global post is populated with the autosave content for the preview link. + $this->go_to( $post_preview_link ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992 edited', get_the_content(), 'Preview link should show autosave content to logged in user' ); + + // Ensure the global post is populated with the published content for the permalink. + $this->go_to( get_permalink( $published_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992', get_the_content(), 'Permalink should show published content to logged in user' ); + + wp_set_current_user( 0 ); + + // New user, new nonce; set up the preview $_GET parameters. + $nonce = wp_create_nonce( 'post_preview_' . $published_post ); + $query_args['preview_id'] = $published_post; + $query_args['preview_nonce'] = $nonce; + $post_preview_link = get_preview_post_link( $published_post, $query_args ); + + /* + * Set up the GET parameters for the preview link. + * + * _show_post_preview() checks the $_GET super global for preview + * and nonce parameters. It needs to run prior to the global query + * being set up in WP_Query (via $this->go_to()), so the preview + * parameters are created here to ensure _show_post_preview() + * runs correctly. + */ + $_GET['preview_id'] = $published_post; + $_GET['preview_nonce'] = $nonce; + _show_post_preview(); + + // Ensure the global post is not populated with the draft content for the preview link when logged out. + $this->go_to( $post_preview_link ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992', get_the_content(), 'Preview link should show published content to logged out users' ); + + // Ensure the global post is not populated with the draft content for the permalink when logged out. + $this->go_to( get_permalink( $published_post ) ); + if ( have_posts() ) { + the_post(); + } + $this->assertSame( 'ticket 56992', get_the_content(), 'Permalink should show published content to logged out users' ); + } +} diff --git a/tests/phpunit/tests/query/vars.php b/tests/phpunit/tests/query/vars.php index 7f336908a233a..87e7fa7bb75c6 100644 --- a/tests/phpunit/tests/query/vars.php +++ b/tests/phpunit/tests/query/vars.php @@ -14,7 +14,7 @@ class Tests_Query_Vars extends WP_UnitTestCase { public function testPublicQueryVarsAreAsExpected() { global $wp; - // Re-initialise any dynamically-added public query vars: + // Re-initialize any dynamically-added public query vars: do_action( 'init' ); $this->assertSame( @@ -80,5 +80,4 @@ public function testPublicQueryVarsAreAsExpected() { 'Care should be taken when introducing new public query vars. See https://core.trac.wordpress.org/ticket/35115' ); } - } diff --git a/tests/phpunit/tests/readme.php b/tests/phpunit/tests/readme.php new file mode 100644 index 0000000000000..bcab29f69a368 --- /dev/null +++ b/tests/phpunit/tests/readme.php @@ -0,0 +1,127 @@ +skipOnAutomatedBranches(); + + $readme = file_get_contents( ABSPATH . 'readme.html' ); + + preg_match( '#Recommendations.*PHP version ([0-9.]*)#s', $readme, $matches ); + + $response_body = $this->get_response_body( 'https://www.php.net/supported-versions.php' ); + + preg_match_all( '#
      \s*]*>\s*([0-9.]*)#s', $response_body, $php_matches ); + + $this->assertContains( $matches[1], $php_matches[1], "readme.html's Recommended PHP version is too old. Remember to update the WordPress.org Requirements page, too." ); + } + + /** + * @coversNothing + */ + public function test_readme_mysql_version() { + // This test is designed to only run on trunk. + $this->skipOnAutomatedBranches(); + + $readme = file_get_contents( ABSPATH . 'readme.html' ); + + preg_match( '#Recommendations.*MySQL version ([0-9.]*)#s', $readme, $matches ); + + $response_body = json_decode( $this->get_response_body( 'https://endoflife.date/api/mysql.json' ) ); + $eol_date = ''; + + foreach ( $response_body as $version ) { + if ( $version->cycle === $matches[1] && false !== $version->eol ) { + $eol_date = $version->eol; + break; + } + } + + /* + * Per https://www.mysql.com/support/, Oracle actively supports MySQL releases for 5 years from GA release. + * + * The currently recommended MySQL 8.0 branch moved from active support to extended support on 2023-04-19. + * As WordPress core may not be fully compatible with MySQL 8.1 at this time, the "supported" period here + * is increased to 8 years to include extended support. + * + * TODO: Reduce this back to 5 years once MySQL 8.1 compatibility is achieved. + */ + $mysql_eol = gmdate( 'Y-m-d', strtotime( $eol_date . ' +8 years' ) ); + $current_date = gmdate( 'Y-m-d' ); + + $this->assertLessThan( $mysql_eol, $current_date, "readme.html's Recommended MySQL version is too old. Remember to update the WordPress.org Requirements page, too." ); + } + + /** + * @coversNothing + */ + public function test_readme_mariadb_version() { + // This test is designed to only run on trunk. + $this->skipOnAutomatedBranches(); + + $readme = file_get_contents( ABSPATH . 'readme.html' ); + + preg_match( '#Recommendations.*MariaDB version ([0-9.]*)#s', $readme, $matches ); + + $response_body = $this->get_response_body( 'https://downloads.mariadb.org/rest-api/mariadb/' ); + $releases = json_decode( $response_body, true ); + + foreach ( $releases['major_releases'] as $release ) { + if ( isset( $release['release_id'] ) && $release['release_id'] === $matches[1] ) { + $mariadb_eol = $release['release_eol_date']; + } + } + + // If the release ID is not found the version is unsupported. + if ( ! isset( $mariadb_eol ) ) { + $this->fail( "{$matches[1]} is not included in MariaDB's list of supported versions. Remember to update the WordPress.org Requirements page, too." ); + } + + $current_date = gmdate( 'Y-m-d' ); + + $this->assertLessThan( $mariadb_eol, $current_date, "readme.html's Recommended MariaDB version is too old. Remember to update the WordPress.org Requirements page, too." ); + } + + /** + * Helper function to retrieve the response body or skip the test on HTTP timeout. + * + * @param string $url The URL to retrieve the response from. + * @return string The response body. + */ + public function get_response_body( $url ) { + $response = $this->wp_remote_get( $url ); + + $this->assertNotWPError( $response ); + + $response_code = wp_remote_retrieve_response_code( $response ); + $response_body = wp_remote_retrieve_body( $response ); + + if ( 200 !== $response_code ) { + $parsed_url = parse_url( $url ); + + $error_message = sprintf( + 'Could not contact %1$s to check versions. Response code: %2$s. Response body: %3$s', + $parsed_url['host'], + $response_code, + $response_body + ); + + if ( 503 === $response_code ) { + $this->markTestSkipped( $error_message ); + } + + $this->fail( $error_message ); + } + + return $response_body; + } +} diff --git a/tests/phpunit/tests/rest-api.php b/tests/phpunit/tests/rest-api.php index acc0e7110c8ab..90de3e13eecea 100644 --- a/tests/phpunit/tests/rest-api.php +++ b/tests/phpunit/tests/rest-api.php @@ -27,6 +27,15 @@ public function tear_down() { parent::tear_down(); } + public function filter_wp_rest_server_class( $class_name ) { + return 'Spy_REST_Server'; + } + + public function test_rest_get_server_fails_with_undefined_method() { + $this->expectException( Error::class ); + rest_get_server()->does_not_exist(); + } + /** * Checks that the main classes are loaded. */ @@ -822,7 +831,14 @@ public function test_always_prepend_path_with_slash_in_rest_url_filter() { unset( $filter ); } - public function jsonp_callback_provider() { + /** + * @dataProvider data_jsonp_callback_check + */ + public function test_jsonp_callback_check( $callback, $expected ) { + $this->assertSame( $expected, wp_check_jsonp_callback( $callback ) ); + } + + public function data_jsonp_callback_check() { return array( // Standard names. array( 'Springfield', true ), @@ -841,13 +857,13 @@ public function jsonp_callback_provider() { } /** - * @dataProvider jsonp_callback_provider + * @dataProvider data_rest_parse_date */ - public function test_jsonp_callback_check( $callback, $valid ) { - $this->assertSame( $valid, wp_check_jsonp_callback( $callback ) ); + public function test_rest_parse_date( $date, $expected ) { + $this->assertEquals( $expected, rest_parse_date( $date ) ); } - public function rest_date_provider() { + public function data_rest_parse_date() { return array( // Valid dates with timezones. array( '2017-01-16T11:30:00-05:00', gmmktime( 11, 30, 0, 1, 16, 2017 ) + 5 * HOUR_IN_SECONDS ), @@ -873,13 +889,13 @@ public function rest_date_provider() { } /** - * @dataProvider rest_date_provider + * @dataProvider data_rest_parse_date_force_utc */ - public function test_rest_parse_date( $string, $value ) { - $this->assertEquals( $value, rest_parse_date( $string ) ); + public function test_rest_parse_date_force_utc( $date, $expected ) { + $this->assertSame( $expected, rest_parse_date( $date, true ) ); } - public function rest_date_force_utc_provider() { + public function data_rest_parse_date_force_utc() { return array( // Valid dates with timezones. array( '2017-01-16T11:30:00-05:00', gmmktime( 11, 30, 0, 1, 16, 2017 ) ), @@ -904,17 +920,6 @@ public function rest_date_force_utc_provider() { ); } - /** - * @dataProvider rest_date_force_utc_provider - */ - public function test_rest_parse_date_force_utc( $string, $value ) { - $this->assertSame( $value, rest_parse_date( $string, true ) ); - } - - public function filter_wp_rest_server_class( $class_name ) { - return 'Spy_REST_Server'; - } - public function test_register_rest_route_without_server() { $GLOBALS['wp_rest_server'] = null; add_filter( 'wp_rest_server_class', array( $this, 'filter_wp_rest_server_class' ) ); @@ -955,30 +960,46 @@ public function test_rest_preload_api_request_with_method() { } /** + * @dataProvider data_rest_preload_api_request_removes_trailing_slashes + * * @ticket 51636 + * @ticket 57048 + * + * @param string $preload_path The path to preload. + * @param array|string $expected_preload_path Expected path after preloading. */ - public function test_rest_preload_api_request_removes_trailing_slashes() { + public function test_rest_preload_api_request_removes_trailing_slashes( $preload_path, $expected_preload_path ) { $rest_server = $GLOBALS['wp_rest_server']; $GLOBALS['wp_rest_server'] = null; - $preload_paths = array( - '/wp/v2/types//', - array( '/wp/v2/media///', 'OPTIONS' ), - '////', - ); - - $preload_data = array_reduce( - $preload_paths, - 'rest_preload_api_request', - array() - ); - - $this->assertSame( array_keys( $preload_data ), array( '/wp/v2/types', 'OPTIONS', '/' ) ); - $this->assertArrayHasKey( '/wp/v2/media', $preload_data['OPTIONS'] ); + $actual_preload_path = rest_preload_api_request( array(), $preload_path ); + if ( '' !== $preload_path ) { + $actual_preload_path = key( $actual_preload_path ); + } + $this->assertSame( $expected_preload_path, $actual_preload_path ); $GLOBALS['wp_rest_server'] = $rest_server; } + /** + * Data provider. + * + * @return array + */ + public static function data_rest_preload_api_request_removes_trailing_slashes() { + return array( + 'no query part' => array( '/wp/v2/types//', '/wp/v2/types' ), + 'no query part, more slashes' => array( '/wp/v2/media///', '/wp/v2/media' ), + 'only slashes' => array( '////', '/' ), + 'empty path' => array( '', array() ), + 'no query parameters' => array( '/wp/v2/types//?////', '/wp/v2/types?' ), + 'no query parameters, with slashes' => array( '/wp/v2/types//?fields////', '/wp/v2/types?fields' ), + 'query parameters with no values' => array( '/wp/v2/types//?fields=////', '/wp/v2/types?fields=' ), + 'single query parameter' => array( '/wp/v2/types//?_fields=foo,bar////', '/wp/v2/types?_fields=foo,bar' ), + 'multiple query parameters' => array( '/wp/v2/types////?_fields=foo,bar&limit=1000////', '/wp/v2/types?_fields=foo,bar&limit=1000' ), + ); + } + /** * @ticket 40614 */ @@ -1783,7 +1804,7 @@ public function test_rest_ensure_response_accepts_wp_error_and_returns_wp_error( } /** - * @dataProvider rest_ensure_response_data_provider + * @dataProvider data_rest_ensure_response_returns_instance_of_wp_rest_response * * @param mixed $response The response passed to rest_ensure_response(). * @param mixed $expected_data The expected data a response should include. @@ -1799,7 +1820,7 @@ public function test_rest_ensure_response_returns_instance_of_wp_rest_response( * * @return array */ - public function rest_ensure_response_data_provider() { + public function data_rest_ensure_response_returns_instance_of_wp_rest_response() { return array( array( null, null ), array( array( 'chocolate' => 'cookies' ), array( 'chocolate' => 'cookies' ) ), @@ -2490,4 +2511,76 @@ public function data_rest_preload_api_request_embeds_links() { array( '', array(), array() ), ); } + + /** + * @ticket 55213 + */ + public function test_rest_preload_api_request_fields() { + $preload_paths = array( + '/', + '/?_fields=description', + ); + + $preload_data = array_reduce( + $preload_paths, + 'rest_preload_api_request', + array() + ); + + $this->assertSame( array_keys( $preload_data ), array( '/', '/?_fields=description' ) ); + + // Unfiltered request has all fields + $this->assertArrayHasKey( 'description', $preload_data['/']['body'] ); + $this->assertArrayHasKey( 'routes', $preload_data['/']['body'] ); + + // Filtered request only has the desired fields. + $this->assertSame( + array_keys( $preload_data['/?_fields=description']['body'] ), + array( 'description' ) + ); + } + + /** + * @ticket 51986 + */ + public function test_route_args_is_array_of_arrays() { + $this->setExpectedIncorrectUsage( 'register_rest_route' ); + + $registered = register_rest_route( + 'my-ns/v1', + '/my-route', + array( + 'callback' => '__return_true', + 'permission_callback' => '__return_true', + 'args' => array( 'pattern' ), + ) + ); + + $this->assertTrue( $registered ); + } + + /** + * @ticket 62932 + */ + public function test_should_return_error_if_rest_route_not_string() { + global $wp; + + $wp = new stdClass(); + + $wp->query_vars = array( + 'rest_route' => array( 'invalid' ), + ); + + $this->expectException( WPDieException::class ); + + try { + rest_api_loaded(); + } catch ( WPDieException $e ) { + $this->assertStringContainsString( + 'The REST route parameter must be a string.', + $e->getMessage() + ); + throw $e; // Re-throw to satisfy expectException + } + } } diff --git a/tests/phpunit/tests/rest-api/application-passwords.php b/tests/phpunit/tests/rest-api/application-passwords.php index b3edab0d72d79..65e3bf222d85f 100644 --- a/tests/phpunit/tests/rest-api/application-passwords.php +++ b/tests/phpunit/tests/rest-api/application-passwords.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi * @group app_password */ @@ -79,14 +77,6 @@ public function data_create_new_application_password_validation() { ), 'args' => array( 'name' => '' ), ), - 'application_password_duplicate_name when name exists' => array( - 'expected' => array( - 'error_code' => 'application_password_duplicate_name', - 'error_message' => 'Each application name should be unique.', - ), - 'args' => array( 'name' => 'test2' ), - 'names' => array( 'test1', 'test2' ), - ), ); } @@ -164,7 +154,7 @@ public function test_update_application_password( array $update, array $existing // Check updated only given values. $updated_item = WP_Application_Passwords::get_user_application_password( self::$user_id, $uuid ); foreach ( $updated_item as $key => $update_value ) { - $expected_value = isset( $update[ $key ] ) ? $update[ $key ] : $original_item[ $key ]; + $expected_value = $update[ $key ] ?? $original_item[ $key ]; $this->assertSame( $expected_value, $update_value ); } } @@ -198,4 +188,14 @@ public function data_update_application_password() { ), ); } + + /** + * @ticket 51941 + */ + public function test_can_create_duplicate_app_password_names() { + $created = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'My App' ) ); + $this->assertNotWPError( $created, 'First attempt to create an application password should not return an error' ); + $created = WP_Application_Passwords::create_new_application_password( self::$user_id, array( 'name' => 'My App' ) ); + $this->assertNotWPError( $created, 'Second attempt to create an application password should not return an error' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php index cbfe965fa1a3e..060a5c0912a94 100644 --- a/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php +++ b/tests/phpunit/tests/rest-api/rest-application-passwords-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Application_Passwords_Controller extends WP_Test_REST_Controller_Testcase { @@ -409,6 +407,32 @@ public function test_create_item_invalid_user_id() { $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); } + /** + * @ticket 53224 + * @group ms-required + */ + public function test_create_item_for_super_admin_on_site_where_they_are_not_a_member() { + wp_set_current_user( self::$admin ); + + // Create a site where the Super Admin is not a member. + $blog_id = self::factory()->blog->create( + array( + 'user_id' => self::$subscriber_id, + ) + ); + + switch_to_blog( $blog_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' ); + $request->set_body_params( array( 'name' => 'App' ) ); + $response = rest_do_request( $request ); + + restore_current_blog(); + + $this->assertNotWPError( $response ); + $this->assertSame( 201, $response->get_status() ); + } + /** * @ticket 51939 */ @@ -824,7 +848,50 @@ public function test_prepare_item() { } /** - * Checks the password response matches the exepcted format. + * @ticket 53692 + */ + public function test_create_item_with_empty_app_id() { + wp_set_current_user( self::$admin ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' ); + $request->set_body_params( + array( + 'name' => 'Test', + 'app_id' => '', + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( '', $data['app_id'] ); + } + + /** + * @ticket 53692 + */ + public function test_create_item_with_uuid_app_id() { + wp_set_current_user( self::$admin ); + + $uuid = wp_generate_uuid4(); + $request = new WP_REST_Request( 'POST', '/wp/v2/users/me/application-passwords' ); + $request->set_body_params( + array( + 'name' => 'Test', + 'app_id' => $uuid, + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + $this->assertSame( $uuid, $data['app_id'] ); + } + + /** + * Checks the password response matches the expected format. * * @since 5.6.0 * @@ -937,7 +1004,7 @@ public function test_introspect_item_password_invalid() { $this->setup_app_password_authenticated_request(); add_action( 'application_password_did_authenticate', - static function() { + static function () { $GLOBALS['wp_rest_application_password_uuid'] = 'invalid_uuid'; } ); @@ -946,6 +1013,86 @@ static function() { $this->assertErrorResponse( 'rest_application_password_not_found', $response, 500 ); } + /** + * @ticket 53658 + * + * @covers ::wp_is_application_passwords_supported + */ + public function test_wp_is_application_passwords_supported_with_https_only() { + $_SERVER['HTTPS'] = 'on'; + $this->assertTrue( wp_is_application_passwords_supported() ); + } + + /** + * @ticket 53658 + * + * @covers ::wp_is_application_passwords_supported + */ + public function test_wp_is_application_passwords_supported_with_local_environment_only() { + putenv( 'WP_ENVIRONMENT_TYPE=local' ); + + $actual = wp_is_application_passwords_supported(); + + // Revert to default behavior so that other tests are not affected. + putenv( 'WP_ENVIRONMENT_TYPE' ); + + $this->assertTrue( $actual ); + } + + /** + * @dataProvider data_wp_is_application_passwords_available + * + * @ticket 53658 + * + * @covers ::wp_is_application_passwords_available + * + * @param bool|string $expected The expected value. + * @param string|null $callback Optional. The callback for the `wp_is_application_passwords_available` hook. + * Default: null. + */ + public function test_wp_is_application_passwords_available( $expected, $callback = null ) { + remove_filter( 'wp_is_application_passwords_available', '__return_true' ); + + if ( $callback ) { + add_filter( 'wp_is_application_passwords_available', $callback ); + } + + if ( 'default' === $expected ) { + putenv( 'WP_ENVIRONMENT_TYPE=local' ); + $expected = wp_is_application_passwords_supported(); + } + + $actual = wp_is_application_passwords_available(); + + if ( 'default' === $expected ) { + // Revert to default behavior so that other tests are not affected. + putenv( 'WP_ENVIRONMENT_TYPE' ); + } + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_is_application_passwords_available() { + return array( + 'availability not forced' => array( + 'expected' => 'default', + ), + 'availability forced true' => array( + 'expected' => true, + 'callback' => '__return_true', + ), + 'availability forced false' => array( + 'expected' => false, + 'callback' => '__return_false', + ), + ); + } + /** * Sets up a REST API request to be authenticated using an App Password. * diff --git a/tests/phpunit/tests/rest-api/rest-attachments-controller.php b/tests/phpunit/tests/rest-api/rest-attachments-controller.php index 14c1f080751a1..c8746931ed30a 100644 --- a/tests/phpunit/tests/rest-api/rest-attachments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-attachments-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Controller_Testcase { @@ -22,12 +20,42 @@ class WP_Test_REST_Attachments_Controller extends WP_Test_REST_Post_Type_Control /** * @var string The path to a test file. */ - private $test_file; + private static $test_file; /** * @var string The path to a second test file. */ - private $test_file2; + private static $test_file2; + + /** + * @var string The path to the AVIF test image. + */ + private static $test_avif_file; + + /** + * @var string The path to the SVG test image. + */ + private static $test_svg_file; + + /** + * @var string The path to the test video. + */ + private static $test_video_file; + + /** + * @var string The path to the test audio. + */ + private static $test_audio_file; + + /** + * @var string The path to the test RTF file. + */ + private static $test_rtf_file; + + /** + * @var array The recorded posts query clauses. + */ + protected $posts_clauses; public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$superadmin_id = $factory->user->create( @@ -63,6 +91,25 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { } public static function wpTearDownAfterClass() { + if ( file_exists( self::$test_file ) ) { + unlink( self::$test_file ); + } + if ( file_exists( self::$test_file2 ) ) { + unlink( self::$test_file2 ); + } + if ( file_exists( self::$test_avif_file ) ) { + unlink( self::$test_avif_file ); + } + if ( file_exists( self::$test_video_file ) ) { + unlink( self::$test_video_file ); + } + if ( file_exists( self::$test_audio_file ) ) { + unlink( self::$test_audio_file ); + } + if ( file_exists( self::$test_rtf_file ) ) { + unlink( self::$test_rtf_file ); + } + self::delete_user( self::$editor_id ); self::delete_user( self::$author_id ); self::delete_user( self::$contributor_id ); @@ -80,21 +127,62 @@ public function set_up() { $role->add_cap( 'level_0' ); $orig_file = DIR_TESTDATA . '/images/canola.jpg'; - $this->test_file = get_temp_dir() . 'canola.jpg'; - copy( $orig_file, $this->test_file ); + self::$test_file = get_temp_dir() . 'canola.jpg'; + if ( ! file_exists( self::$test_file ) ) { + copy( $orig_file, self::$test_file ); + } + $orig_file2 = DIR_TESTDATA . '/images/codeispoetry.png'; - $this->test_file2 = get_temp_dir() . 'codeispoetry.png'; - copy( $orig_file2, $this->test_file2 ); - } + self::$test_file2 = get_temp_dir() . 'codeispoetry.png'; + if ( ! file_exists( self::$test_file2 ) ) { + copy( $orig_file2, self::$test_file2 ); + } - public function tear_down() { - if ( file_exists( $this->test_file ) ) { - unlink( $this->test_file ); + $orig_avif_file = DIR_TESTDATA . '/images/avif-lossy.avif'; + self::$test_avif_file = get_temp_dir() . 'avif-lossy.avif'; + if ( ! file_exists( self::$test_avif_file ) ) { + copy( $orig_avif_file, self::$test_avif_file ); + } + + $test_svg_file = DIR_TESTDATA . '/uploads/video-play.svg'; + self::$test_svg_file = get_temp_dir() . 'video-play.svg'; + if ( ! file_exists( self::$test_svg_file ) ) { + copy( $test_svg_file, self::$test_svg_file ); + } + + $test_video_file = DIR_TESTDATA . '/uploads/small-video.mp4'; + self::$test_video_file = get_temp_dir() . 'small-video.mp4'; + if ( ! file_exists( self::$test_video_file ) ) { + copy( $test_video_file, self::$test_video_file ); + } + + $test_audio_file = DIR_TESTDATA . '/uploads/small-audio.mp3'; + self::$test_audio_file = get_temp_dir() . 'small-audio.mp3'; + if ( ! file_exists( self::$test_audio_file ) ) { + copy( $test_audio_file, self::$test_audio_file ); } - if ( file_exists( $this->test_file2 ) ) { - unlink( $this->test_file2 ); + + $test_rtf_file = DIR_TESTDATA . '/uploads/test.rtf'; + self::$test_rtf_file = get_temp_dir() . 'test.rtf'; + if ( ! file_exists( self::$test_rtf_file ) ) { + copy( $test_rtf_file, self::$test_rtf_file ); } + add_filter( 'rest_pre_dispatch', array( $this, 'wpSetUpBeforeRequest' ), 10, 3 ); + add_filter( 'posts_clauses', array( $this, 'save_posts_clauses' ), 10, 2 ); + } + + public function wpSetUpBeforeRequest( $result ) { + $this->posts_clauses = array(); + return $result; + } + + public function save_posts_clauses( $clauses ) { + $this->posts_clauses[] = $clauses; + return $clauses; + } + + public function tear_down() { $this->remove_added_uploads(); if ( class_exists( WP_Image_Editor_Mock::class ) ) { @@ -114,7 +202,16 @@ public function test_register_routes() { $this->assertCount( 3, $routes['/wp/v2/media/(?P[\d]+)'] ); } - public static function disposition_provider() { + /** + * @dataProvider data_parse_disposition + */ + public function test_parse_disposition( $header, $expected ) { + $header_list = array( $header ); + $parsed = WP_REST_Attachments_Controller::get_filename_from_disposition( $header_list ); + $this->assertSame( $expected, $parsed ); + } + + public static function data_parse_disposition() { return array( // Types. array( 'attachment; filename="foo.jpg"', 'foo.jpg' ), @@ -146,25 +243,17 @@ public static function disposition_provider() { ); } - /** - * @dataProvider disposition_provider - */ - public function test_parse_disposition( $header, $expected ) { - $header_list = array( $header ); - $parsed = WP_REST_Attachments_Controller::get_filename_from_disposition( $header_list ); - $this->assertSame( $expected, $parsed ); - } - public function test_context_param() { // Collection. $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -174,6 +263,7 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/media/' . $attachment_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][0] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } @@ -205,6 +295,8 @@ public function test_registered_query_params() { 'parent_exclude', 'per_page', 'search', + 'search_columns', + 'search_semantics', 'slug', 'status', ), @@ -215,16 +307,14 @@ public function test_registered_query_params() { 'video', 'image', 'audio', + 'text', ); - if ( ! is_multisite() ) { - $media_types[] = 'text'; - } - $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['enum'] ); + $this->assertSameSets( $media_types, $data['endpoints'][0]['args']['media_type']['items']['enum'] ); } public function test_registered_get_item_params() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -235,16 +325,15 @@ public function test_registered_get_item_params() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $keys = array_keys( $data['endpoints'][0]['args'] ); - sort( $keys ); - $this->assertSame( array( 'context', 'id' ), $keys ); + $this->assertEqualSets( array( 'context', 'id' ), $keys ); } /** * @ticket 43701 */ public function test_allow_header_sent_on_options_request() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -271,26 +360,26 @@ public function test_allow_header_sent_on_options_request() { public function test_get_items() { wp_set_current_user( 0 ); - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) ); - $id2 = $this->factory->attachment->create_object( - $this->test_file, + $draft_post = self::factory()->post->create( array( 'post_status' => 'draft' ) ); + $id2 = self::factory()->attachment->create_object( + self::$test_file, $draft_post, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->attachment->create_object( - $this->test_file, + $published_post = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->attachment->create_object( + self::$test_file, $published_post, array( 'post_mime_type' => 'image/jpeg', @@ -311,26 +400,26 @@ public function test_get_items() { public function test_get_items_logged_in_editor() { wp_set_current_user( self::$editor_id ); - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) ); - $id2 = $this->factory->attachment->create_object( - $this->test_file, + $draft_post = self::factory()->post->create( array( 'post_status' => 'draft' ) ); + $id2 = self::factory()->attachment->create_object( + self::$test_file, $draft_post, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $published_post = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->attachment->create_object( - $this->test_file, + $published_post = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->attachment->create_object( + self::$test_file, $published_post, array( 'post_mime_type' => 'image/jpeg', @@ -349,8 +438,8 @@ public function test_get_items_logged_in_editor() { } public function test_get_items_media_type() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -371,9 +460,236 @@ public function test_get_items_media_type() { $this->assertSame( $id1, $data[0]['id'] ); } + /** + * Test multiple media types support with various input formats. + * + * @ticket 63668 + */ + public function test_get_items_multiple_media_types() { + $image_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + ) + ); + + $video_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + ) + ); + + $audio_id = self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + + // Test single media type. + $request->set_param( 'media_type', 'image' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 1, $data, 'Response count for single media type is not 1' ); + $this->assertSame( $image_id, $data[0]['id'], 'Image ID not found in response for single media type' ); + + // Test multiple media types with comma-separated string. + $request->set_param( 'media_type', 'image,video' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple media types with comma-separated string is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with comma-separated string' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with comma-separated string' ); + $this->assertNotContains( $audio_id, $ids, 'Audio ID found in response for multiple media types with comma-separated string' ); + + // Test multiple media types with array format. + $request->set_param( 'media_type', array( 'image', 'video', 'audio' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 3, $data, 'Response count for multiple media types with array format is not 3' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $image_id, $ids, 'Image ID not found in response for multiple media types with array format' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response for multiple media types with array format' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response for multiple media types with array format' ); + + // Test invalid media type mixed with valid ones. + $request->set_param( 'media_type', 'image,invalid,video' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * Test multiple MIME types support and combination with media types. + * + * @ticket 63668 + */ + public function test_get_items_multiple_mime_types_and_combination() { + $jpeg_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + ) + ); + + $png_id = self::factory()->attachment->create_object( + self::$test_file2, + 0, + array( + 'post_mime_type' => 'image/png', + ) + ); + + $mp4_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + + // Test single MIME type + $request->set_param( 'mime_type', 'image/jpeg' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 1, $data, 'Response count for single MIME type is not 1' ); + $this->assertSame( $jpeg_id, $data[0]['id'], 'JPEG ID not found in response for single MIME type' ); + + // Test multiple MIME types with comma-separated string. + $request->set_param( 'mime_type', 'image/jpeg,image/png' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple MIME types with comma-separated string is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with comma-separated string' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response for multiple MIME types with comma-separated string' ); + + // Test multiple MIME types with array format. + $request->set_param( 'mime_type', array( 'image/jpeg', 'video/mp4' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response count for multiple MIME types with array format is not 2' ); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response for multiple MIME types with array format' ); + $this->assertContains( $mp4_id, $ids, 'MP4 ID not found in response for multiple MIME types with array format' ); + } + + /** + * Test combination of media type and mime type parameters. + * + * @ticket 63668 + */ + public function test_get_items_with_media_type_and_media_types() { + $audio_id = self::factory()->attachment->create_object( + self::$test_audio_file, + 0, + array( + 'post_mime_type' => 'audio/mpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $jpeg_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $png_id = self::factory()->attachment->create_object( + self::$test_file2, + 0, + array( + 'post_mime_type' => 'image/png', + ) + ); + + $video_id = self::factory()->attachment->create_object( + self::$test_video_file, + 0, + array( + 'post_mime_type' => 'video/mp4', + ) + ); + + $rtf_id = self::factory()->attachment->create_object( + self::$test_rtf_file, + 0, + array( + 'post_mime_type' => 'application/rtf', + ) + ); + + // Test combination of single media type and single mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'image' ); + $request->set_param( 'mime_type', 'audio/mpeg' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of single media type and single mime type parameters is not 3' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + $this->assertContains( $audio_id, $ids, 'Audio ID found in response' ); + + // Test combination of single media type and multiple mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio' ); + $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of single media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + + // Test combination of multiple media types and single mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio,video' ); + $request->set_param( 'mime_type', array( 'image/jpeg' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 3, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response' ); + + // Test combination of multiple media types and multiple mime type parameters. + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'audio,video' ); + $request->set_param( 'mime_type', array( 'image/jpeg', 'image/png', 'application/rtf' ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 5, $data, 'Response count for combination of multiple media type and multiple mime type parameters is not 3' ); + $this->assertContains( $audio_id, $ids, 'Audio ID not found in response' ); + $this->assertContains( $jpeg_id, $ids, 'JPEG ID not found in response' ); + $this->assertContains( $video_id, $ids, 'Video ID not found in response' ); + $this->assertContains( $png_id, $ids, 'PNG ID not found in response' ); + $this->assertContains( $rtf_id, $ids, 'RTF ID not found in response' ); + } + public function test_get_items_mime_type() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -395,17 +711,17 @@ public function test_get_items_mime_type() { } public function test_get_items_parent() { - $post_id = $this->factory->post->create( array( 'post_title' => 'Test Post' ) ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $post_id = self::factory()->post->create( array( 'post_title' => 'Test Post' ) ); + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, $post_id, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ) ); - $attachment_id2 = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id2 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -440,8 +756,8 @@ public function test_get_items_parent() { public function test_get_items_invalid_status_param_is_error_response() { wp_set_current_user( self::$editor_id ); - $this->factory->attachment->create_object( - $this->test_file, + self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -458,8 +774,8 @@ public function test_get_items_invalid_status_param_is_error_response() { public function test_get_items_private_status() { // Logged out users can't make the request. wp_set_current_user( 0 ); - $attachment_id1 = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -482,8 +798,8 @@ public function test_get_items_private_status() { public function test_get_items_multiple_statuses() { // Logged out users can't make the request. wp_set_current_user( 0 ); - $attachment_id1 = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -491,8 +807,8 @@ public function test_get_items_multiple_statuses() { 'post_status' => 'private', ) ); - $attachment_id2 = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id2 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -520,15 +836,15 @@ public function test_get_items_multiple_statuses() { public function test_get_items_invalid_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'after', rand_str() ); - $request->set_param( 'before', rand_str() ); + $request->set_param( 'after', 'foo' ); + $request->set_param( 'before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function test_get_items_valid_date() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-15T00:00:00Z', @@ -536,8 +852,8 @@ public function test_get_items_valid_date() { 'post_excerpt' => 'A sample caption', ) ); - $id2 = $this->factory->attachment->create_object( - $this->test_file, + $id2 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-16T00:00:00Z', @@ -545,8 +861,8 @@ public function test_get_items_valid_date() { 'post_excerpt' => 'A sample caption', ) ); - $id3 = $this->factory->attachment->create_object( - $this->test_file, + $id3 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-17T00:00:00Z', @@ -568,8 +884,8 @@ public function test_get_items_valid_date() { */ public function test_get_items_invalid_modified_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); - $request->set_param( 'modified_after', rand_str() ); - $request->set_param( 'modified_before', rand_str() ); + $request->set_param( 'modified_after', 'foo' ); + $request->set_param( 'modified_before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } @@ -578,8 +894,8 @@ public function test_get_items_invalid_modified_date() { * @ticket 50617 */ public function test_get_items_valid_modified_date() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-01 00:00:00', @@ -587,8 +903,8 @@ public function test_get_items_valid_modified_date() { 'post_excerpt' => 'A sample caption', ) ); - $id2 = $this->factory->attachment->create_object( - $this->test_file, + $id2 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-02 00:00:00', @@ -596,8 +912,8 @@ public function test_get_items_valid_modified_date() { 'post_excerpt' => 'A sample caption', ) ); - $id3 = $this->factory->attachment->create_object( - $this->test_file, + $id3 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_date' => '2016-01-03 00:00:00', @@ -617,9 +933,51 @@ public function test_get_items_valid_modified_date() { $this->assertSame( $id2, $data[0]['id'] ); } + /** + * @ticket 55677 + */ + public function test_get_items_avoid_duplicated_count_query_if_no_items() { + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'video' ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertCount( 1, $this->posts_clauses ); + + $headers = $response->get_headers(); + + $this->assertSame( 0, $headers['X-WP-Total'] ); + $this->assertSame( 0, $headers['X-WP-TotalPages'] ); + } + + /** + * @ticket 55677 + */ + public function test_get_items_with_empty_page_runs_count_query_after() { + self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_date' => '2022-06-12T00:00:00Z', + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + $request->set_param( 'media_type', 'image' ); + $request->set_param( 'page', 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertCount( 2, $this->posts_clauses ); + + $this->assertErrorResponse( 'rest_post_invalid_page_number', $response, 400 ); + } + public function test_get_item() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -638,18 +996,18 @@ public function test_get_item() { * @requires function imagejpeg */ public function test_get_item_sizes() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ), - $this->test_file + self::$test_file ); add_image_size( 'rest-api-test', 119, 119, true ); - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) ); + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, self::$test_file ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/media/' . $attachment_id ); $response = rest_get_server()->dispatch( $request ); @@ -669,18 +1027,18 @@ public function test_get_item_sizes() { * @requires function imagejpeg */ public function test_get_item_sizes_with_no_url() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', ), - $this->test_file + self::$test_file ); add_image_size( 'rest-api-test', 119, 119, true ); - wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, $this->test_file ) ); + wp_update_attachment_metadata( $attachment_id, wp_generate_attachment_metadata( $attachment_id, self::$test_file ) ); add_filter( 'wp_get_attachment_image_src', '__return_false' ); @@ -696,9 +1054,9 @@ public function test_get_item_sizes_with_no_url() { public function test_get_item_private_post_not_authenticated() { wp_set_current_user( 0 ); - $draft_post = $this->factory->post->create( array( 'post_status' => 'draft' ) ); - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $draft_post = self::factory()->post->create( array( 'post_status' => 'draft' ) ); + $id1 = self::factory()->attachment->create_object( + self::$test_file, $draft_post, array( 'post_mime_type' => 'image/jpeg', @@ -711,8 +1069,8 @@ public function test_get_item_private_post_not_authenticated() { } public function test_get_item_inherit_status_with_invalid_parent() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, array( 'post_mime_type' => 'image/jpeg', @@ -728,8 +1086,8 @@ public function test_get_item_inherit_status_with_invalid_parent() { } public function test_get_item_auto_status_with_invalid_parent_not_authenticated_returns_error() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, array( 'post_mime_type' => 'image/jpeg', @@ -757,7 +1115,7 @@ public function test_create_item() { $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -781,14 +1139,14 @@ public function test_create_item_default_filename_title() { $request->set_file_params( array( 'file' => array( - 'file' => file_get_contents( $this->test_file2 ), + 'file' => file_get_contents( self::$test_file2 ), 'name' => 'codeispoetry.png', - 'size' => filesize( $this->test_file2 ), - 'tmp_name' => $this->test_file2, + 'size' => filesize( self::$test_file2 ), + 'tmp_name' => self::$test_file2, ), ) ); - $request->set_header( 'Content-MD5', md5_file( $this->test_file2 ) ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file2 ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 201, $response->get_status() ); $data = $response->get_data(); @@ -804,14 +1162,14 @@ public function test_create_item_with_files() { $request->set_file_params( array( 'file' => array( - 'file' => file_get_contents( $this->test_file ), + 'file' => file_get_contents( self::$test_file ), 'name' => 'canola.jpg', - 'size' => filesize( $this->test_file ), - 'tmp_name' => $this->test_file, + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, ), ) ); - $request->set_header( 'Content-MD5', md5_file( $this->test_file ) ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 201, $response->get_status() ); } @@ -825,14 +1183,14 @@ public function test_create_item_with_upload_files_role() { $request->set_file_params( array( 'file' => array( - 'file' => file_get_contents( $this->test_file ), + 'file' => file_get_contents( self::$test_file ), 'name' => 'canola.jpg', - 'size' => filesize( $this->test_file ), - 'tmp_name' => $this->test_file, + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, ), ) ); - $request->set_header( 'Content-MD5', md5_file( $this->test_file ) ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 201, $response->get_status() ); } @@ -847,7 +1205,7 @@ public function test_create_item_empty_body() { public function test_create_item_missing_content_type() { wp_set_current_user( self::$author_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_upload_no_content_type', $response, 400 ); } @@ -856,7 +1214,7 @@ public function test_create_item_missing_content_disposition() { wp_set_current_user( self::$author_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_upload_no_content_disposition', $response, 400 ); } @@ -867,7 +1225,7 @@ public function test_create_item_bad_md5_header() { $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); $request->set_header( 'Content-MD5', 'abc123' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_upload_hash_mismatch', $response, 412 ); } @@ -878,10 +1236,10 @@ public function test_create_item_with_files_bad_md5_header() { $request->set_file_params( array( 'file' => array( - 'file' => file_get_contents( $this->test_file ), + 'file' => file_get_contents( self::$test_file ), 'name' => 'canola.jpg', - 'size' => filesize( $this->test_file ), - 'tmp_name' => $this->test_file, + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, ), ) ); @@ -898,7 +1256,7 @@ public function test_create_item_invalid_upload_files_capability() { } public function test_create_item_invalid_edit_permissions() { - $post_id = $this->factory->post->create( array( 'post_author' => self::$editor_id ) ); + $post_id = self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); wp_set_current_user( self::$author_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_param( 'post', $post_id ); @@ -907,7 +1265,7 @@ public function test_create_item_invalid_edit_permissions() { } public function test_create_item_invalid_upload_permissions() { - $post_id = $this->factory->post->create( array( 'post_author' => self::$editor_id ) ); + $post_id = self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); wp_set_current_user( self::$uploader_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_param( 'post', $post_id ); @@ -916,7 +1274,7 @@ public function test_create_item_invalid_upload_permissions() { } public function test_create_item_invalid_post_type() { - $attachment_id = $this->factory->post->create( + $attachment_id = self::factory()->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', @@ -927,7 +1285,7 @@ public function test_create_item_invalid_post_type() { $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $request->set_param( 'post', $attachment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -942,7 +1300,7 @@ public function test_create_item_alt_text() { $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $request->set_param( 'alt_text', 'test alt text' ); $response = rest_get_server()->dispatch( $request ); $attachment = $response->get_data(); @@ -957,7 +1315,7 @@ public function test_create_item_unsafe_alt_text() { $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $request->set_param( 'alt_text', '' ); $response = rest_get_server()->dispatch( $request ); $attachment = $response->get_data(); @@ -973,24 +1331,117 @@ public function test_create_item_ensure_relative_path() { $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $attachment = $response->get_data(); $this->assertStringNotContainsString( ABSPATH, get_post_meta( $attachment['id'], '_wp_attached_file', true ) ); } - public function test_update_item() { + /** + * @ticket 57897 + * + * @requires function imagejpeg + */ + public function test_create_item_with_terms() { + wp_set_current_user( self::$author_id ); + register_taxonomy_for_object_type( 'category', 'attachment' ); + $category = wp_insert_term( 'Media Category', 'category' ); + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + + $request->set_body( file_get_contents( self::$test_file ) ); + $request->set_param( 'categories', array( $category['term_id'] ) ); + $response = rest_get_server()->dispatch( $request ); + $attachment = $response->get_data(); + + $term = wp_get_post_terms( $attachment['id'], 'category' ); + $this->assertSame( $category['term_id'], $term[0]->term_id ); + } + + /** + * @ticket 41692 + */ + public function test_create_update_post_with_featured_media() { + // Add support for thumbnails on all attachment types to avoid incorrect-usage notice. + add_post_type_support( 'attachment', 'thumbnail' ); + wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, - 0, + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_file_params( array( - 'post_mime_type' => 'image/jpeg', - 'post_excerpt' => 'A sample caption', - 'post_author' => self::$editor_id, + 'file' => array( + 'file' => file_get_contents( self::$test_file ), + 'name' => 'canola.jpg', + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, + ), ) ); - $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); + + $file = DIR_TESTDATA . '/images/canola.jpg'; + $attachment_id = self::factory()->attachment->create_object( + $file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'menu_order' => rand( 1, 100 ), + ) + ); + + $request->set_param( 'featured_media', $attachment_id ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 201, $response->get_status() ); + + $new_attachment = get_post( $data['id'] ); + + $this->assertSame( $attachment_id, get_post_thumbnail_id( $new_attachment->ID ) ); + $this->assertSame( $attachment_id, $data['featured_media'] ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/media/' . $new_attachment->ID ); + $params = $this->set_post_data( + array( + 'featured_media' => 0, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 0, $data['featured_media'] ); + $this->assertSame( 0, get_post_thumbnail_id( $new_attachment->ID ) ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/media/' . $new_attachment->ID ); + $params = $this->set_post_data( + array( + 'featured_media' => $attachment_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( $attachment_id, $data['featured_media'] ); + $this->assertSame( $attachment_id, get_post_thumbnail_id( $new_attachment->ID ) ); + } + + public function test_update_item() { + wp_set_current_user( self::$editor_id ); + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption', + 'post_author' => self::$editor_id, + ) + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id ); $request->set_param( 'title', 'My title is very cool' ); $request->set_param( 'caption', 'This is a better caption.' ); $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); @@ -1010,21 +1461,21 @@ public function test_update_item() { public function test_update_item_parent() { wp_set_current_user( self::$editor_id ); - $original_parent = $this->factory->post->create( array() ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $original_parent = self::factory()->post->create( array() ); + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, $original_parent, array( 'post_mime_type' => 'image/jpeg', 'post_excerpt' => 'A sample caption', - 'post_author' => $this->editor_id, + 'post_author' => self::$editor_id, ) ); $attachment = get_post( $attachment_id ); $this->assertSame( $original_parent, $attachment->post_parent ); - $new_parent = $this->factory->post->create( array() ); + $new_parent = self::factory()->post->create( array() ); $request = new WP_REST_Request( 'POST', '/wp/v2/media/' . $attachment_id ); $request->set_param( 'post', $new_parent ); rest_get_server()->dispatch( $request ); @@ -1035,8 +1486,8 @@ public function test_update_item_parent() { public function test_update_item_invalid_permissions() { wp_set_current_user( self::$author_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1051,7 +1502,7 @@ public function test_update_item_invalid_permissions() { } public function test_update_item_invalid_post_type() { - $attachment_id = $this->factory->post->create( + $attachment_id = self::factory()->post->create( array( 'post_type' => 'attachment', 'post_status' => 'inherit', @@ -1059,8 +1510,8 @@ public function test_update_item_invalid_post_type() { ) ); wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1081,7 +1532,7 @@ public function test_update_item_with_existing_inherit_status() { wp_set_current_user( self::$editor_id ); $parent_id = self::factory()->post->create( array() ); $attachment_id = self::factory()->attachment->create_object( - $this->test_file, + self::$test_file, $parent_id, array( 'post_mime_type' => 'image/jpeg', @@ -1104,7 +1555,7 @@ public function test_update_item_with_existing_inherit_status() { public function test_update_item_with_new_inherit_status() { wp_set_current_user( self::$editor_id ); $attachment_id = self::factory()->attachment->create_object( - $this->test_file, + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1126,7 +1577,7 @@ public function verify_attachment_roundtrip( $input = array(), $expected_output $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); foreach ( $input as $name => $value ) { $request->set_param( $name, $value ); @@ -1191,7 +1642,17 @@ public function verify_attachment_roundtrip( $input = array(), $expected_output $this->assertSame( $expected_output['caption']['raw'], $post->post_excerpt ); } - public static function attachment_roundtrip_provider() { + /** + * @dataProvider data_attachment_roundtrip_as_author + * @requires function imagejpeg + */ + public function test_attachment_roundtrip_as_author( $raw, $expected ) { + wp_set_current_user( self::$author_id ); + $this->assertFalse( current_user_can( 'unfiltered_html' ) ); + $this->verify_attachment_roundtrip( $raw, $expected ); + } + + public static function data_attachment_roundtrip_as_author() { return array( array( // Raw values. @@ -1276,28 +1737,18 @@ public static function attachment_roundtrip_provider() { 'rendered' => 'link', ), 'description' => array( - 'raw' => 'link', - 'rendered' => '

      link

      ', + 'raw' => 'link', + 'rendered' => '

      link

      ', ), 'caption' => array( - 'raw' => 'link', - 'rendered' => '

      link

      ', + 'raw' => 'link', + 'rendered' => '

      link

      ', ), ), ), ); } - /** - * @dataProvider attachment_roundtrip_provider - * @requires function imagejpeg - */ - public function test_post_roundtrip_as_author( $raw, $expected ) { - wp_set_current_user( self::$author_id ); - $this->assertFalse( current_user_can( 'unfiltered_html' ) ); - $this->verify_attachment_roundtrip( $raw, $expected ); - } - /** * @requires function imagejpeg */ @@ -1383,8 +1834,8 @@ public function test_attachment_roundtrip_as_superadmin_unfiltered_html() { public function test_delete_item() { wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1399,8 +1850,8 @@ public function test_delete_item() { public function test_delete_item_no_trash() { wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1424,8 +1875,8 @@ public function test_delete_item_no_trash() { public function test_delete_item_invalid_delete_permissions() { wp_set_current_user( self::$author_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1439,8 +1890,8 @@ public function test_delete_item_invalid_delete_permissions() { } public function test_prepare_item() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1458,8 +1909,8 @@ public function test_prepare_item() { } public function test_prepare_item_limit_fields() { - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1488,9 +1939,12 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 27, $properties ); + $this->assertCount( 32, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'alt_text', $properties ); + $this->assertArrayHasKey( 'exif_orientation', $properties ); + $this->assertArrayHasKey( 'filename', $properties ); + $this->assertArrayHasKey( 'filesize', $properties ); $this->assertArrayHasKey( 'caption', $properties ); $this->assertArrayHasKey( 'raw', $properties['caption']['properties'] ); $this->assertArrayHasKey( 'rendered', $properties['caption']['properties'] ); @@ -1522,6 +1976,8 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'rendered', $properties['title']['properties'] ); $this->assertArrayHasKey( 'type', $properties ); $this->assertArrayHasKey( 'missing_image_sizes', $properties ); + $this->assertArrayHasKey( 'featured_media', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } public function test_get_additional_field_registration() { @@ -1549,8 +2005,8 @@ public function test_get_additional_field_registration() { $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] ); $this->assertSame( $schema, $data['schema']['properties']['my_custom_int'] ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1586,8 +2042,8 @@ public function test_additional_field_update_errors() { ); wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1611,23 +2067,33 @@ public function test_additional_field_update_errors() { $wp_rest_additional_fields = array(); } + public function additional_field_get_callback( $response_data, $field_name ) { + return 123; + } + + public function additional_field_update_callback( $value, $attachment ) { + if ( 'returnError' === $value ) { + return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); + } + } + public function test_search_item_by_filename() { - $id1 = $this->factory->attachment->create_object( - $this->test_file, + $id1 = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', ) ); - $id2 = $this->factory->attachment->create_object( - $this->test_file2, + $id2 = self::factory()->attachment->create_object( + self::$test_file2, 0, array( 'post_mime_type' => 'image/png', ) ); - $filename = wp_basename( $this->test_file2 ); + $filename = wp_basename( self::$test_file2 ); $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); $request->set_param( 'search', $filename ); @@ -1639,16 +2105,6 @@ public function test_search_item_by_filename() { $this->assertSame( 'image/png', $data[0]['mime_type'] ); } - public function additional_field_get_callback( $object, $request ) { - return 123; - } - - public function additional_field_update_callback( $value, $attachment ) { - if ( 'returnError' === $value ) { - return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); - } - } - public function test_links_exist() { wp_set_current_user( self::$editor_id ); @@ -1664,12 +2120,54 @@ public function test_links_exist() { $this->assertArrayHasKey( 'self', $links ); $this->assertArrayHasKey( 'author', $links ); + $this->assertArrayNotHasKey( 'post', $links ); $this->assertCount( 1, $links['author'] ); $this->assertArrayHasKey( 'embeddable', $links['author'][0]['attributes'] ); $this->assertTrue( $links['author'][0]['attributes']['embeddable'] ); } + /** + * @ticket 64034 + */ + public function test_links_contain_parent() { + wp_set_current_user( self::$editor_id ); + + $post = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + 'post_title' => 'Test Post', + ) + ); + $attachment = self::factory()->attachment->create_object( + array( + 'file' => self::$test_file, + 'post_author' => self::$editor_id, + 'post_parent' => $post, + 'post_mime_type' => 'image/jpeg', + ) + ); + + $this->assertGreaterThan( 0, $attachment ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/media/{$attachment}" ); + $request->set_query_params( array( 'context' => 'edit' ) ); + + $response = rest_get_server()->dispatch( $request ); + $links = $response->get_links(); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'author', $links ); + $this->assertArrayHasKey( 'https://api.w.org/attached-to', $links ); + + $this->assertCount( 1, $links['author'] ); + $this->assertSame( rest_url( '/wp/v2/posts/' . $post ), $links['https://api.w.org/attached-to'][0]['href'] ); + $this->assertSame( 'post', $links['https://api.w.org/attached-to'][0]['attributes']['post_type'] ); + $this->assertSame( $post, $links['https://api.w.org/attached-to'][0]['attributes']['id'] ); + $this->assertTrue( $links['https://api.w.org/attached-to'][0]['attributes']['embeddable'] ); + } + public function test_publish_action_ldo_not_registered() { $response = rest_get_server()->dispatch( new WP_REST_Request( 'OPTIONS', '/wp/v2/media' ) ); @@ -1721,7 +2219,6 @@ protected function check_post_data( $attachment, $data, $context = 'view', $link } $this->assertSame( wp_get_attachment_url( $attachment->ID ), $data['source_url'] ); - } /** @@ -1739,16 +2236,16 @@ public function test_create_item_with_file_exceeds_multisite_max_filesize() { array( 'file' => array( 'error' => '0', - 'file' => file_get_contents( $this->test_file ), + 'file' => file_get_contents( self::$test_file ), 'name' => 'canola.jpg', - 'size' => filesize( $this->test_file ), - 'tmp_name' => $this->test_file, + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, ), ) ); $request->set_param( 'title', 'My title is very cool' ); $request->set_param( 'caption', 'This is a better caption.' ); - $request->set_header( 'Content-MD5', md5_file( $this->test_file ) ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_upload_file_too_big', $response, 400 ); @@ -1767,7 +2264,7 @@ public function test_create_item_with_data_exceeds_multisite_max_filesize() { $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $request->set_param( 'title', 'My title is very cool' ); $request->set_param( 'caption', 'This is a better caption.' ); @@ -1790,16 +2287,16 @@ public function test_create_item_with_file_exceeds_multisite_site_upload_space() array( 'file' => array( 'error' => '0', - 'file' => file_get_contents( $this->test_file ), + 'file' => file_get_contents( self::$test_file ), 'name' => 'canola.jpg', - 'size' => filesize( $this->test_file ), - 'tmp_name' => $this->test_file, + 'size' => filesize( self::$test_file ), + 'tmp_name' => self::$test_file, ), ) ); $request->set_param( 'title', 'My title is very cool' ); $request->set_param( 'caption', 'This is a better caption.' ); - $request->set_header( 'Content-MD5', md5_file( $this->test_file ) ); + $request->set_header( 'Content-MD5', md5_file( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_upload_limited_space', $response, 400 ); @@ -1818,7 +2315,7 @@ public function test_create_item_with_data_exceeds_multisite_site_upload_space() $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); $request->set_header( 'Content-Type', 'image/jpeg' ); $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $request->set_param( 'title', 'My title is very cool' ); $request->set_param( 'caption', 'This is a better caption.' ); @@ -1848,7 +2345,7 @@ public function test_rest_insert_attachment_hooks_fire_once_on_create() { $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertSame( 201, $response->get_status() ); @@ -1857,6 +2354,60 @@ public function test_rest_insert_attachment_hooks_fire_once_on_create() { $this->assertSame( 1, self::$rest_after_insert_attachment_count ); } + /** + * Tests that the naming behavior of REST media uploads matches core media uploads. + * + * In particular, filenames with spaces should maintain the spaces rather than + * replacing them with hyphens. + * + * @ticket 57957 + * + * @covers WP_REST_Attachments_Controller::insert_attachment + * @dataProvider rest_upload_filename_spaces + */ + public function test_rest_upload_filename_spaces( $filename, $expected ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_body( file_get_contents( self::$test_file ) ); + $request->set_file_params( + array( + 'file' => array( + 'file' => file_get_contents( self::$test_file2 ), + 'name' => $filename, + 'size' => filesize( self::$test_file2 ), + 'tmp_name' => self::$test_file2, + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 201, $response->get_status(), 'The file was not uploaded.' ); + $this->assertSame( $expected, $data['title']['raw'], 'An incorrect filename was returned.' ); + } + + /** + * Data provider for text_rest_upload_filename_spaces. + * + * @return array + */ + public function rest_upload_filename_spaces() { + return array( + 'filename with spaces' => array( + 'Filename With Spaces.jpg', + 'Filename With Spaces', + ), + 'filename.with.periods' => array( + 'Filename.With.Periods.jpg', + 'Filename.With.Periods', + ), + 'filename-with-dashes' => array( + 'Filename-With-Dashes.jpg', + 'Filename-With-Dashes', + ), + ); + } + /** * Ensure the `rest_after_insert_attachment` and `rest_insert_attachment` hooks only fire * once when attachments are updated. @@ -1870,8 +2421,8 @@ public function test_rest_insert_attachment_hooks_fire_once_on_update() { add_action( 'rest_after_insert_attachment', array( $this, 'filter_rest_after_insert_attachment' ) ); wp_set_current_user( self::$editor_id ); - $attachment_id = $this->factory->attachment->create_object( - $this->test_file, + $attachment_id = self::factory()->attachment->create_object( + self::$test_file, 0, array( 'post_mime_type' => 'image/jpeg', @@ -1909,7 +2460,7 @@ public function test_create_item_with_meta_values() { $request->set_header( 'Content-Disposition', 'attachment; filename=cannoli.jpg' ); $request->set_param( 'meta', array( 'best_cannoli' => 'Chocolate-dipped, no filling' ) ); - $request->set_body( file_get_contents( $this->test_file ) ); + $request->set_body( file_get_contents( self::$test_file ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1917,12 +2468,104 @@ public function test_create_item_with_meta_values() { $this->assertSame( 'Chocolate-dipped, no filling', get_post_meta( $response->get_data()['id'], 'best_cannoli', true ) ); } + /** + * @ticket 61189 + * @requires function imagejpeg + */ + public function test_create_item_year_month_based_folders() { + update_option( 'uploads_use_yearmonth_folders', 1 ); + + wp_set_current_user( self::$editor_id ); + + $published_post = self::factory()->post->create( + array( + 'post_status' => 'publish', + 'post_date' => '2017-02-14 00:00:00', + 'post_date_gmt' => '2017-02-14 00:00:00', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_param( 'title', 'My title is very cool' ); + $request->set_param( 'caption', 'This is a better caption.' ); + $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); + $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); + $request->set_param( 'post', $published_post ); + + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + update_option( 'uploads_use_yearmonth_folders', 0 ); + + $this->assertSame( 201, $response->get_status() ); + + $attachment = get_post( $data['id'] ); + + $this->assertSame( $attachment->post_parent, $data['post'] ); + $this->assertSame( $attachment->post_parent, $published_post ); + $this->assertSame( wp_get_attachment_url( $attachment->ID ), $data['source_url'] ); + $this->assertStringContainsString( '2017/02', $data['source_url'] ); + } + + + /** + * @ticket 61189 + * @requires function imagejpeg + */ + public function test_create_item_year_month_based_folders_page_post_type() { + update_option( 'uploads_use_yearmonth_folders', 1 ); + + wp_set_current_user( self::$editor_id ); + + $published_post = self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + 'post_date' => '2017-02-14 00:00:00', + 'post_date_gmt' => '2017-02-14 00:00:00', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/jpeg' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=canola.jpg' ); + $request->set_param( 'title', 'My title is very cool' ); + $request->set_param( 'caption', 'This is a better caption.' ); + $request->set_param( 'description', 'Without a description, my attachment is descriptionless.' ); + $request->set_param( 'alt_text', 'Alt text is stored outside post schema.' ); + $request->set_param( 'post', $published_post ); + + $request->set_body( file_get_contents( self::$test_file ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + update_option( 'uploads_use_yearmonth_folders', 0 ); + + $time = current_time( 'mysql' ); + $y = substr( $time, 0, 4 ); + $m = substr( $time, 5, 2 ); + $subdir = "/$y/$m"; + + $this->assertSame( 201, $response->get_status() ); + + $attachment = get_post( $data['id'] ); + + $this->assertSame( $attachment->post_parent, $data['post'] ); + $this->assertSame( $attachment->post_parent, $published_post ); + $this->assertSame( wp_get_attachment_url( $attachment->ID ), $data['source_url'] ); + $this->assertStringNotContainsString( '2017/02', $data['source_url'] ); + $this->assertStringContainsString( $subdir, $data['source_url'] ); + } + public function filter_rest_insert_attachment( $attachment ) { - self::$rest_insert_attachment_count++; + ++self::$rest_insert_attachment_count; } public function filter_rest_after_insert_attachment( $attachment ) { - self::$rest_after_insert_attachment_count++; + ++self::$rest_after_insert_attachment_count; } /** @@ -1930,7 +2573,7 @@ public function filter_rest_after_insert_attachment( $attachment ) { * @requires function imagejpeg */ public function test_edit_image_returns_error_if_logged_out() { - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); $request->set_body_params( array( 'src' => wp_get_attachment_image_url( $attachment, 'full' ) ) ); @@ -1947,7 +2590,7 @@ public function test_edit_image_returns_error_if_cannot_upload() { $user->add_cap( 'upload_files', false ); wp_set_current_user( $user->ID ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); $request->set_body_params( array( 'src' => wp_get_attachment_image_url( $attachment, 'full' ) ) ); @@ -1961,7 +2604,7 @@ public function test_edit_image_returns_error_if_cannot_upload() { */ public function test_edit_image_returns_error_if_cannot_edit() { wp_set_current_user( self::$uploader_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); $request->set_body_params( array( 'src' => wp_get_attachment_image_url( $attachment, 'full' ) ) ); @@ -1988,7 +2631,7 @@ public function test_edit_image_returns_error_if_no_attachment() { */ public function test_edit_image_returns_error_if_unsupported_mime_type() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); wp_update_post( array( 'ID' => $attachment, @@ -2008,7 +2651,7 @@ public function test_edit_image_returns_error_if_unsupported_mime_type() { */ public function test_edit_image_returns_error_if_no_edits() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); $request->set_body_params( array( 'src' => wp_get_attachment_image_url( $attachment, 'full' ) ) ); @@ -2022,7 +2665,7 @@ public function test_edit_image_returns_error_if_no_edits() { */ public function test_edit_image_rotate() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $this->setup_mock_editor(); WP_Image_Editor_Mock::$edit_return['rotate'] = new WP_Error(); @@ -2047,7 +2690,7 @@ public function test_edit_image_rotate() { */ public function test_edit_image_crop() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $this->setup_mock_editor(); WP_Image_Editor_Mock::$size_return = array( @@ -2073,7 +2716,44 @@ public function test_edit_image_crop() { $this->assertCount( 1, WP_Image_Editor_Mock::$spy['crop'] ); $this->assertSame( - array( 320.0, 48.0, 64.0, 24.0 ), + array( 320, 48, 64, 24 ), + WP_Image_Editor_Mock::$spy['crop'][0] + ); + } + + /** + * @ticket 61514 + * @requires function imagejpeg + */ + public function test_edit_image_crop_one_axis() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + $this->setup_mock_editor(); + WP_Image_Editor_Mock::$size_return = array( + 'width' => 640, + 'height' => 480, + ); + + WP_Image_Editor_Mock::$edit_return['crop'] = new WP_Error(); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( + array( + 'x' => 50, + 'y' => 0, + 'width' => 10, + 'height' => 100, + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + + ) + ); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'rest_image_crop_failed', $response, 500 ); + + $this->assertCount( 1, WP_Image_Editor_Mock::$spy['crop'] ); + $this->assertSame( + array( 320, 0, 64, 480 ), WP_Image_Editor_Mock::$spy['crop'][0] ); } @@ -2084,7 +2764,7 @@ public function test_edit_image_crop() { */ public function test_edit_image() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $params = array( 'rotation' => 60, @@ -2101,7 +2781,7 @@ public function test_edit_image() { $this->assertStringEndsWith( '-edited.jpg', $item['media_details']['file'] ); $this->assertArrayHasKey( 'parent_image', $item['media_details'] ); - $this->assertEquals( $attachment, $item['media_details']['parent_image']['attachment_id'] ); + $this->assertSame( (string) $attachment, $item['media_details']['parent_image']['attachment_id'] ); $this->assertStringContainsString( 'canola', $item['media_details']['parent_image']['file'] ); } @@ -2111,7 +2791,7 @@ public function test_edit_image() { */ public function test_batch_edit_image() { wp_set_current_user( self::$superadmin_id ); - $attachment = self::factory()->attachment->create_upload_object( $this->test_file ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); $params = array( 'modifiers' => array( @@ -2144,7 +2824,7 @@ public function test_batch_edit_image() { $this->assertStringEndsWith( '-edited.jpg', $item['media_details']['file'] ); $this->assertArrayHasKey( 'parent_image', $item['media_details'] ); - $this->assertEquals( $attachment, $item['media_details']['parent_image']['attachment_id'] ); + $this->assertSame( (string) $attachment, $item['media_details']['parent_image']['attachment_id'] ); $this->assertStringContainsString( 'canola', $item['media_details']['parent_image']['file'] ); } @@ -2154,8 +2834,8 @@ public function test_batch_edit_image() { */ public function test_edit_image_returns_error_if_mismatched_src() { wp_set_current_user( self::$superadmin_id ); - $attachment_id_image1 = self::factory()->attachment->create_upload_object( $this->test_file ); - $attachment_id_image2 = self::factory()->attachment->create_upload_object( $this->test_file2 ); + $attachment_id_image1 = self::factory()->attachment->create_upload_object( self::$test_file ); + $attachment_id_image2 = self::factory()->attachment->create_upload_object( self::$test_file2 ); $attachment_id_file = self::factory()->attachment->create(); // URL to the first uploaded image. @@ -2200,4 +2880,315 @@ static function () { } ); } + + /** + * Test that uploading unsupported image types throws a `rest_upload_image_type_not_supported` error. + * + * @ticket 61167 + */ + public function test_upload_unsupported_image_type() { + + // Only run this test when the editor doesn't support AVIF. + if ( wp_image_editor_supports( array( 'AVIF' ) ) ) { + $this->markTestSkipped( 'The image editor suppports AVIF.' ); + } + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + + wp_set_current_user( self::$author_id ); + $request->set_header( 'Content-Type', 'image/avif' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=avif-lossy.avif' ); + $request->set_body( file_get_contents( self::$test_avif_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_upload_image_type_not_supported', $response, 400 ); + } + + /** + * Test that the `wp_prevent_unsupported_image_uploads` filter enables uploading of unsupported image types. + * + * @ticket 61167 + */ + public function test_upload_unsupported_image_type_with_filter() { + + // Only run this test when the editor doesn't support AVIF. + if ( wp_image_editor_supports( array( 'AVIF' ) ) ) { + $this->markTestSkipped( 'The image editor suppports AVIF.' ); + } + + add_filter( 'wp_prevent_unsupported_image_uploads', '__return_false' ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + + wp_set_current_user( self::$author_id ); + $request->set_header( 'Content-Type', 'image/avif' ); + $request->set_header( 'Content-Disposition', 'attachment; filename=avif-lossy.avif' ); + $request->set_body( file_get_contents( self::$test_avif_file ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 201, $response->get_status() ); + } + + /** + * Test that unsupported image type check is enforced when generating sub-sizes. + * + * When the server handles image processing (generate_sub_sizes is true), + * the server should still check image editor support. + * + * Tests the permissions check directly with file params set, since the core + * check uses get_file_params() which is only populated for multipart uploads. + * + * @ticket 64836 + */ + public function test_upload_unsupported_image_type_enforced_when_generating_sub_sizes() { + wp_set_current_user( self::$author_id ); + + add_filter( 'wp_image_editors', '__return_empty_array' ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_file_params( + array( + 'file' => array( + 'name' => 'avif-lossy.avif', + 'type' => 'image/avif', + 'tmp_name' => self::$test_avif_file, + 'error' => 0, + 'size' => filesize( self::$test_avif_file ), + ), + ) + ); + + $controller = new WP_REST_Attachments_Controller( 'attachment' ); + $result = $controller->create_item_permissions_check( $request ); + + // Should fail because the server needs to generate sub-sizes but can't. + $this->assertWPError( $result ); + $this->assertSame( 'rest_upload_image_type_not_supported', $result->get_error_code() ); + } + + /** + * Test that uploading an SVG image doesn't throw a `rest_upload_image_type_not_supported` error. + * + * @ticket 63302 + */ + public function test_upload_svg_image() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/media' ); + $request->set_header( 'Content-Type', 'image/svg+xml' ); + $request->set_file_params( + array( + 'file' => array( + 'file' => file_get_contents( self::$test_svg_file ), + 'name' => 'video-play.svg', + 'size' => filesize( self::$test_svg_file ), + 'tmp_name' => self::$test_svg_file, + 'type' => 'image/svg+xml', + ), + ) + ); + $rest_controller = new WP_REST_Attachments_Controller( 'attachment' ); + $result = $rest_controller->create_item_permissions_check( $request ); + + $this->assertTrue( $result ); + } + + /** + * Tests that the attachment fields caption, description, and title, post and alt_text are updated correctly. + * + * @ticket 64035 + * @requires function imagejpeg + */ + public function test_edit_image_updates_attachment_fields() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + // In order to test the edit endpoint editable fields, we need to create a new attachment. + $params = array( + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + 'modifiers' => array( + array( + 'type' => 'crop', + 'args' => array( + 'left' => 10, + 'top' => 10, + 'width' => 80, + 'height' => 80, + ), + ), + ), + 'caption' => 'Test Caption', + 'description' => 'Test Description', + 'title' => 'Test Title', + 'post' => 1, + 'alt_text' => 'Test Alt Text', + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( $params ); + $response = rest_do_request( $request ); + + // The edit endpoint creates a new attachment, so we expect a 201 status. + $this->assertEquals( 201, $response->get_status() ); + + $data = $response->get_data(); + $new_attachment_id = $data['id']; + + $updated_attachment = get_post( $new_attachment_id ); + + $this->assertSame( 'Test Title', $updated_attachment->post_title, 'Title of the updated attachment is not identical.' ); + + $this->assertSame( 'Test Caption', $updated_attachment->post_excerpt, 'Caption of the updated attachment is not identical.' ); + + $this->assertSame( 'Test Description', $updated_attachment->post_content, 'Description of the updated attachment is not identical.' ); + + $this->assertSame( 1, $updated_attachment->post_parent, 'Post parent of the updated attachment is not identical.' ); + + $this->assertSame( 'Test Alt Text', get_post_meta( $new_attachment_id, '_wp_attachment_image_alt', true ), 'Alt text of the updated attachment is not identical.' ); + } + + /** + * Tests that the image is flipped correctly vertically and horizontally. + * + * @ticket 64035 + * @requires function imagejpeg + */ + public function test_edit_image_vertical_and_horizontal_flip() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + $this->setup_mock_editor(); + WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error(); + + $params = array( + 'flip' => array( + 'vertical' => true, + 'horizontal' => true, + ), + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( $params ); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 ); + + $this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] ); + // The controller converts the integer values to booleans: 0 !== (int) 1 = true. + $this->assertSame( array( true, true ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical and horizontal flip of the image is not identical.' ); + } + + /** + * Tests that the image is flipped correctly vertically only. + * + * @ticket 64035 + * @requires function imagejpeg + */ + public function test_edit_image_vertical_flip_with_horizontal_false() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + $this->setup_mock_editor(); + WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error(); + + $params = array( + 'flip' => array( + 'vertical' => true, + 'horizontal' => false, + ), + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( $params ); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 ); + + $this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] ); + // The controller converts the integer values to booleans: 0 !== (int) 1 = true. + $this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' ); + } + + /** + * Tests that the image is flipped correctly with only vertical flip in arguments. + * + * @ticket 64035 + * @requires function imagejpeg + */ + public function test_edit_image_vertical_flip_only() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + $this->setup_mock_editor(); + WP_Image_Editor_Mock::$edit_return['flip'] = new WP_Error(); + + $params = array( + 'flip' => array( + 'vertical' => true, + ), + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( $params ); + $response = rest_do_request( $request ); + $this->assertErrorResponse( 'rest_image_flip_failed', $response, 500 ); + + $this->assertCount( 1, WP_Image_Editor_Mock::$spy['flip'] ); + // The controller converts the integer values to booleans: 0 !== (int) 1 = true. + $this->assertSame( array( true, false ), WP_Image_Editor_Mock::$spy['flip'][0], 'Vertical flip of the image is not identical.' ); + } + + /** + * Test that wp_slash() is properly applied when creating edited images. + * + * This test verifies that the object returned by prepare_item_for_database() + * is properly cast to an array before being passed to wp_slash(), ensuring + * that string values are properly escaped for database insertion. + * + * @ticket 64149 + * @requires function imagejpeg + */ + public function test_edit_image_wp_slash_with_object_cast() { + wp_set_current_user( self::$superadmin_id ); + $attachment = self::factory()->attachment->create_upload_object( self::$test_file ); + + // Create a mock to capture the data passed to wp_insert_attachment. + $captured_data = null; + + // Mock wp_insert_attachment to capture the data being passed. + add_filter( + 'wp_insert_attachment_data', + static function ( $data ) use ( &$captured_data ) { + $captured_data = $data; + return $data; + }, + 10, + 1 + ); + + $params = array( + 'rotation' => 60, + 'src' => wp_get_attachment_image_url( $attachment, 'full' ), + 'title' => 'Test Title with "quotes" and \'apostrophes\'', + 'caption' => 'Test Caption with "quotes" and \'apostrophes\'', + 'description' => 'Test Description with "quotes" and \'apostrophes\'', + ); + + $request = new WP_REST_Request( 'POST', "/wp/v2/media/{$attachment}/edit" ); + $request->set_body_params( $params ); + $response = rest_do_request( $request ); + + $this->assertSame( 201, $response->get_status() ); + + // Verify that the data was properly slashed (escaped) + $this->assertNotNull( $captured_data, 'wp_insert_attachment was not called with data' ); + + // Check that quotes are properly escaped in the captured data. + $this->assertStringContainsString( 'Test Title with \"quotes\"', $captured_data['post_title'] ?? '', 'Title quotes not properly escaped' ); + $this->assertStringContainsString( 'Test Caption with \"quotes\"', $captured_data['post_excerpt'] ?? '', 'Caption quotes not properly escaped' ); + $this->assertStringContainsString( 'Test Description with \"quotes\"', $captured_data['post_content'] ?? '', 'Description quotes not properly escaped' ); + + // Verify that the data is an array (not an object). + $this->assertIsArray( $captured_data, 'Data passed to wp_insert_attachment should be an array' ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php index cf85a864be591..7815f8ced23c9 100644 --- a/tests/phpunit/tests/rest-api/rest-autosaves-controller.php +++ b/tests/phpunit/tests/rest-api/rest-autosaves-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi-autosave * @group restapi */ @@ -25,6 +23,8 @@ class WP_Test_REST_Autosaves_Controller extends WP_Test_REST_Post_Type_Controlle protected static $child_page_id; protected static $child_draft_page_id; + private $post_autosave; + protected function set_post_data( $args = array() ) { $defaults = array( 'title' => 'Post Title', @@ -179,9 +179,35 @@ public function test_get_items() { $this->check_get_autosave_response( $data[0], $this->post_autosave ); } - public function test_get_items_no_permission() { + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_autosaves_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + + $hook_name = 'rest_prepare_autosave'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); wp_set_current_user( self::$contributor_id ); @@ -189,16 +215,40 @@ public function test_get_items_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_items_missing_parent() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_items_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } @@ -215,12 +265,13 @@ public function test_get_item() { 'author', 'date', 'date_gmt', + 'id', + 'meta', 'modified', 'modified_gmt', - 'guid', - 'id', 'parent', 'slug', + 'guid', 'title', 'excerpt', 'content', @@ -229,6 +280,43 @@ public function test_get_item() { $this->assertSame( self::$editor_id, $data['author'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + + $hook_name = 'rest_prepare_autosave'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_item_embed_context() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); @@ -247,30 +335,50 @@ public function test_get_item_embed_context() { $this->assertSameSets( $fields, array_keys( $data ) ); } - public function test_get_item_no_permission() { - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_no_permission( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/autosaves/' . self::$autosave_post_id ); wp_set_current_user( self::$contributor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_item_missing_parent() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves/' . self::$autosave_post_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/autosaves/' . self::$autosave_post_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); - } - public function test_get_item_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/autosaves' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/autosaves' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } + /** + * @doesNotPerformAssertions + */ public function test_delete_item() { - // Doesn't exist. + // Controller does not implement delete_item(). } public function test_prepare_item() { @@ -286,7 +394,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 13, $properties ); + $this->assertCount( 14, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'content', $properties ); $this->assertArrayHasKey( 'date', $properties ); @@ -300,13 +408,14 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'slug', $properties ); $this->assertArrayHasKey( 'title', $properties ); $this->assertArrayHasKey( 'preview_link', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); } public function test_create_item() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $params = $this->set_post_data( array( @@ -322,12 +431,76 @@ public function test_create_item() { public function test_update_item() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + + $params = $this->set_post_data( + array( + 'id' => self::$post_id, + 'author' => self::$contributor_id, + ) + ); + + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_autosave_response( $response ); + } + + public function test_update_item_with_meta() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + register_post_meta( + 'post', + 'foo', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => true, + ) + ); + $params = $this->set_post_data( + array( + 'id' => self::$post_id, + 'author' => self::$contributor_id, + 'meta' => array( + 'foo' => 'bar', + ), + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_autosave_response( $response ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertArrayHasKey( 'foo', $data['meta'] ); + $this->assertSame( 'bar', $data['meta']['foo'] ); + } + + public function test_update_item_with_json_meta() { + $meta = '[{\"content\":\"foot 1\",\"id\":\"fa97a10d-7401-42b9-ac54-df8f4510749a\"},{\"content\":\"fdddddoot 2\\\"\",\"id\":\"2216d0aa-34b8-42b4-b441-84dedc0406e0\"}]'; + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + register_post_meta( + 'post', + 'foo', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => true, + ) + ); $params = $this->set_post_data( array( 'id' => self::$post_id, 'author' => self::$contributor_id, + 'meta' => array( + 'foo' => $meta, + ), ) ); @@ -335,13 +508,19 @@ public function test_update_item() { $response = rest_get_server()->dispatch( $request ); $this->check_create_autosave_response( $response ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertArrayHasKey( 'foo', $data['meta'] ); + $values = json_decode( wp_unslash( $data['meta']['foo'] ), true ); + $this->assertNotNull( $values ); } public function test_update_item_nopriv() { wp_set_current_user( self::$contributor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $params = $this->set_post_data( array( @@ -360,7 +539,7 @@ public function test_rest_autosave_published_post() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $current_post = get_post( self::$post_id ); @@ -407,7 +586,7 @@ public function test_rest_autosave_draft_post_same_author() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $autosave_data ) ); $response = rest_get_server()->dispatch( $request ); @@ -446,7 +625,7 @@ public function test_rest_autosave_draft_post_different_author() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $autosave_data ) ); $response = rest_get_server()->dispatch( $request ); @@ -509,12 +688,12 @@ public function test_get_additional_field_registration() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { - return get_post_meta( $object['id'], 'my_custom_int', true ); + public function additional_field_get_callback( $response_data, $field_name ) { + return get_post_meta( $response_data['id'], $field_name, true ); } - public function additional_field_update_callback( $value, $post ) { - update_post_meta( $post->ID, 'my_custom_int', $value ); + public function additional_field_update_callback( $value, $post, $field_name ) { + update_post_meta( $post->ID, $field_name, $value ); } protected function check_get_autosave_response( $response, $autosave ) { @@ -567,7 +746,7 @@ public function test_get_item_sets_up_postdata() { public function test_update_item_draft_page_with_parent() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/pages/' . self::$child_draft_page_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $params = $this->set_post_data( array( @@ -588,7 +767,7 @@ public function test_schema_validation_is_applied() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'POST', '/wp/v2/pages/' . self::$draft_page_id . '/autosaves' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $params = $this->set_post_data( array( @@ -602,4 +781,143 @@ public function test_schema_validation_is_applied() { $response = rest_get_server()->dispatch( $request ); $this->assertNotEquals( 'garbage', get_post( self::$draft_page_id )->comment_status ); } + + /** + * Test ensuring that autosave from the original author doesn't overwrite changes after it has been taken over by a 2nd author. + * + * @ticket 55659 + */ + public function test_rest_autosave_draft_post_locked_to_different_author() { + + // Create a post by the editor. + $post_data = array( + 'post_content' => 'Test post content', + 'post_title' => 'Test post title', + 'post_excerpt' => 'Test post excerpt', + 'post_author' => self::$editor_id, + 'post_status' => 'draft', + ); + $post_id = wp_insert_post( $post_data ); + + // Set the post lock to the contributor, simulating a takeover of the post. + wp_set_current_user( self::$contributor_id ); + wp_set_post_lock( $post_id ); + + // Update the post with new data from the contributor. + $updated_post_data = array( + 'ID' => $post_id, + 'post_content' => 'New post content from the contributor', + 'post_title' => 'New post title', + ); + wp_update_post( $updated_post_data ); + + // Set the current user to the editor and initiate an autosave with some new data. + wp_set_current_user( self::$editor_id ); + $autosave_data = array( + 'id' => $post_id, + 'content' => 'Updated post content', + 'excerpt' => 'A new excerpt to test', + 'title' => $post_data['post_title'], + ); + + // Initiate an autosave via the REST API as Gutenberg does. + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . self::$post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $autosave_data ) ); + + $response = rest_get_server()->dispatch( $request ); + $new_data = $response->get_data(); + + // The current version of our test post. + $current_post = get_post( $post_id ); + + // The new data from the autosave should have its parent ID set to the original post ID. + $this->assertSame( $post_id, $new_data['parent'] ); + + // The post title and content should still be the updated versions from the contributor. + $this->assertSame( $current_post->post_title, $updated_post_data['post_title'] ); + $this->assertSame( $current_post->post_content, $updated_post_data['post_content'] ); + + // The excerpt should have stayed the same. + $this->assertSame( $current_post->post_excerpt, $post_data['post_excerpt'] ); + + $autosave_post = wp_get_post_autosave( $post_id ); + + // Has changes. + $this->assertSame( $autosave_data['content'], $autosave_post->post_content ); + + wp_delete_post( $post_id ); + } + + /** + * @ticket 49532 + * + * @covers WP_REST_Autosaves_Controller::create_post_autosave + */ + public function test_rest_autosave_do_not_create_autosave_when_post_is_unchanged() { + // Create a post by the editor. + $post_data = array( + 'post_content' => 'Test post content', + 'post_title' => 'Test post title', + 'post_excerpt' => 'Test post excerpt', + 'post_author' => self::$editor_id, + 'post_status' => 'publish', + ); + $post_id = wp_insert_post( $post_data ); + wp_set_current_user( self::$editor_id ); + + // Make a small change create the initial autosave. + $autosave_data = array( + 'post_content' => 'Test post content changed', + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $post_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $autosave_data ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + // Store the first autosave ID. + $autosave = $response->get_data(); + + // Try creating an autosave using the REST endpoint with unchanged content. + $request->set_body( wp_json_encode( $autosave_data ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( $autosave['id'], $data['id'], 'Original autosave was not returned' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'HEAD', sprintf( $path, self::$post_id, self::$autosave_post_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/posts/%d/autosaves/%d' ), + 'get_items request' => array( '/wp/v2/posts/%d' ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-block-directory-controller.php b/tests/phpunit/tests/rest-api/rest-block-directory-controller.php index ef70ecb319d08..f61b317240532 100644 --- a/tests/phpunit/tests/rest-api/rest-block-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-directory-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_REST_Block_Directory_Controller_Test extends WP_Test_REST_Controller_Testcase { @@ -69,6 +67,11 @@ public function test_context_param() { */ public function test_get_items() { wp_set_current_user( self::$admin_id ); + $this->mock_remote_request( + array( + 'body' => '{"info":{"page":1,"pages":0,"results":0},"plugins":[]}', + ) + ); $request = new WP_REST_Request( 'GET', '/wp/v2/block-directory/search' ); $request->set_query_params( array( 'term' => 'foo' ) ); @@ -109,6 +112,11 @@ public function test_get_items_logged_out() { */ public function test_get_items_no_results() { wp_set_current_user( self::$admin_id ); + $this->mock_remote_request( + array( + 'body' => '{"info":{"page":1,"pages":0,"results":0},"plugins":[]}', + ) + ); $request = new WP_REST_Request( 'GET', '/wp/v2/block-directory/search' ); $request->set_query_params( array( 'term' => '0c4549ee68f24eaaed46a49dc983ecde' ) ); @@ -120,20 +128,32 @@ public function test_get_items_no_results() { $this->assertSame( array(), $data ); } + /** + * @doesNotPerformAssertions + */ public function test_get_item() { - $this->markTestSkipped( 'Controller does not have get_item route.' ); + // Controller does not implement get_item(). } + /** + * @doesNotPerformAssertions + */ public function test_create_item() { - $this->markTestSkipped( 'Controller does not have create_item route.' ); + // Controller does not implement create_item(). } + /** + * @doesNotPerformAssertions + */ public function test_update_item() { - $this->markTestSkipped( 'Controller does not have update_item route.' ); + // Controller does not implement update_item(). } + /** + * @doesNotPerformAssertions + */ public function test_delete_item() { - $this->markTestSkipped( 'Controller does not have delete_item route.' ); + // Controller does not implement delete_item(). } /** @@ -202,6 +222,46 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'humanized_updated', $properties ); } + /** + * @ticket 53621 + */ + public function test_get_items_response_conforms_to_schema() { + wp_set_current_user( self::$admin_id ); + $plugin = $this->get_mock_plugin(); + + // Fetch the block directory schema. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/block-directory/search' ); + $schema = rest_get_server()->dispatch( $request )->get_data()['schema']; + + add_filter( + 'plugins_api', + static function () use ( $plugin ) { + return (object) array( + 'info' => + array( + 'page' => 1, + 'pages' => 1, + 'results' => 1, + ), + 'plugins' => array( + $plugin, + ), + ); + } + ); + + // Fetch a block plugin. + $request = new WP_REST_Request( 'GET', '/wp/v2/block-directory/search' ); + $request->set_query_params( array( 'term' => 'cache' ) ); + + $result = rest_get_server()->dispatch( $request ); + $data = $result->get_data(); + + $valid = rest_validate_value_from_schema( $data[0], $schema ); + + $this->assertNotWPError( $valid ); + } + /** * Simulate a network failure on outbound http requests to a given hostname. * @@ -212,13 +272,13 @@ public function test_get_item_schema() { private function prevent_requests_to_host( $blocked_host = 'api.wordpress.org' ) { add_filter( 'pre_http_request', - static function ( $return, $args, $url ) use ( $blocked_host ) { + static function ( $response, $parsed_args, $url ) use ( $blocked_host ) { if ( @parse_url( $url, PHP_URL_HOST ) === $blocked_host ) { return new WP_Error( 'plugins_api_failed', "An expected error occurred connecting to $blocked_host because of a unit test", "cURL error 7: Failed to connect to $blocked_host port 80: Connection refused" ); } - return $return; + return $response; }, 10, 3 @@ -287,4 +347,31 @@ private function get_mock_plugin() { 'author_block_rating' => 0, ); } + + /** + * Mocks the remote request via `'pre_http_request'` filter by + * returning the expected response. + * + * @since 5.9.0 + * + * @param array $expected Expected response, which is merged with the default response. + */ + private function mock_remote_request( array $expected ) { + add_filter( + 'pre_http_request', + static function () use ( $expected ) { + $default = array( + 'headers' => array(), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => '', + 'cookies' => array(), + 'filename' => null, + ); + return array_merge( $default, $expected ); + } + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php index 454bc6da35e13..3573ecb6e0bea 100644 --- a/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-renderer-controller.php @@ -1,16 +1,10 @@ get_data(); $this->assertSame( $expected_attributes, json_decode( $data['rendered'], true ) ); - $this->assertEquals( + $this->assertEqualSetsWithIndex( json_decode( $block_type->render( $attributes ), true ), json_decode( $data['rendered'], true ) ); @@ -435,7 +429,7 @@ public function test_get_item() { public function test_get_item_with_pre_render_block_filter() { wp_set_current_user( self::$user_id ); - $pre_render_filter = static function( $output, $block ) { + $pre_render_filter = static function ( $output, $block ) { if ( $block['blockName'] === self::$block_name ) { return '

      Alternate content.

      '; } @@ -501,7 +495,7 @@ public function test_get_item_post_request() { $attributes = array( 'some_string' => $string_attribute ); $request = new WP_REST_Request( 'POST', self::$rest_api_route . self::$block_name ); $request->set_param( 'context', 'edit' ); - $request->set_header( 'content-type', 'application/json' ); + $request->set_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( compact( 'attributes' ) ) ); $response = rest_get_server()->dispatch( $request ); @@ -614,43 +608,55 @@ public function test_get_item_schema() { /** * The update_item() method does not exist for block rendering. + * + * @doesNotPerformAssertions */ public function test_update_item() { - $this->markTestSkipped( 'Controller does not implement update_item().' ); + // Controller does not implement update_item(). } /** * The create_item() method does not exist for block rendering. + * + * @doesNotPerformAssertions */ public function test_create_item() { - $this->markTestSkipped( 'Controller does not implement create_item().' ); + // Controller does not implement create_item(). } /** * The delete_item() method does not exist for block rendering. + * + * @doesNotPerformAssertions */ public function test_delete_item() { - $this->markTestSkipped( 'Controller does not implement delete_item().' ); + // Controller does not implement delete_item(). } /** * The get_items() method does not exist for block rendering. + * + * @doesNotPerformAssertions */ public function test_get_items() { - $this->markTestSkipped( 'Controller does not implement get_items().' ); + // Controller does not implement get_items(). } /** - * The context_param() method does not exist for block rendering. + * The get_context_param() method is not used for block rendering. + * + * @doesNotPerformAssertions */ public function test_context_param() { - $this->markTestSkipped( 'Controller does not implement context_param().' ); + // Controller does not use get_context_param(). } /** * The prepare_item() method does not exist for block rendering. + * + * @doesNotPerformAssertions */ public function test_prepare_item() { - $this->markTestSkipped( 'Controller does not implement prepare_item().' ); + // Controller does not implement prepare_item(). } } diff --git a/tests/phpunit/tests/rest-api/rest-block-type-controller.php b/tests/phpunit/tests/rest-api/rest-block-type-controller.php index a774b9be1fc71..7ba693286c993 100644 --- a/tests/phpunit/tests/rest-api/rest-block-type-controller.php +++ b/tests/phpunit/tests/rest-api/rest-block-type-controller.php @@ -1,16 +1,10 @@ dispatch( $request ); $data = $response->get_data(); $this->assertSameSets( array( $block_styles ), $data['styles'] ); - } /** @@ -185,7 +180,6 @@ public function test_get_item_with_styles_merge() { ), ); $this->assertSameSets( $expected, $data['styles'] ); - } /** @@ -202,30 +196,37 @@ public function test_get_block_invalid_name() { /** * @ticket 47620 + * @ticket 57585 + * @ticket 59346 + * @ticket 59797 */ public function test_get_item_invalid() { $block_type = 'fake/invalid'; $settings = array( 'title' => true, - 'description' => true, + 'category' => true, + 'parent' => 'invalid_parent', + 'ancestor' => 'invalid_ancestor', + 'allowed_blocks' => 'invalid_allowed_blocks', 'icon' => true, + 'description' => true, + 'keywords' => 'invalid_keywords', + 'textdomain' => true, 'attributes' => 'invalid_attributes', 'provides_context' => 'invalid_provides_context', 'uses_context' => 'invalid_uses_context', - 'category' => true, + 'selectors' => 'invalid_selectors', + 'supports' => 'invalid_supports', + 'styles' => array(), + 'example' => 'invalid_example', + 'variations' => 'invalid_variations', + 'block_hooks' => 'invalid_block_hooks', + 'render_callback' => 'invalid_callback', 'editor_script' => true, 'script' => true, 'view_script' => true, 'editor_style' => true, 'style' => true, - 'keywords' => 'invalid_keywords', - 'example' => 'invalid_example', - 'parent' => 'invalid_parent', - 'supports' => 'invalid_supports', - 'styles' => 'invalid_styles', - 'render_callback' => 'invalid_callback', - 'textdomain' => true, - 'variations' => 'invalid_variations', ); register_block_type( $block_type, $settings ); wp_set_current_user( self::$admin_id ); @@ -234,53 +235,77 @@ public function test_get_item_invalid() { $data = $response->get_data(); $this->assertSame( $block_type, $data['name'] ); $this->assertSame( '1', $data['title'] ); - $this->assertSame( '1', $data['description'] ); + $this->assertNull( $data['category'] ); + $this->assertSameSets( array( 'invalid_parent' ), $data['parent'] ); + $this->assertSameSets( array( 'invalid_ancestor' ), $data['ancestor'] ); + $this->assertSameSets( array( 'invalid_allowed_blocks' ), $data['allowed_blocks'] ); $this->assertNull( $data['icon'] ); - $this->assertNull( $data['editor_script'] ); - $this->assertNull( $data['script'] ); - $this->assertNull( $data['view_script'] ); - $this->assertNull( $data['editor_style'] ); - $this->assertNull( $data['style'] ); - $this->assertSameSets( array(), $data['provides_context'] ); - $this->assertSameSets( array(), $data['attributes'] ); - $this->assertSameSets( array( 'invalid_uses_context' ), $data['uses_context'] ); + $this->assertSame( '1', $data['description'] ); $this->assertSameSets( array( 'invalid_keywords' ), $data['keywords'] ); - $this->assertSameSets( array( 'invalid_parent' ), $data['parent'] ); + $this->assertNull( $data['textdomain'] ); + $this->assertSameSetsWithIndex( + array( + 'lock' => array( 'type' => 'object' ), + 'metadata' => array( 'type' => 'object' ), + ), + $data['attributes'] + ); + $this->assertSameSets( array( 'invalid_uses_context' ), $data['uses_context'] ); + $this->assertSameSets( array(), $data['provides_context'] ); + $this->assertSameSets( array(), $data['selectors'], 'invalid selectors defaults to empty array' ); $this->assertSameSets( array(), $data['supports'] ); $this->assertSameSets( array(), $data['styles'] ); $this->assertNull( $data['example'] ); - $this->assertNull( $data['category'] ); - $this->assertNull( $data['textdomain'] ); - $this->assertFalse( $data['is_dynamic'] ); $this->assertSameSets( array( array() ), $data['variations'] ); + $this->assertSameSets( array(), $data['block_hooks'], 'invalid block_hooks defaults to empty array' ); + $this->assertSameSets( array(), $data['editor_script_handles'] ); + $this->assertSameSets( array(), $data['script_handles'] ); + $this->assertSameSets( array(), $data['view_script_handles'] ); + $this->assertSameSets( array(), $data['view_script_module_ids'] ); + $this->assertSameSets( array(), $data['editor_style_handles'] ); + $this->assertSameSets( array(), $data['style_handles'] ); + $this->assertFalse( $data['is_dynamic'] ); + // Deprecated properties. + $this->assertNull( $data['editor_script'] ); + $this->assertNull( $data['script'] ); + $this->assertNull( $data['view_script'] ); + $this->assertNull( $data['editor_style'] ); + $this->assertNull( $data['style'] ); } /** * @ticket 47620 + * @ticket 57585 + * @ticket 59346 + * @ticket 59797 */ public function test_get_item_defaults() { $block_type = 'fake/false'; $settings = array( 'title' => false, - 'description' => false, + 'category' => false, + 'parent' => false, + 'ancestor' => false, + 'allowed_blocks' => false, 'icon' => false, + 'description' => false, + 'keywords' => false, + 'textdomain' => false, 'attributes' => false, 'provides_context' => false, 'uses_context' => false, - 'category' => false, + 'selectors' => false, + 'supports' => false, + 'styles' => false, + 'example' => false, + 'variations' => false, + 'block_hooks' => false, 'editor_script' => false, 'script' => false, 'view_script' => false, 'editor_style' => false, 'style' => false, - 'keywords' => false, - 'parent' => false, - 'supports' => false, - 'styles' => false, 'render_callback' => false, - 'textdomain' => false, - 'example' => false, - 'variations' => false, ); register_block_type( $block_type, $settings ); wp_set_current_user( self::$admin_id ); @@ -289,26 +314,184 @@ public function test_get_item_defaults() { $data = $response->get_data(); $this->assertSame( $block_type, $data['name'] ); $this->assertSame( '', $data['title'] ); - $this->assertSame( '', $data['description'] ); + $this->assertNull( $data['category'] ); + $this->assertSameSets( array(), $data['parent'] ); + $this->assertSameSets( array(), $data['ancestor'] ); + $this->assertSameSets( array(), $data['allowed_blocks'] ); $this->assertNull( $data['icon'] ); - $this->assertNull( $data['editor_script'] ); - $this->assertNull( $data['script'] ); - $this->assertNull( $data['view_script'] ); - $this->assertNull( $data['editor_style'] ); - $this->assertNull( $data['style'] ); - $this->assertSameSets( array(), $data['attributes'] ); + $this->assertSame( '', $data['description'] ); + $this->assertSameSets( array(), $data['keywords'] ); + $this->assertNull( $data['textdomain'] ); + $this->assertSameSetsWithIndex( + array( + 'lock' => array( 'type' => 'object' ), + 'metadata' => array( 'type' => 'object' ), + ), + $data['attributes'] + ); $this->assertSameSets( array(), $data['provides_context'] ); $this->assertSameSets( array(), $data['uses_context'] ); - $this->assertSameSets( array(), $data['keywords'] ); - $this->assertSameSets( array(), $data['parent'] ); + $this->assertSameSets( array(), $data['selectors'], 'selectors defaults to empty array' ); $this->assertSameSets( array(), $data['supports'] ); $this->assertSameSets( array(), $data['styles'] ); $this->assertNull( $data['example'] ); - $this->assertNull( $data['category'] ); - $this->assertNull( $data['example'] ); - $this->assertNull( $data['textdomain'] ); - $this->assertFalse( $data['is_dynamic'] ); $this->assertSameSets( array(), $data['variations'] ); + $this->assertSameSets( array(), $data['block_hooks'], 'block_hooks defaults to empty array' ); + $this->assertSameSets( array(), $data['editor_script_handles'] ); + $this->assertSameSets( array(), $data['script_handles'] ); + $this->assertSameSets( array(), $data['view_script_handles'] ); + $this->assertSameSets( array(), $data['view_script_module_ids'] ); + $this->assertSameSets( array(), $data['editor_style_handles'] ); + $this->assertSameSets( array(), $data['style_handles'] ); + $this->assertFalse( $data['is_dynamic'] ); + // Deprecated properties. + $this->assertNull( $data['editor_script'] ); + $this->assertNull( $data['script'] ); + $this->assertNull( $data['view_script'] ); + $this->assertNull( $data['editor_style'] ); + $this->assertNull( $data['style'] ); + } + + /** + * @ticket 56733 + */ + public function test_get_item_deprecated() { + $block_type = 'fake/deprecated'; + $settings = array( + 'editor_script' => 'hello_world', + 'script' => 'gutenberg', + 'view_script' => 'foo_bar', + 'editor_style' => 'guten_tag', + 'style' => 'out_of_style', + ); + register_block_type( $block_type, $settings ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/' . $block_type ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSameSets( + array( 'hello_world' ), + $data['editor_script_handles'], + "Endpoint doesn't return correct array for editor_script_handles." + ); + $this->assertSameSets( + array( 'gutenberg' ), + $data['script_handles'], + "Endpoint doesn't return correct array for script_handles." + ); + $this->assertSameSets( + array( 'foo_bar' ), + $data['view_script_handles'], + "Endpoint doesn't return correct array for view_script_handles." + ); + $this->assertSameSets( + array( 'guten_tag' ), + $data['editor_style_handles'], + "Endpoint doesn't return correct array for editor_style_handles." + ); + $this->assertSameSets( + array( 'out_of_style' ), + $data['style_handles'], + "Endpoint doesn't return correct array for style_handles." + ); + // Deprecated properties. + $this->assertSame( + 'hello_world', + $data['editor_script'], + "Endpoint doesn't return correct string for editor_script." + ); + $this->assertSame( + 'gutenberg', + $data['script'], + "Endpoint doesn't return correct string for script." + ); + $this->assertSame( + 'foo_bar', + $data['view_script'], + "Endpoint doesn't return correct string for view_script." + ); + $this->assertSame( + 'guten_tag', + $data['editor_style'], + "Endpoint doesn't return correct string for editor_style." + ); + $this->assertSame( + 'out_of_style', + $data['style'], + "Endpoint doesn't return correct string for style." + ); + } + + /** + * @ticket 56733 + */ + public function test_get_item_deprecated_with_arrays() { + $block_type = 'fake/deprecated-with-arrays'; + $settings = array( + 'editor_script' => array( 'hello', 'world' ), + 'script' => array( 'gutenberg' ), + 'view_script' => array( 'foo', 'bar' ), + 'editor_style' => array( 'guten', 'tag' ), + 'style' => array( 'out', 'of', 'style' ), + ); + register_block_type( $block_type, $settings ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/' . $block_type ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSameSets( + $settings['editor_script'], + $data['editor_script_handles'], + "Endpoint doesn't return correct array for editor_script_handles." + ); + $this->assertSameSets( + $settings['script'], + $data['script_handles'], + "Endpoint doesn't return correct array for script_handles." + ); + $this->assertSameSets( + $settings['view_script'], + $data['view_script_handles'], + "Endpoint doesn't return correct array for view_script_handles." + ); + $this->assertSameSets( + $settings['editor_style'], + $data['editor_style_handles'], + "Endpoint doesn't return correct array for editor_style_handles." + ); + $this->assertSameSets( + $settings['style'], + $data['style_handles'], + "Endpoint doesn't return correct array for style_handles." + ); + // Deprecated properties. + // Since the schema only allows strings or null (but no arrays), we return the first array item. + // Deprecated properties. + $this->assertSame( + 'hello', + $data['editor_script'], + "Endpoint doesn't return first array element for editor_script." + ); + $this->assertSame( + 'gutenberg', + $data['script'], + "Endpoint doesn't return first array element for script." + ); + $this->assertSame( + 'foo', + $data['view_script'], + "Endpoint doesn't return first array element for view_script." + ); + $this->assertSame( + 'guten', + $data['editor_style'], + "Endpoint doesn't return first array element for editor_style." + ); + $this->assertSame( + 'out', + $data['style'], + "Endpoint doesn't return first array element for style." + ); } public function test_get_variation() { @@ -371,6 +554,9 @@ public function test_get_variation() { /** * @ticket 47620 + * @ticket 57585 + * @ticket 59346 + * @ticket 60403 */ public function test_get_item_schema() { wp_set_current_user( self::$admin_id ); @@ -378,84 +564,208 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 22, $properties ); + $this->assertCount( 33, $properties ); $this->assertArrayHasKey( 'api_version', $properties ); + $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'category', $properties ); + $this->assertArrayHasKey( 'parent', $properties ); + $this->assertArrayHasKey( 'ancestor', $properties ); + $this->assertArrayHasKey( 'allowed_blocks', $properties ); $this->assertArrayHasKey( 'icon', $properties ); $this->assertArrayHasKey( 'description', $properties ); $this->assertArrayHasKey( 'keywords', $properties ); - $this->assertArrayHasKey( 'styles', $properties ); $this->assertArrayHasKey( 'textdomain', $properties ); - $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'attributes', $properties ); + $this->assertArrayHasKey( 'provides_context', $properties ); + $this->assertArrayHasKey( 'uses_context', $properties ); + $this->assertArrayHasKey( 'selectors', $properties, 'schema must contain selectors' ); $this->assertArrayHasKey( 'supports', $properties ); - $this->assertArrayHasKey( 'category', $properties ); + $this->assertArrayHasKey( 'styles', $properties ); + $this->assertArrayHasKey( 'example', $properties ); + $this->assertArrayHasKey( 'variations', $properties ); + $this->assertArrayHasKey( 'block_hooks', $properties ); + $this->assertArrayHasKey( 'editor_script_handles', $properties ); + $this->assertArrayHasKey( 'script_handles', $properties ); + $this->assertArrayHasKey( 'view_script_handles', $properties ); + $this->assertArrayHasKey( 'view_script_module_ids', $properties ); + $this->assertArrayHasKey( 'editor_style_handles', $properties ); + $this->assertArrayHasKey( 'style_handles', $properties ); + $this->assertArrayHasKey( 'view_style_handles', $properties, 'schema must contain view_style_handles' ); $this->assertArrayHasKey( 'is_dynamic', $properties ); + // Deprecated properties. $this->assertArrayHasKey( 'editor_script', $properties ); $this->assertArrayHasKey( 'script', $properties ); $this->assertArrayHasKey( 'view_script', $properties ); $this->assertArrayHasKey( 'editor_style', $properties ); $this->assertArrayHasKey( 'style', $properties ); - $this->assertArrayHasKey( 'parent', $properties ); - $this->assertArrayHasKey( 'example', $properties ); - $this->assertArrayHasKey( 'uses_context', $properties ); - $this->assertArrayHasKey( 'provides_context', $properties ); - $this->assertArrayHasKey( 'variations', $properties ); } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $block_name = 'fake/test'; + wp_set_current_user( self::$admin_id ); + + $hook_name = 'rest_prepare_block_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/' . $block_name ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_type_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/block-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'title' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/block-types/fake/test' ), + 'get_items request' => array( '/wp/v2/block-types' ), + ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_wrong_permission() { + public function test_get_items_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_wrong_permission() { + public function test_get_item_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/fake/test' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/fake/test' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/fake/test' ); + $request = new WP_REST_Request( $method, '/wp/v2/block-types/fake/test' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_block_type_cannot_view', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 47620 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ public function test_prepare_item() { - $registry = new WP_Block_Type_Registry; + $registry = new WP_Block_Type_Registry(); $settings = array( 'icon' => 'text', 'render_callback' => '__return_null', ); $registry->register( 'fake/line', $settings ); $block_type = $registry->get_registered( 'fake/line' ); - $endpoint = new WP_REST_Block_Types_Controller; - $request = new WP_REST_Request; + $endpoint = new WP_REST_Block_Types_Controller(); + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $response = $endpoint->prepare_item_for_response( $block_type, $request ); $this->check_block_type_object( $block_type, $response->get_data(), $response->get_links() ); @@ -465,15 +775,15 @@ public function test_prepare_item() { * @ticket 47620 */ public function test_prepare_item_limit_fields() { - $registry = new WP_Block_Type_Registry; + $registry = new WP_Block_Type_Registry(); $settings = array( 'icon' => 'text', 'render_callback' => '__return_null', ); $registry->register( 'fake/line', $settings ); $block_type = $registry->get_registered( 'fake/line' ); - $request = new WP_REST_Request; - $endpoint = new WP_REST_Block_Types_Controller; + $request = new WP_REST_Request(); + $endpoint = new WP_REST_Block_Types_Controller(); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'name' ); $response = $endpoint->prepare_item_for_response( $block_type, $request ); @@ -489,6 +799,7 @@ public function test_prepare_item_limit_fields() { * Util check block type object against. * * @since 5.5.0 + * @since 6.4.0 Added the `block_hooks` extra field. * * @param WP_Block_Type $block_type Sample block type. * @param array $data Data to compare against. @@ -502,23 +813,35 @@ protected function check_block_type_object( $block_type, $data, $links ) { $extra_fields = array( 'api_version', 'name', - 'category', - 'editor_script', - 'script', - 'view_script', - 'editor_style', - 'style', 'title', + 'category', + 'parent', + 'ancestor', + 'allowedBlocks', 'icon', 'description', 'keywords', - 'parent', + 'textdomain', 'provides_context', 'uses_context', + 'selectors', 'supports', 'styles', - 'textdomain', 'example', + 'variations', + 'block_hooks', + 'editor_script_handles', + 'script_handles', + 'view_script_handles', + 'view_script_module_ids', + 'editor_style_handles', + 'style_handles', + // Deprecated fields. + 'editor_script', + 'script', + 'view_script', + 'editor_style', + 'style', ); foreach ( $extra_fields as $extra_field ) { @@ -536,17 +859,58 @@ protected function check_block_type_object( $block_type, $data, $links ) { } /** - * The test_create_item() method does not exist for block types. + * @ticket 59969 */ - public function test_create_item() {} + public function test_variation_callback() { + $block_type = 'test/block'; + $settings = array( + 'title' => true, + 'variation_callback' => array( $this, 'mock_variation_callback' ), + ); + register_block_type( $block_type, $settings ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/block-types/' . $block_type ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSameSets( $this->mock_variation_callback(), $data['variations'] ); + } /** - * The test_update_item() method does not exist for block types. + * Mock variation callback. + * + * @return array */ - public function test_update_item() {} + public function mock_variation_callback() { + return array( + array( 'name' => 'var1' ), + array( 'name' => 'var2' ), + ); + } /** - * The test_delete_item() method does not exist for block types. + * The create_item() method does not exist for block types. + * + * @doesNotPerformAssertions */ - public function test_delete_item() {} + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * The update_item() method does not exist for block types. + * + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement create_item(). + } + + /** + * The delete_item() method does not exist for block types. + * + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } } diff --git a/tests/phpunit/tests/rest-api/rest-blocks-controller.php b/tests/phpunit/tests/rest-api/rest-blocks-controller.php index c3e0a9fb75b1c..43a181683c788 100644 --- a/tests/phpunit/tests/rest-api/rest-blocks-controller.php +++ b/tests/phpunit/tests/rest-api/rest-blocks-controller.php @@ -1,18 +1,12 @@ true, + 'type' => 'string', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'string', + 'properties' => array( + 'sync_status' => array( + 'type' => 'string', + ), + ), + ), + ), + ) + ); + wp_set_current_user( self::$user_ids['author'] ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/blocks/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'wp_pattern_sync_status', $data ); + $this->assertArrayNotHasKey( 'wp_pattern_sync_status', $data['meta'] ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-categories-controller.php b/tests/phpunit/tests/rest-api/rest-categories-controller.php index 311137159927a..5d1f893d87fd4 100644 --- a/tests/phpunit/tests/rest-api/rest-categories-controller.php +++ b/tests/phpunit/tests/rest-api/rest-categories-controller.php @@ -5,9 +5,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Categories_Controller extends WP_Test_REST_Controller_Testcase { @@ -38,7 +36,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { // Set up categories for pagination tests. for ( $i = 0; $i < self::$total_categories - 1; $i++ ) { - $category_ids[] = $factory->category->create( + self::$category_ids[] = $factory->category->create( array( 'name' => "Category {$i}", ) @@ -117,13 +115,15 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. - $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) ); + $category1 = self::factory()->category->create( array( 'name' => 'Season 5' ) ); $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/categories/' . $category1 ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } @@ -170,9 +170,9 @@ public function test_get_items_invalid_permission_for_context() { } public function test_get_items_hide_empty_arg() { - $post_id = $this->factory->post->create(); - $category1 = $this->factory->category->create( array( 'name' => 'Season 5' ) ); - $category2 = $this->factory->category->create( array( 'name' => 'The Be Sharps' ) ); + $post_id = self::factory()->post->create(); + $category1 = self::factory()->category->create( array( 'name' => 'Season 5' ) ); + $category2 = self::factory()->category->create( array( 'name' => 'The Be Sharps' ) ); $total_categories = self::$total_categories + 2; @@ -195,15 +195,15 @@ public function test_get_items_hide_empty_arg() { } public function test_get_items_parent_zero_arg() { - $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) ); - $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) ); - $this->factory->category->create( + $parent1 = self::factory()->category->create( array( 'name' => 'Homer' ) ); + $parent2 = self::factory()->category->create( array( 'name' => 'Marge' ) ); + self::factory()->category->create( array( 'name' => 'Bart', 'parent' => $parent1, ) ); - $this->factory->category->create( + self::factory()->category->create( array( 'name' => 'Lisa', 'parent' => $parent2, @@ -223,19 +223,19 @@ public function test_get_items_parent_zero_arg() { 'parent' => 0, ); $categories = get_terms( 'category', $args ); - $this->assertSame( count( $categories ), count( $data ) ); + $this->assertCount( count( $categories ), $data ); } public function test_get_items_parent_zero_arg_string() { - $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) ); - $parent2 = $this->factory->category->create( array( 'name' => 'Marge' ) ); - $this->factory->category->create( + $parent1 = self::factory()->category->create( array( 'name' => 'Homer' ) ); + $parent2 = self::factory()->category->create( array( 'name' => 'Marge' ) ); + self::factory()->category->create( array( 'name' => 'Bart', 'parent' => $parent1, ) ); - $this->factory->category->create( + self::factory()->category->create( array( 'name' => 'Lisa', 'parent' => $parent2, @@ -255,11 +255,11 @@ public function test_get_items_parent_zero_arg_string() { 'parent' => 0, ); $categories = get_terms( 'category', $args ); - $this->assertSame( count( $categories ), count( $data ) ); + $this->assertCount( count( $categories ), $data ); } public function test_get_items_by_parent_non_found() { - $parent1 = $this->factory->category->create( array( 'name' => 'Homer' ) ); + $parent1 = self::factory()->category->create( array( 'name' => 'Homer' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); $request->set_param( 'parent', $parent1 ); @@ -282,8 +282,8 @@ public function test_get_items_invalid_page() { } public function test_get_items_include_query() { - $id1 = $this->factory->category->create(); - $id2 = $this->factory->category->create(); + $id1 = self::factory()->category->create(); + $id2 = self::factory()->category->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); @@ -303,8 +303,8 @@ public function test_get_items_include_query() { } public function test_get_items_exclude_query() { - $id1 = $this->factory->category->create(); - $id2 = $this->factory->category->create(); + $id1 = self::factory()->category->create(); + $id2 = self::factory()->category->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); $request->set_param( 'per_page', self::$per_page ); @@ -323,8 +323,8 @@ public function test_get_items_exclude_query() { } public function test_get_items_orderby_args() { - $this->factory->category->create( array( 'name' => 'Apple' ) ); - $this->factory->category->create( array( 'name' => 'Banana' ) ); + self::factory()->category->create( array( 'name' => 'Apple' ) ); + self::factory()->category->create( array( 'name' => 'Banana' ) ); /* * Tests: @@ -354,9 +354,9 @@ public function test_get_items_orderby_args() { } public function test_get_items_orderby_id() { - $this->factory->category->create( array( 'name' => 'Cantaloupe' ) ); - $this->factory->category->create( array( 'name' => 'Apple' ) ); - $this->factory->category->create( array( 'name' => 'Banana' ) ); + self::factory()->category->create( array( 'name' => 'Cantaloupe' ) ); + self::factory()->category->create( array( 'name' => 'Apple' ) ); + self::factory()->category->create( array( 'name' => 'Banana' ) ); // Defaults to 'orderby' => 'name', 'order' => 'asc'. $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); @@ -390,9 +390,9 @@ public function test_get_items_orderby_id() { } public function test_get_items_orderby_slugs() { - $this->factory->category->create( array( 'name' => 'Burrito' ) ); - $this->factory->category->create( array( 'name' => 'Taco' ) ); - $this->factory->category->create( array( 'name' => 'Chalupa' ) ); + self::factory()->category->create( array( 'name' => 'Burrito' ) ); + self::factory()->category->create( array( 'name' => 'Taco' ) ); + self::factory()->category->create( array( 'name' => 'Chalupa' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); $request->set_param( 'orderby', 'include_slugs' ); @@ -406,20 +406,20 @@ public function test_get_items_orderby_slugs() { } protected function post_with_categories() { - $post_id = $this->factory->post->create(); - $category1 = $this->factory->category->create( + $post_id = self::factory()->post->create(); + $category1 = self::factory()->category->create( array( 'name' => 'DC', 'description' => 'Purveyor of fine detective comics', ) ); - $category2 = $this->factory->category->create( + $category2 = self::factory()->category->create( array( 'name' => 'Marvel', 'description' => 'Home of the Marvel Universe', ) ); - $category3 = $this->factory->category->create( + $category3 = self::factory()->category->create( array( 'name' => 'Image', 'description' => 'American independent comic publisher', @@ -491,25 +491,25 @@ public function test_get_items_custom_tax_post_args() { register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) ); $controller = new WP_REST_Terms_Controller( 'batman' ); $controller->register_routes(); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman', ) ); - $term2 = $this->factory->term->create( + $term2 = self::factory()->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman', ) ); - $this->factory->term->create( + self::factory()->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman', ) ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' ); $request = new WP_REST_Request( 'GET', '/wp/v2/batman' ); @@ -523,8 +523,8 @@ public function test_get_items_custom_tax_post_args() { } public function test_get_items_search_args() { - $this->factory->category->create( array( 'name' => 'Apple' ) ); - $this->factory->category->create( array( 'name' => 'Banana' ) ); + self::factory()->category->create( array( 'name' => 'Apple' ) ); + self::factory()->category->create( array( 'name' => 'Banana' ) ); /* * Tests: @@ -547,8 +547,8 @@ public function test_get_items_search_args() { } public function test_get_items_slug_arg() { - $this->factory->category->create( array( 'name' => 'Apple' ) ); - $this->factory->category->create( array( 'name' => 'Banana' ) ); + self::factory()->category->create( array( 'name' => 'Apple' ) ); + self::factory()->category->create( array( 'name' => 'Banana' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); $request->set_param( 'slug', 'apple' ); @@ -560,8 +560,8 @@ public function test_get_items_slug_arg() { } public function test_get_terms_parent_arg() { - $category1 = $this->factory->category->create( array( 'name' => 'Parent' ) ); - $this->factory->category->create( + $category1 = self::factory()->category->create( array( 'name' => 'Parent' ) ); + self::factory()->category->create( array( 'name' => 'Child', 'parent' => $category1, @@ -576,8 +576,14 @@ public function test_get_terms_parent_arg() { $this->assertSame( 'Child', $data[0]['name'] ); } - public function test_get_terms_invalid_parent_arg() { - $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_invalid_parent_arg( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); $request->set_param( 'parent', 'invalid-parent' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -585,13 +591,13 @@ public function test_get_terms_invalid_parent_arg() { public function test_get_terms_private_taxonomy() { register_taxonomy( 'robin', 'post', array( 'public' => false ) ); - $this->factory->term->create( + self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', ) ); - $this->factory->term->create( + self::factory()->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin', @@ -609,17 +615,25 @@ public function test_get_terms_invalid_taxonomy() { $this->assertErrorResponse( 'rest_no_route', $response, 404 ); } - public function test_get_terms_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_pagination_headers( $method ) { $total_categories = self::$total_categories; $total_pages = (int) ceil( $total_categories / 10 ); // Start of the index + Uncategorized default term. - $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 10, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 10, $response->get_data() ); + } $next_link = add_query_arg( array( 'page' => 2, @@ -630,9 +644,9 @@ public function test_get_terms_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // 3rd page. - $this->factory->category->create(); - $total_categories++; - $total_pages++; + self::factory()->category->create(); + ++$total_categories; + ++$total_pages; $request = new WP_REST_Request( 'GET', '/wp/v2/categories' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); @@ -662,7 +676,9 @@ public function test_get_terms_pagination_headers() { $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 1, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 1, $response->get_data() ); + } $prev_link = add_query_arg( array( 'page' => $total_pages - 1, @@ -679,7 +695,9 @@ public function test_get_terms_pagination_headers() { $headers = $response->get_headers(); $this->assertSame( $total_categories, $headers['X-WP-Total'] ); $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); - $this->assertCount( 0, $response->get_data() ); + if ( 'HEAD' !== $method ) { + $this->assertCount( 0, $response->get_data() ); + } $prev_link = add_query_arg( array( 'page' => $total_pages, @@ -773,7 +791,7 @@ public function test_get_item_invalid_permission_for_context() { public function test_get_term_private_taxonomy() { register_taxonomy( 'robin', 'post', array( 'public' => false ) ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', @@ -787,7 +805,7 @@ public function test_get_term_private_taxonomy() { public function test_get_item_incorrect_taxonomy() { register_taxonomy( 'robin', 'post' ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', @@ -822,7 +840,7 @@ public function test_create_item() { public function test_create_item_term_already_exists() { wp_set_current_user( self::$administrator ); - $existing_id = $this->factory->category->create( array( 'name' => 'Existing' ) ); + $existing_id = self::factory()->category->create( array( 'name' => 'Existing' ) ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories' ); $request->set_param( 'name', 'Existing' ); @@ -888,7 +906,7 @@ public function test_create_item_with_parent() { public function test_create_item_invalid_parent() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'name', 'My Awesome Term' ); @@ -920,7 +938,7 @@ public function test_update_item() { 'slug' => 'original-slug', ); - $term = get_term_by( 'id', $this->factory->category->create( $orig_args ), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create( $orig_args ), 'category' ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'name', 'New Name' ); @@ -966,7 +984,7 @@ public function test_update_item_invalid_term() { public function test_update_item_incorrect_permissions() { wp_set_current_user( self::$subscriber ); - $term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'name', 'Incorrect permissions' ); @@ -977,8 +995,8 @@ public function test_update_item_incorrect_permissions() { public function test_update_item_parent() { wp_set_current_user( self::$administrator ); - $parent = get_term_by( 'id', $this->factory->category->create(), 'category' ); - $term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $parent = get_term_by( 'id', self::factory()->category->create(), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'parent', $parent->term_id ); @@ -992,12 +1010,12 @@ public function test_update_item_parent() { public function test_update_item_remove_parent() { wp_set_current_user( self::$administrator ); - $old_parent_term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $old_parent_term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $new_parent_id = 0; $term = get_term_by( 'id', - $this->factory->category->create( + self::factory()->category->create( array( 'parent' => $old_parent_term->term_id, ) @@ -1019,7 +1037,7 @@ public function test_update_item_remove_parent() { public function test_update_item_invalid_parent() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $request = new WP_REST_Request( 'POST', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); @@ -1030,7 +1048,7 @@ public function test_update_item_invalid_parent() { public function test_delete_item() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create( array( 'name' => 'Deleted Category' ) ), 'category' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id ); $request->set_param( 'force', true ); @@ -1044,7 +1062,7 @@ public function test_delete_item() { public function test_delete_item_no_trash() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->category->create( array( 'name' => 'Deleted Category' ) ), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create( array( 'name' => 'Deleted Category' ) ), 'category' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); @@ -1074,7 +1092,7 @@ public function test_delete_item_invalid_term() { public function test_delete_item_incorrect_permissions() { wp_set_current_user( self::$subscriber ); - $term = get_term_by( 'id', $this->factory->category->create(), 'category' ); + $term = get_term_by( 'id', self::factory()->category->create(), 'category' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/categories/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_delete', $response, 403 ); @@ -1091,7 +1109,7 @@ public function test_prepare_item() { } public function test_prepare_item_limit_fields() { - $request = new WP_REST_Request; + $request = new WP_REST_Request(); $endpoint = new WP_REST_Terms_Controller( 'category' ); $request->set_param( '_fields', 'id,name' ); $term = get_term( 1, 'category' ); @@ -1106,7 +1124,7 @@ public function test_prepare_item_limit_fields() { } public function test_prepare_taxonomy_term_child() { - $child = $this->factory->category->create( + $child = self::factory()->category->create( array( 'parent' => 1, ) @@ -1168,7 +1186,7 @@ public function test_get_additional_field_registration() { $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] ); $this->assertSame( $schema, $data['schema']['properties']['my_custom_int'] ); - $category_id = $this->factory->category->create(); + $category_id = self::factory()->category->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/categories/' . $category_id ); $response = rest_get_server()->dispatch( $request ); @@ -1178,7 +1196,7 @@ public function test_get_additional_field_registration() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object, $request ) { + public function additional_field_get_callback( $response_data, $field_name ) { return 123; } @@ -1189,7 +1207,7 @@ protected function check_get_taxonomy_terms_response( $response ) { 'hide_empty' => false, ); $categories = get_terms( 'category', $args ); - $this->assertSame( count( $categories ), count( $data ) ); + $this->assertCount( count( $categories ), $data ); $this->assertSame( $categories[0]->term_id, $data[0]['id'] ); $this->assertSame( $categories[0]->name, $data[0]['name'] ); $this->assertSame( $categories[0]->slug, $data[0]['slug'] ); @@ -1236,4 +1254,142 @@ protected function check_get_taxonomy_term_response( $response ) { $category = get_term( 1, 'category' ); $this->check_taxonomy_term( $category, $data, $response->get_links() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/categories' ); + + $filter = new MockAction(); + + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Term_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "ids" for non-HEAD requests.' + ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $terms_table = preg_quote( $wpdb->terms, '/' ); + + $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is'; + + // Assert that the SQL query only fetches the term_id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $category_id = self::factory()->category->create(); + + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/categories/%d', $category_id ) ); + + $hook_name = 'rest_prepare_category'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/categories/1' ), + 'get_items request' => array( '/wp/v2/categories' ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-comments-controller.php b/tests/phpunit/tests/rest-api/rest-comments-controller.php index 0c7ed625b7aea..8542bcd42af24 100644 --- a/tests/phpunit/tests/rest-api/rest-comments-controller.php +++ b/tests/phpunit/tests/rest-api/rest-comments-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase { @@ -14,8 +12,10 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase protected static $admin_id; protected static $editor_id; protected static $moderator_id; + protected static $contributor_id; protected static $subscriber_id; protected static $author_id; + protected static $user_ids = array(); protected static $post_id; protected static $password_id; @@ -28,6 +28,7 @@ class WP_Test_REST_Comments_Controller extends WP_Test_REST_Controller_Testcase protected static $comment_ids = array(); protected static $total_comments = 30; protected static $per_page = 50; + protected static $num_notes = 10; protected $endpoint; @@ -41,33 +42,38 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); - self::$superadmin_id = $factory->user->create( + self::$superadmin_id = $factory->user->create( array( 'role' => 'administrator', 'user_login' => 'superadmin', ) ); - self::$admin_id = $factory->user->create( + self::$admin_id = $factory->user->create( array( 'role' => 'administrator', ) ); - self::$editor_id = $factory->user->create( + self::$editor_id = $factory->user->create( array( 'role' => 'editor', ) ); - self::$moderator_id = $factory->user->create( + self::$moderator_id = $factory->user->create( array( 'role' => 'comment_moderator', ) ); - self::$subscriber_id = $factory->user->create( + self::$contributor_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + self::$subscriber_id = $factory->user->create( array( 'role' => 'subscriber', ) ); - self::$author_id = $factory->user->create( + self::$author_id = $factory->user->create( array( 'role' => 'author', 'display_name' => 'Sea Captain', @@ -115,9 +121,19 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); + self::$user_ids = array( + 'superadmin' => self::$superadmin_id, + 'administrator' => self::$admin_id, + 'editor' => self::$editor_id, + 'moderator' => self::$moderator_id, + 'contributor' => self::$contributor_id, + 'subscriber' => self::$subscriber_id, + 'author' => self::$author_id, + ); + // Set up comments for pagination tests. for ( $i = 0; $i < self::$total_comments - 1; $i++ ) { - $comment_ids[] = $factory->comment->create( + self::$comment_ids[] = $factory->comment->create( array( 'comment_content' => "Comment {$i}", 'comment_post_ID' => self::$post_id, @@ -133,6 +149,7 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$admin_id ); self::delete_user( self::$editor_id ); self::delete_user( self::$moderator_id ); + self::delete_user( self::$contributor_id ); self::delete_user( self::$subscriber_id ); self::delete_user( self::$author_id ); @@ -152,7 +169,9 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); - $this->endpoint = new WP_REST_Comments_Controller; + $this->endpoint = new WP_REST_Comments_Controller(); + wp_create_initial_comment_meta(); + if ( is_multisite() ) { update_site_option( 'site_admins', array( 'superadmin' ) ); } @@ -237,7 +256,7 @@ public function test_get_items_with_password() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_param( 'password', 'toomanysecrets' ); @@ -261,7 +280,7 @@ public function test_get_items_with_password_without_post() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_param( 'password', 'toomanysecrets' ); @@ -284,7 +303,7 @@ public function test_get_items_with_password_with_multiple_post() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_param( 'password', 'toomanysecrets' ); @@ -302,7 +321,7 @@ public function test_get_password_items_without_edit_post_permission() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -321,7 +340,7 @@ public function test_get_password_items_with_edit_post_permission() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -340,7 +359,7 @@ public function test_get_items_without_private_post_permission() { 'comment_post_ID' => self::$private_id, ); - $private_comment = $this->factory->comment->create( $args ); + $private_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -359,7 +378,7 @@ public function test_get_items_with_private_post_permission() { 'comment_post_ID' => self::$private_id, ); - $private_comment = $this->factory->comment->create( $args ); + $private_comment = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -373,7 +392,7 @@ public function test_get_items_with_private_post_permission() { public function test_get_items_with_invalid_post() { wp_set_current_user( 0 ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, @@ -394,7 +413,7 @@ public function test_get_items_with_invalid_post() { public function test_get_items_with_invalid_post_permission() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, @@ -424,7 +443,7 @@ public function test_get_items_no_permission_for_context() { public function test_get_items_no_post() { wp_set_current_user( self::$admin_id ); - $this->factory->comment->create_post_comments( 0, 2 ); + self::factory()->comment->create_post_comments( 0, 2 ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_param( 'post', 0 ); @@ -434,27 +453,51 @@ public function test_get_items_no_post() { $this->assertCount( 2, $comments ); } - public function test_get_items_no_permission_for_no_post() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_no_permission_for_no_post( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'post', 0 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); } - public function test_get_items_edit_context() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_edit_context( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); } public function test_get_items_for_post() { - $second_post_id = $this->factory->post->create(); - $this->factory->comment->create_post_comments( $second_post_id, 2 ); + $second_post_id = self::factory()->post->create(); + self::factory()->comment->create_post_comments( $second_post_id, 2 ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_query_params( @@ -478,8 +521,8 @@ public function test_get_items_include_query() { 'comment_post_ID' => self::$post_id, ); - $id1 = $this->factory->comment->create( $args ); - $id2 = $this->factory->comment->create( $args ); + $id1 = self::factory()->comment->create( $args ); + $id2 = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -518,8 +561,8 @@ public function test_get_items_exclude_query() { 'comment_post_ID' => self::$post_id, ); - $id1 = $this->factory->comment->create( $args ); - $id2 = $this->factory->comment->create( $args ); + $id1 = self::factory()->comment->create( $args ); + $id2 = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $response = rest_get_server()->dispatch( $request ); @@ -574,7 +617,7 @@ public function test_get_items_order_query() { 'comment_post_ID' => self::$post_id, ); - $id = $this->factory->comment->create( $args ); + $id = self::factory()->comment->create( $args ); $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -595,12 +638,18 @@ public function test_get_items_order_query() { $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } - public function test_get_items_private_post_no_permissions() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_private_post_no_permissions( $method ) { wp_set_current_user( 0 ); - $post_id = $this->factory->post->create( array( 'post_status' => 'private' ) ); + $post_id = self::factory()->post->create( array( 'post_status' => 'private' ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $request->set_param( 'post', $post_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read_post', $response, 401 ); @@ -616,11 +665,11 @@ public function test_get_items_author_arg() { 'user_id' => self::$author_id, ); - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $args['user_id'] = self::$subscriber_id; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); unset( $args['user_id'] ); - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); // Limit to comment author. $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); @@ -659,11 +708,11 @@ public function test_get_items_author_exclude_arg() { 'user_id' => self::$author_id, ); - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $args['user_id'] = self::$subscriber_id; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); unset( $args['user_id'] ); - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $total_comments = self::$total_comments + 3; @@ -709,12 +758,12 @@ public function test_get_items_parent_arg() { 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, ); - $parent_id = $this->factory->comment->create( $args ); - $parent_id2 = $this->factory->comment->create( $args ); + $parent_id = self::factory()->comment->create( $args ); + $parent_id2 = self::factory()->comment->create( $args ); $args['comment_parent'] = $parent_id; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $args['comment_parent'] = $parent_id2; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $total_comments = self::$total_comments + 4; @@ -745,12 +794,12 @@ public function test_get_items_parent_exclude_arg() { 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, ); - $parent_id = $this->factory->comment->create( $args ); - $parent_id2 = $this->factory->comment->create( $args ); + $parent_id = self::factory()->comment->create( $args ); + $parent_id2 = self::factory()->comment->create( $args ); $args['comment_parent'] = $parent_id; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $args['comment_parent'] = $parent_id2; - $this->factory->comment->create( $args ); + self::factory()->comment->create( $args ); $total_comments = self::$total_comments + 4; @@ -786,7 +835,7 @@ public function test_get_items_search_query() { 'comment_author' => 'Homer J Simpson', ); - $id = $this->factory->comment->create( $args ); + $id = self::factory()->comment->create( $args ); $total_comments = self::$total_comments + 1; @@ -803,18 +852,24 @@ public function test_get_items_search_query() { $this->assertSame( $id, $data[0]['id'] ); } - public function test_get_comments_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comments_pagination_headers( $method ) { $total_comments = self::$total_comments; $total_pages = (int) ceil( $total_comments / 10 ); wp_set_current_user( self::$admin_id ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); - $this->assertSame( $total_comments, $headers['X-WP-Total'] ); - $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $this->assertSame( (string) $total_comments, $headers['X-WP-Total'] ); + $this->assertSame( (string) $total_pages, $headers['X-WP-TotalPages'] ); $next_link = add_query_arg( array( 'page' => 2, @@ -825,19 +880,19 @@ public function test_get_comments_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // 3rd page. - $this->factory->comment->create( + self::factory()->comment->create( array( 'comment_post_ID' => self::$post_id, ) ); - $total_comments++; - $total_pages++; + ++$total_comments; + ++$total_pages; $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); - $this->assertSame( $total_comments, $headers['X-WP-Total'] ); - $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $this->assertSame( (string) $total_comments, $headers['X-WP-Total'] ); + $this->assertSame( (string) $total_pages, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => 2, @@ -858,8 +913,8 @@ public function test_get_comments_pagination_headers() { $request->set_param( 'page', $total_pages ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); - $this->assertSame( $total_comments, $headers['X-WP-Total'] ); - $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $this->assertSame( (string) $total_comments, $headers['X-WP-Total'] ); + $this->assertSame( (string) $total_pages, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => $total_pages - 1, @@ -874,8 +929,8 @@ public function test_get_comments_pagination_headers() { $request->set_param( 'page', 100 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); - $this->assertSame( $total_comments, $headers['X-WP-Total'] ); - $this->assertEquals( $total_pages, $headers['X-WP-TotalPages'] ); + $this->assertSame( (string) $total_comments, $headers['X-WP-Total'] ); + $this->assertEquals( (string) $total_pages, $headers['X-WP-TotalPages'] ); $prev_link = add_query_arg( array( 'page' => $total_pages, @@ -886,28 +941,34 @@ public function test_get_comments_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); } - public function test_get_comments_invalid_date() { - $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); - $request->set_param( 'after', rand_str() ); - $request->set_param( 'before', rand_str() ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comments_invalid_date( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); + $request->set_param( 'after', 'foo' ); + $request->set_param( 'before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function test_get_comments_valid_date() { - $comment1 = $this->factory->comment->create( + $comment1 = self::factory()->comment->create( array( 'comment_date' => '2016-01-15T00:00:00Z', 'comment_post_ID' => self::$post_id, ) ); - $comment2 = $this->factory->comment->create( + $comment2 = self::factory()->comment->create( array( 'comment_date' => '2016-01-16T00:00:00Z', 'comment_post_ID' => self::$post_id, ) ); - $comment3 = $this->factory->comment->create( + $comment3 = self::factory()->comment->create( array( 'comment_date' => '2016-01-17T00:00:00Z', 'comment_post_ID' => self::$post_id, @@ -953,7 +1014,7 @@ public function test_prepare_item() { public function test_prepare_item_limit_fields() { wp_set_current_user( self::$admin_id ); - $endpoint = new WP_REST_Comments_Controller; + $endpoint = new WP_REST_Comments_Controller(); $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'id,status' ); @@ -968,6 +1029,21 @@ public function test_prepare_item_limit_fields() { ); } + /** + * @ticket 58238 + */ + public function test_prepare_item_comment_text_filter() { + $filter = new MockAction(); + add_filter( 'comment_text', array( $filter, 'filter' ), 10, 3 ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 1, $filter->get_call_count() ); + $this->assertCount( 3, $filter->get_args()[0] ); + } + public function test_get_comment_author_avatar_urls() { $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); @@ -984,70 +1060,106 @@ public function test_get_comment_author_avatar_urls() { $this->assertSame( substr( get_avatar_url( $comment->comment_author_email ), 9 ), substr( $data['author_avatar_urls'][96], 9 ) ); } - public function test_get_comment_invalid_id() { - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_id( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_comment_invalid_id', $response, 404 ); } - public function test_get_comment_invalid_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', self::$approved_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', self::$approved_id ) ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } - public function test_get_comment_invalid_post_id() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_post_id( $method ) { wp_set_current_user( 0 ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); } - public function test_get_comment_invalid_post_id_as_admin() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_invalid_post_id_as_admin( $method ) { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); } - public function test_get_comment_not_approved() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_not_approved( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); } - public function test_get_comment_not_approved_same_user() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_not_approved_same_user( $method ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); } public function test_get_comment_with_children_link() { - $comment_id_1 = $this->factory->comment->create( + $comment_id_1 = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -1055,7 +1167,7 @@ public function test_get_comment_with_children_link() { ) ); - $child_comment = $this->factory->comment->create( + $child_comment = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_parent' => $comment_id_1, @@ -1071,7 +1183,7 @@ public function test_get_comment_with_children_link() { } public function test_get_comment_without_children_link() { - $comment_id_1 = $this->factory->comment->create( + $comment_id_1 = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -1085,7 +1197,13 @@ public function test_get_comment_without_children_link() { $this->assertArrayNotHasKey( 'children', $response->get_links() ); } - public function test_get_comment_with_password_without_edit_post_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_comment_with_password_without_edit_post_permission( $method ) { wp_set_current_user( self::$subscriber_id ); $args = array( @@ -1093,17 +1211,21 @@ public function test_get_comment_with_password_without_edit_post_permission() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 38692 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_comment_with_password_with_valid_password() { + public function test_get_comment_with_password_with_valid_password( $method ) { wp_set_current_user( self::$subscriber_id ); $args = array( @@ -1111,9 +1233,9 @@ public function test_get_comment_with_password_with_valid_password() { 'comment_post_ID' => self::$password_id, ); - $password_comment = $this->factory->comment->create( $args ); + $password_comment = self::factory()->comment->create( $args ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $password_comment ) ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%s', $password_comment ) ); $request->set_param( 'password', 'toomanysecrets' ); $response = rest_get_server()->dispatch( $request ); @@ -1133,7 +1255,7 @@ public function test_create_item() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1146,7 +1268,7 @@ public function test_create_item() { $this->assertSame( self::$post_id, $data['post'] ); } - public function comment_dates_provider() { + public function data_comment_dates() { return array( 'set date without timezone' => array( 'params' => array( @@ -1192,7 +1314,7 @@ public function comment_dates_provider() { } /** - * @dataProvider comment_dates_provider + * @dataProvider data_comment_dates */ public function test_create_comment_date( $params, $results ) { wp_set_current_user( self::$admin_id ); @@ -1239,7 +1361,7 @@ public function test_create_item_using_accepted_content_raw_value() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1264,7 +1386,7 @@ public function test_create_item_error_from_filter() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1287,7 +1409,7 @@ public function test_create_comment_missing_required_author_name() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1307,7 +1429,7 @@ public function test_create_comment_empty_required_author_name() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1327,7 +1449,7 @@ public function test_create_comment_missing_required_author_email() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1347,7 +1469,7 @@ public function test_create_comment_empty_required_author_email() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1365,7 +1487,7 @@ public function test_create_comment_author_email_too_short() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1386,7 +1508,7 @@ public function test_create_item_invalid_no_content() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1413,7 +1535,7 @@ public function test_create_item_invalid_only_spaces_content() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1435,7 +1557,7 @@ public function test_create_item_allows_0_as_content() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1460,7 +1582,7 @@ public function test_create_item_allow_empty_comment_filter() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1477,11 +1599,11 @@ public function test_create_item_invalid_date() { 'author_email' => 'lovejoy@example.com', 'author_url' => 'http://timothylovejoy.jr', 'content' => 'It\'s all over\, people! We don\'t have a prayer!', - 'date' => rand_str(), + 'date' => 'foo-bar', ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1490,7 +1612,7 @@ public function test_create_item_invalid_date() { public function test_create_item_assign_different_user() { - $subscriber_id = $this->factory->user->create( + $subscriber_id = self::factory()->user->create( array( 'role' => 'subscriber', 'user_email' => 'cbg@androidsdungeon.com', @@ -1510,7 +1632,7 @@ public function test_create_item_assign_different_user() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 201, $response->get_status() ); @@ -1521,7 +1643,7 @@ public function test_create_item_assign_different_user() { } public function test_create_comment_without_type() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -1536,7 +1658,7 @@ public function test_create_comment_without_type() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1559,7 +1681,7 @@ public function test_create_comment_without_type() { * @ticket 38820 */ public function test_create_comment_with_invalid_type() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -1575,7 +1697,7 @@ public function test_create_comment_with_invalid_type() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1583,7 +1705,7 @@ public function test_create_comment_with_invalid_type() { } public function test_create_comment_invalid_email() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -1598,7 +1720,7 @@ public function test_create_comment_invalid_email() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1606,7 +1728,7 @@ public function test_create_comment_invalid_email() { } public function test_create_item_current_user() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'subscriber', 'user_email' => 'lylelanley@example.com', @@ -1625,7 +1747,7 @@ public function test_create_item_current_user() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1654,7 +1776,7 @@ public function test_create_comment_other_user() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1679,7 +1801,7 @@ public function test_create_comment_other_user_without_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1699,7 +1821,7 @@ public function test_create_comment_invalid_post() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1720,7 +1842,7 @@ public function test_create_comment_status_without_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1728,7 +1850,7 @@ public function test_create_comment_status_without_permission() { } public function test_create_comment_with_status_IP_and_user_agent() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -1744,7 +1866,7 @@ public function test_create_comment_with_status_IP_and_user_agent() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1768,8 +1890,8 @@ public function test_create_comment_user_agent_header() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); - $request->add_header( 'user_agent', 'Mozilla/4.0 (compatible; MSIE 5.5; AOL 4.0; Windows 95)' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->add_header( 'User_Agent', 'Mozilla/4.0 (compatible; MSIE 5.5; AOL 4.0; Windows 95)' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1794,7 +1916,7 @@ public function test_create_comment_author_ip() { 'status' => 'approved', ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1815,7 +1937,7 @@ public function test_create_comment_invalid_author_IP() { 'status' => 'approved', ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1835,7 +1957,7 @@ public function test_create_comment_author_ip_no_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_comment_invalid_author_ip', $response, 403 ); @@ -1855,7 +1977,7 @@ public function test_create_comment_author_ip_defaults_to_remote_addr() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1875,7 +1997,7 @@ public function test_create_comment_no_post_id() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1895,7 +2017,7 @@ public function test_create_comment_no_post_id_no_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1915,7 +2037,7 @@ public function test_create_comment_invalid_post_id() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1935,7 +2057,7 @@ public function test_create_comment_draft_post() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1955,7 +2077,7 @@ public function test_create_comment_trash_post() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1976,7 +2098,7 @@ public function test_create_comment_private_post_invalid_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1996,7 +2118,7 @@ public function test_create_comment_password_post_invalid_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2006,7 +2128,7 @@ public function test_create_comment_password_post_invalid_permission() { public function test_create_item_duplicate() { wp_set_current_user( self::$subscriber_id ); - $this->factory->comment->create( + self::factory()->comment->create( array( 'comment_post_ID' => self::$post_id, 'comment_author' => 'Guy N. Cognito', @@ -2023,7 +2145,7 @@ public function test_create_item_duplicate() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2031,7 +2153,7 @@ public function test_create_item_duplicate() { } public function test_create_comment_closed() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'comment_status' => 'closed', ) @@ -2044,7 +2166,7 @@ public function test_create_comment_closed() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2075,7 +2197,7 @@ public function test_create_item_invalid_author() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2093,7 +2215,7 @@ public function test_create_item_pull_author_info() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2117,7 +2239,7 @@ public function test_create_comment_two_times() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2132,7 +2254,7 @@ public function test_create_comment_two_times() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2156,7 +2278,7 @@ public function test_allow_anonymous_comments_null() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2183,7 +2305,7 @@ public function test_create_comment_author_name_too_long() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2207,7 +2329,7 @@ public function test_create_comment_author_email_too_long() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2231,7 +2353,7 @@ public function test_create_comment_author_url_too_long() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2255,7 +2377,7 @@ public function test_create_comment_content_too_long() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2275,7 +2397,7 @@ public function test_create_comment_without_password() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2296,14 +2418,14 @@ public function test_create_comment_with_password() { $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 201, $response->get_status() ); } public function test_update_item() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -2319,7 +2441,7 @@ public function test_update_item() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2340,14 +2462,14 @@ public function test_update_item() { } /** - * @dataProvider comment_dates_provider + * @dataProvider data_comment_dates */ public function test_update_comment_date( $params, $results ) { wp_set_current_user( self::$editor_id ); update_option( 'timezone_string', $params['timezone_string'] ); - $comment_id = $this->factory->comment->create(); + $comment_id = self::factory()->comment->create(); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); if ( isset( $params['date'] ) ) { @@ -2374,7 +2496,7 @@ public function test_update_comment_date( $params, $results ) { } public function test_update_item_no_content() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$admin_id ); @@ -2392,6 +2514,30 @@ public function test_update_item_no_content() { $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 ); } + /** + * @ticket 64049 + */ + public function test_update_item_no_content_allow_empty_comment_filter() { + $post_id = self::factory()->post->create(); + + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); + $request->set_param( 'author_email', 'another@email.com' ); + + // Sending a request without content is fine. + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Sending a request with empty comment content is also fine. + $request->set_param( 'author_email', 'yetanother@email.com' ); + $request->set_param( 'content', '' ); + add_filter( 'allow_empty_comment', '__return_true' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( 'allow_empty_comment', '__return_true' ); + $this->assertSame( 200, $response->get_status() ); + } + public function test_update_item_no_change() { $comment = get_comment( self::$approved_id ); @@ -2412,7 +2558,7 @@ public function test_update_item_no_change() { public function test_update_comment_status() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 0, 'comment_post_ID' => self::$post_id, @@ -2424,7 +2570,7 @@ public function test_update_comment_status() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2439,7 +2585,7 @@ public function test_update_comment_status() { public function test_update_comment_field_does_not_use_default_values() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 0, 'comment_post_ID' => self::$post_id, @@ -2452,7 +2598,7 @@ public function test_update_comment_field_does_not_use_default_values() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $comment_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2474,7 +2620,7 @@ public function test_update_comment_date_gmt() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2498,7 +2644,7 @@ public function test_update_comment_author_email_only() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2518,7 +2664,7 @@ public function test_update_comment_empty_author_name() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2537,7 +2683,7 @@ public function test_update_comment_author_name_only() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2557,7 +2703,7 @@ public function test_update_comment_empty_author_email() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2575,7 +2721,7 @@ public function test_update_comment_author_email_too_short() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2592,7 +2738,7 @@ public function test_update_comment_invalid_type() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2609,7 +2755,7 @@ public function test_update_comment_with_raw_property() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2625,12 +2771,12 @@ public function test_update_item_invalid_date() { wp_set_current_user( self::$admin_id ); $params = array( - 'content' => rand_str(), - 'date' => rand_str(), + 'content' => 'content', + 'date' => 'foo', ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2641,12 +2787,12 @@ public function test_update_item_invalid_date_gmt() { wp_set_current_user( self::$admin_id ); $params = array( - 'content' => rand_str(), - 'date_gmt' => rand_str(), + 'content' => 'content', + 'date_gmt' => 'foo', ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2661,7 +2807,7 @@ public function test_update_comment_invalid_id() { ); $request = new WP_REST_Request( 'PUT', '/wp/v2/comments/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2686,7 +2832,7 @@ public function test_update_comment_invalid_permission() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$hold_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2705,7 +2851,7 @@ public function test_update_comment_when_can_moderate_comments() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2720,7 +2866,7 @@ public function test_update_comment_when_can_moderate_comments() { } public function test_update_comment_private_post_invalid_permission() { - $private_comment_id = $this->factory->comment->create( + $private_comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$private_id, @@ -2735,7 +2881,7 @@ public function test_update_comment_private_post_invalid_permission() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', $private_comment_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2745,7 +2891,7 @@ public function test_update_comment_private_post_invalid_permission() { public function test_update_comment_with_children_link() { wp_set_current_user( self::$admin_id ); - $comment_id_1 = $this->factory->comment->create( + $comment_id_1 = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -2753,7 +2899,7 @@ public function test_update_comment_with_children_link() { ) ); - $child_comment = $this->factory->comment->create( + $child_comment = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -2770,7 +2916,7 @@ public function test_update_comment_with_children_link() { // Change the comment parent. $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%s', $child_comment ) ); $request->set_param( 'parent', $comment_id_1 ); - $request->set_param( 'content', rand_str() ); + $request->set_param( 'content', 'foo bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); @@ -2794,7 +2940,7 @@ public function test_update_comment_author_name_too_long() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2814,7 +2960,7 @@ public function test_update_comment_author_email_too_long() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2834,7 +2980,7 @@ public function test_update_comment_author_url_too_long() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2853,7 +2999,7 @@ public function test_update_comment_content_too_long() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2874,7 +3020,7 @@ public function test_update_comment_is_wp_error() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -2972,6 +3118,7 @@ public function test_comment_roundtrip_as_editor_unfiltered_html() { 'content' => '
      div
      strong ', 'author_name' => '
      div
      strong ', 'author_user_agent' => '
      div
      strong ', + 'author' => self::$editor_id, ), array( 'content' => array( @@ -2980,6 +3127,7 @@ public function test_comment_roundtrip_as_editor_unfiltered_html() { ), 'author_name' => 'div strong', 'author_user_agent' => 'div strong', + 'author' => self::$editor_id, ) ); } else { @@ -2989,6 +3137,7 @@ public function test_comment_roundtrip_as_editor_unfiltered_html() { 'content' => '
      div
      strong ', 'author_name' => '
      div
      strong ', 'author_user_agent' => '
      div
      strong ', + 'author' => self::$editor_id, ), array( 'content' => array( @@ -2997,6 +3146,7 @@ public function test_comment_roundtrip_as_editor_unfiltered_html() { ), 'author_name' => 'div strong', 'author_user_agent' => 'div strong', + 'author' => self::$editor_id, ) ); } @@ -3011,6 +3161,7 @@ public function test_comment_roundtrip_as_superadmin() { 'content' => '\\\&\\\ & &invalid; < < &lt;', 'author_name' => '\\\&\\\ & &invalid; < < &lt;', 'author_user_agent' => '\\\&\\\ & &invalid; < < &lt;', + 'author' => self::$superadmin_id, ), array( 'content' => array( @@ -3019,6 +3170,7 @@ public function test_comment_roundtrip_as_superadmin() { ), 'author_name' => '\\\&\\\ & &invalid; < < &lt;', 'author_user_agent' => '\\\&\\\ & &invalid; < < &lt;', + 'author' => self::$superadmin_id, ) ); } @@ -3032,6 +3184,7 @@ public function test_comment_roundtrip_as_superadmin_unfiltered_html() { 'content' => '
      div
      strong ', 'author_name' => '
      div
      strong ', 'author_user_agent' => '
      div
      strong ', + 'author' => self::$superadmin_id, ), array( 'content' => array( @@ -3040,6 +3193,7 @@ public function test_comment_roundtrip_as_superadmin_unfiltered_html() { ), 'author_name' => 'div strong', 'author_user_agent' => 'div strong', + 'author' => self::$superadmin_id, ) ); } @@ -3047,7 +3201,7 @@ public function test_comment_roundtrip_as_superadmin_unfiltered_html() { public function test_delete_item() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -3067,7 +3221,7 @@ public function test_delete_item() { public function test_delete_item_skip_trash() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -3088,7 +3242,7 @@ public function test_delete_item_skip_trash() { public function test_delete_item_already_trashed() { wp_set_current_user( self::$admin_id ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -3123,7 +3277,7 @@ public function test_delete_comment_without_permission() { public function test_delete_child_comment_link() { wp_set_current_user( self::$admin_id ); - $comment_id_1 = $this->factory->comment->create( + $comment_id_1 = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => self::$post_id, @@ -3131,7 +3285,7 @@ public function test_delete_child_comment_link() { ) ); - $child_comment = $this->factory->comment->create( + $child_comment = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_parent' => $comment_id_1, @@ -3290,15 +3444,15 @@ public function test_additional_field_update_errors() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { - return get_comment_meta( $object['id'], 'my_custom_int', true ); + public function additional_field_get_callback( $response_data, $field_name ) { + return get_comment_meta( $response_data['id'], $field_name, true ); } - public function additional_field_update_callback( $value, $comment ) { + public function additional_field_update_callback( $value, $comment, $field_name ) { if ( 'returnError' === $value ) { return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); } - update_comment_meta( $comment->comment_ID, 'my_custom_int', $value ); + update_comment_meta( $comment->comment_ID, $field_name, $value ); } protected function check_comment_data( $data, $context, $links ) { @@ -3333,9 +3487,7 @@ protected function check_comment_data( $data, $context, $links ) { $this->assertSame( $comment->comment_author_IP, $data['author_ip'] ); $this->assertSame( $comment->comment_agent, $data['author_user_agent'] ); $this->assertSame( $comment->comment_content, $data['content']['raw'] ); - } - - if ( 'edit' !== $context ) { + } else { $this->assertArrayNotHasKey( 'author_email', $data ); $this->assertArrayNotHasKey( 'author_ip', $data ); $this->assertArrayNotHasKey( 'author_user_agent', $data ); @@ -3344,9 +3496,13 @@ protected function check_comment_data( $data, $context, $links ) { } /** + * @dataProvider data_readable_http_methods * @ticket 42238 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_check_read_post_permission_with_invalid_post_type() { + public function test_check_read_post_permission_with_invalid_post_type( $method ) { register_post_type( 'bug-post', array( @@ -3365,8 +3521,730 @@ public function test_check_read_post_permission_with_invalid_post_type() { $this->setExpectedIncorrectUsage( 'map_meta_cap' ); wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request = new WP_REST_Request( $method, '/wp/v2/comments/' . $comment_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 403, $response->get_status() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/comments' ); + + $filter = new MockAction(); + + add_filter( 'comments_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Comment_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Comment_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_comment_meta_cache', $query->query_vars, 'The update_comment_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_comment_meta_cache'], 'The update_comment_meta_cache value should be true for non-HEAD requests.' ); + return; + } + + global $wpdb; + $comments_table = preg_quote( $wpdb->comments, '/' ); + $pattern = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $comments_table . '\.comment_ID\s+FROM\s+' . $comments_table . '\s+WHERE/i'; + + // Assert that the SQL query only fetches the ID column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/comments/%d', self::$approved_id ) ); + + $hook_name = 'rest_prepare_comment'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $headers = $response->get_headers(); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', sprintf( $path, self::$approved_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/comments/%d' ), + 'get_items request' => array( '/wp/v2/comments' ), + ); + } + + /** + * Create a test post with note. + * + * @param int $user_id Post author's user ID. + * @return int Post ID. + */ + protected function create_test_post_with_note( $role ) { + $user_id = self::$user_ids[ $role ]; + $post_id = self::factory()->post->create( + array( + 'post_title' => 'Test Post for Notes', + 'post_content' => 'This is a test post to check note permissions.', + 'post_status' => 'contributor' === $role ? 'draft' : 'publish', + 'post_author' => $user_id, + ) + ); + + for ( $i = 0; $i < self::$num_notes; $i++ ) { + self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'note', + 'comment_approved' => 0 === $i % 2 ? 1 : 0, + ) + ); + } + + return $post_id; + } + + /** + * @ticket 64096 + */ + public function test_cannot_read_note_without_post_type_support() { + register_post_type( + 'no-notes', + array( + 'label' => 'No Notes', + 'supports' => array( 'title', 'editor', 'author', 'comments' ), + 'show_in_rest' => true, + 'public' => true, + ) + ); + + create_initial_rest_routes(); + wp_set_current_user( self::$admin_id ); + + $post_id = self::factory()->post->create( array( 'post_type' => 'no-notes' ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'context', 'edit' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_not_supported_post_type', $response, 403 ); + + _unregister_post_type( 'no-notes' ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_require_login() { + wp_set_current_user( 0 ); + + $post_id = self::factory()->post->create(); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_comment_login_required', $response, 401 ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_note_without_post_type_support() { + register_post_type( + 'no-note', + array( + 'label' => 'No Notes', + 'supports' => array( 'title', 'editor', 'author', 'comments' ), + 'show_in_rest' => true, + 'public' => true, + ) + ); + + wp_set_current_user( self::$admin_id ); + $post_id = self::factory()->post->create( array( 'post_type' => 'no-note' ) ); + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Call me Ishmael.', + 'author' => self::$admin_id, + 'type' => 'note', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_not_supported_post_type', $response, 403 ); + + _unregister_post_type( 'no-note' ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_draft_post() { + wp_set_current_user( self::$editor_id ); + $draft_id = self::factory()->post->create( + array( + 'post_status' => 'draft', + ) + ); + $params = array( + 'post' => $draft_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Call me Ishmael.', + 'author' => self::$editor_id, + 'type' => 'note', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + $this->assertSame( 'Call me Ishmael.', $new_comment->comment_content ); + $this->assertSame( 'note', $new_comment->comment_type ); + } + + /** + * @ticket 64096 + */ + public function test_create_note_status() { + wp_set_current_user( self::$author_id ); + $post_id = self::factory()->post->create( array( 'post_author' => self::$author_id ) ); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$author_id, + 'type' => 'note', + 'status' => 'hold', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + + $this->assertSame( '0', $new_comment->comment_approved ); + $this->assertSame( 'note', $new_comment->comment_type ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_with_non_valid_comment_type() { + wp_set_current_user( self::$admin_id ); + $post_id = $this->factory->post->create(); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$admin_id, + 'type' => 'review', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_comment_type', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_create_assigns_default_type() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + + $params = array( + 'post' => $post_id, + 'author_name' => 'Ishmael', + 'author_email' => 'herman-melville@earthlink.net', + 'author_url' => 'https://en.wikipedia.org/wiki/Herman_Melville', + 'content' => 'Comic Book Guy', + 'author' => self::$editor_id, + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_comment = get_comment( $data['id'] ); + + $this->assertSame( 'comment', $new_comment->comment_type ); + } + + /** + * @dataProvider data_note_status_provider + * @ticket 64096 + */ + public function test_create_empty_note_with_resolution_meta( $status ) { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + 'meta' => array( + '_wp_note_status' => $status, + ), + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertArrayHasKey( '_wp_note_status', $data['meta'] ); + $this->assertSame( $status, $data['meta']['_wp_note_status'] ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_empty_note_without_resolution_meta() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_cannot_create_empty_note_with_invalid_resolution_meta() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => '', + 'meta' => array( + '_wp_note_status' => 'invalid', + ), + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_comment_content_invalid', $response, 400 ); + } + + /** + * @ticket 64096 + */ + public function test_create_duplicate_note() { + wp_set_current_user( self::$editor_id ); + $post_id = self::factory()->post->create(); + + for ( $i = 0; $i < 2; $i++ ) { + $params = array( + 'post' => $post_id, + 'author_name' => 'Editor', + 'author_email' => 'editor@example.com', + 'author_url' => 'https://example.com', + 'author' => self::$editor_id, + 'type' => 'note', + 'content' => 'Doplicated comment', + ); + $request = new WP_REST_Request( 'POST', '/wp/v2/comments' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $params ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + } + } + + /** + * @dataProvider data_note_get_items_permissions_data_provider + * @ticket 64096 + */ + public function test_note_get_items_permissions_edit_context( $role, $post_author_role, $can_read ) { + wp_set_current_user( self::$user_ids[ $role ] ); + $post_id = $this->create_test_post_with_note( $post_author_role ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', $post_id ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'status', 'all' ); + $request->set_param( 'per_page', 100 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + if ( $can_read ) { + $comments = $response->get_data(); + $this->assertEquals( self::$num_notes, count( $comments ) ); + } else { + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + wp_delete_post( $post_id, true ); + } + + /** + * @ticket 64096 + */ + public function test_note_get_items_permissions_mixed_post_authors() { + $author_post_id = $this->create_test_post_with_note( 'author' ); + $editor_post_id = $this->create_test_post_with_note( 'editor' ); + + wp_set_current_user( self::$author_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', array( $author_post_id, $editor_post_id ) ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'status', 'all' ); + $request->set_param( 'per_page', 100 ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + + wp_delete_post( $author_post_id, true ); + wp_delete_post( $editor_post_id, true ); + } + + /** + * @dataProvider data_note_get_items_permissions_data_provider + * @ticket 64096 + */ + public function test_note_get_item_permissions_edit_context( $role, $post_author_role, $can_read ) { + wp_set_current_user( self::$user_ids[ $role ] ); + + $post_id = self::factory()->post->create( + array( + 'post_title' => 'Test Post for Block Comments', + 'post_content' => 'This is a test post to check block comment permissions.', + 'post_status' => 'contributor' === $post_author_role ? 'draft' : 'publish', + 'post_author' => self::$user_ids[ $post_author_role ], + ) + ); + + $comment_id = self::factory()->comment->create( + array( + 'comment_post_ID' => $post_id, + 'comment_type' => 'note', + // Test with unapproved comment, which is more restrictive. + 'comment_approved' => 0, + 'user_id' => self::$user_ids[ $post_author_role ], + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments/' . $comment_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + if ( $can_read ) { + $comment = $response->get_data(); + $this->assertEquals( $comment_id, $comment['id'] ); + } else { + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + wp_delete_post( $post_id, true ); + } + + public function data_note_get_items_permissions_data_provider() { + return array( + 'Administrator can see note on other posts' => array( 'administrator', 'author', true ), + 'Editor can see note on other posts' => array( 'editor', 'contributor', true ), + 'Author cannot see note on other posts' => array( 'author', 'editor', false ), + 'Contributor cannot see note on other posts' => array( 'contributor', 'author', false ), + 'Subscriber cannot see note' => array( 'subscriber', 'author', false ), + 'Author can see note on own post' => array( 'author', 'author', true ), + 'Contributor can see note on own post' => array( 'contributor', 'contributor', true ), + ); + } + + public function data_note_status_provider() { + return array( + 'resolved' => array( 'resolved' ), + 'reopen' => array( 'reopen' ), + ); + } + + /** + * Test children link for note comment type. Based on test_get_comment_with_children_link. + * + * @ticket 64152 + */ + public function test_get_note_with_children_link() { + $parent_comment_id = self::factory()->comment->create( + array( + 'comment_approved' => 1, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$admin_id, + 'comment_type' => 'note', + 'comment_content' => 'Parent note comment', + ) + ); + + self::factory()->comment->create( + array( + 'comment_approved' => 1, + 'comment_parent' => $parent_comment_id, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$admin_id, + 'comment_type' => 'note', + 'comment_content' => 'First child note comment', + ) + ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%s', $parent_comment_id ) ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertArrayHasKey( 'children', $response->get_links() ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'post', self::$post_id ); + $request->set_param( 'type', 'note' ); + $request->set_param( 'context', 'edit' ); + $request->set_param( 'parent', 0 ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + $this->assertArrayHasKey( '_links', $data[0] ); + $this->assertArrayHasKey( 'children', $data[0]['_links'] ); + + $children = $data[0]['_links']['children']; + + // Verify the href attribute contains the expected status and type parameters. + $this->assertStringContainsString( 'status=all', $children[0]['href'] ); + $this->assertStringContainsString( 'type=note', $children[0]['href'] ); + } + + /** + * Test retrieving comments by type as authenticated user. + * + * @dataProvider data_comment_type_provider + * @ticket 44157 + * + * @param string $comment_type The comment type to test. + * @param int $count The number of comments to create. + */ + public function test_get_items_type_arg_authenticated( $comment_type, $count ) { + wp_set_current_user( self::$admin_id ); + + $args = array( + 'comment_approved' => 1, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$author_id, + 'comment_type' => $comment_type, + ); + + // Create comments of the specified type. + for ( $i = 0; $i < $count; $i++ ) { + self::factory()->comment->create( $args ); + } + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'type', $comment_type ); + $request->set_param( 'per_page', self::$per_page ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Comments endpoint is expected to return a 200 status' ); + + $comments = $response->get_data(); + $expected_count = 'comment' === $comment_type ? $count + self::$total_comments : $count; + $this->assertCount( $expected_count, $comments, "comment type '{$comment_type}' is expect to have {$expected_count} comments" ); + + // Next, test getting the individual comments. + foreach ( $comments as $comment ) { + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $comment['id'] ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Individual comment endpoint is expected to return a 200 status' ); + $data = $response->get_data(); + $this->assertSame( $comment_type, $data['type'], "Individual comment is expected to have type '{$comment_type}'" ); + } + } + + /** + * Test retrieving comments by type as unauthenticated user. + * + * @dataProvider data_comment_type_provider + * @ticket 44157 + * + * @param string $comment_type The comment type to test. + * @param int $count The number of comments to create. + */ + public function test_get_items_type_arg_unauthenticated( $comment_type, $count ) { + // First, create comments as admin. + wp_set_current_user( self::$admin_id ); + + $args = array( + 'comment_approved' => 1, + 'comment_post_ID' => self::$post_id, + 'user_id' => self::$author_id, + 'comment_type' => $comment_type, + ); + + $comments = array(); + + for ( $i = 0; $i < $count; $i++ ) { + $comments[] = self::factory()->comment->create( $args ); + } + + // Log out and test as unauthenticated user. + wp_logout(); + + $request = new WP_REST_Request( 'GET', '/wp/v2/comments' ); + $request->set_param( 'type', $comment_type ); + $request->set_param( 'per_page', self::$per_page ); + + $response = rest_get_server()->dispatch( $request ); + + // Only comments can be retrieved from the /comments (multiple) endpoint when unauthenticated. + $expected_status = 'comment' === $comment_type ? 200 : 401; + $this->assertSame( $expected_status, $response->get_status(), 'Comments endpoint did not return the expected status' ); + if ( 'comment' !== $comment_type ) { + $this->assertErrorResponse( 'rest_forbidden_param', $response, 401, 'Comments endpoint did not return the expected error response for forbidden parameters' ); + } + + // Individual comments. + foreach ( $comments as $comment ) { + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/comments/%d', $comment ) ); + $response = rest_get_server()->dispatch( $request ); + + // Individual comments using the /comments/ endpoint can be retrieved by + // unauthenticated users - except for the 'note' type which is restricted. + // See https://core.trac.wordpress.org/ticket/44157. + $this->assertSame( 'note' === $comment_type ? 401 : 200, $response->get_status(), 'Individual comment endpoint did not return the expected status' ); + } + } + + /** + * Data provider for comment type tests. + * + * @return array[] Data provider. + */ + public function data_comment_type_provider() { + return array( + 'comment type' => array( 'comment', 5 ), + 'annotation type' => array( 'annotation', 5 ), + 'discussion type' => array( 'discussion', 9 ), + 'note type' => array( 'note', 3 ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-controller.php b/tests/phpunit/tests/rest-api/rest-controller.php index 270d1f32d191c..33ac4f3c814f7 100644 --- a/tests/phpunit/tests/rest-api/rest-controller.php +++ b/tests/phpunit/tests/rest-api/rest-controller.php @@ -4,13 +4,16 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Controller extends WP_Test_REST_TestCase { + /** + * @var WP_REST_Request + */ + private $request; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { // Load the WP_REST_Test_Controller class if not already loaded. require_once __DIR__ . '/rest-test-controller.php'; @@ -359,7 +362,6 @@ public function test_get_endpoint_args_for_item_schema_arg_properties() { // Ignored properties. $this->assertArrayNotHasKey( 'ignored_prop', $args['someobject'] ); - } /** @@ -386,6 +388,7 @@ public function test_get_fields_for_response( $param, $expected ) { 'somedefault', 'somearray', 'someobject', + '_links', ), $fields ); @@ -421,11 +424,23 @@ public function data_get_fields_for_response() { 'somedefault', 'somearray', 'someobject', + '_links', ), ), ); } + public function test_get_fields_for_response_respects_embed() { + $controller = new WP_REST_Test_Controller(); + $request = new WP_REST_Request( 'GET', '/wp/v2/testroute' ); + + $this->assertNotContains( '_embedded', $controller->get_fields_for_response( $request ) ); + + $request->set_param( '_embed', 1 ); + + $this->assertContains( '_embedded', $controller->get_fields_for_response( $request ) ); + } + public function test_get_fields_for_response_filters_by_context() { $controller = new WP_REST_Test_Controller(); diff --git a/tests/phpunit/tests/rest-api/rest-global-styles-controller.php b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php new file mode 100644 index 0000000000000..08ed7f65f818b --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-global-styles-controller.php @@ -0,0 +1,949 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + + self::$theme_manager_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + + // Add the 'edit_theme_options' capability to the theme manager (subscriber). + $theme_manager_id = get_user_by( 'id', self::$theme_manager_id ); + if ( $theme_manager_id instanceof WP_User ) { + $theme_manager_id->add_cap( 'edit_theme_options' ); + } + + // This creates the global styles for the current theme. + self::$global_styles_id = $factory->post->create( + array( + 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-tt1-blocks', + 'tax_input' => array( + 'wp_theme' => 'tt1-blocks', + ), + ) + ); + + self::$post_id = $factory->post->create(); + } + + /** + * Clean up after our tests run. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$editor_id ); + self::delete_user( self::$subscriber_id ); + self::delete_user( self::$theme_manager_id ); + } + + /* + * This filter callback normalizes the return value from `get_theme_file_uri` + * to guard against changes in test environments. + * The test suite otherwise returns full system dir path, e.g., + * /var/www/tests/phpunit/includes/../data/themedir1/block-theme/assets/sugarloaf-mountain.jpg + */ + public function filter_theme_file_uri( $file ) { + $file_name = substr( strrchr( $file, '/' ), 1 ); + return 'https://example.org/wp-content/themes/example-theme/assets/' . $file_name; + } + + /** + * @covers WP_REST_Global_Styles_Controller::register_routes + * @ticket 54596 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/global-styles/(?P[\/\d+]+)', + $routes, + 'Single global style based on the given ID route does not exist' + ); + $this->assertCount( + 2, + $routes['/wp/v2/global-styles/(?P[\/\d+]+)'], + 'Single global style based on the given ID route does not have exactly two elements' + ); + $this->assertArrayHasKey( + '/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', + $routes, + 'Theme global styles route does not exist' + ); + $this->assertCount( + 1, + $routes['/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)'], + 'Theme global styles route does not have exactly one element' + ); + $this->assertArrayHasKey( + '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)/variations', + $routes, + 'Theme global styles variations route does not exist' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * Tests a GET request to the global styles variations endpoint. + * + * @covers WP_REST_Global_Styles_Controller::get_theme_items + * @ticket 61273 + */ + public function test_get_theme_items() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/block-theme/variations' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $expected = array( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f2f2f2', + ), + ), + ), + ), + ), + ), + ), + 'title' => 'variation-a', + ), + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f1f1f1', + ), + ), + ), + ), + ), + ), + ), + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/sugarloaf-mountain.jpg', + ), + ), + ), + 'title' => 'variation-b', + '_links' => array( + 'curies' => array( + array( + 'name' => 'wp', + 'href' => 'https://api.w.org/{rel}', + 'templated' => true, + ), + ), + 'wp:theme-file' => array( + array( + 'href' => 'https://example.org/wp-content/themes/example-theme/assets/sugarloaf-mountain.jpg', + 'name' => 'file:./assets/sugarloaf-mountain.jpg', + 'target' => 'styles.background.backgroundImage.url', + 'type' => 'image/jpeg', + ), + ), + ), + ), + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'Block theme variation', + 'settings' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'foreground', + 'color' => '#3F67C6', + 'name' => 'Foreground', + ), + ), + ), + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/post-title' => array( + 'typography' => array( + 'fontWeight' => '700', + ), + ), + ), + ), + ), + ); + + wp_recursive_ksort( $data ); + wp_recursive_ksort( $expected ); + + $this->assertSameSets( $expected, $data ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Controller does not implement get_items(). + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + */ + public function test_get_theme_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read_global_styles', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + * @ticket 62042 + */ + public function test_get_theme_item_subscriber_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read_global_styles', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 62042 + */ + public function test_get_theme_item_editor_permission_check() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $response = rest_get_server()->dispatch( $request ); + // Checks that the response has the expected keys. + $data = $response->get_data(); + $links = $response->get_links(); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); + } + + /** + * @covers WP_REST_Global_Styles_Controller_Gutenberg::get_theme_item + * @ticket 62042 + */ + public function test_get_theme_item_theme_options_manager_permission_check() { + wp_set_current_user( self::$theme_manager_id ); + switch_theme( 'emptytheme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/emptytheme' ); + $response = rest_get_server()->dispatch( $request ); + // Checks that the response has the expected keys. + $data = $response->get_data(); + $links = $response->get_links(); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54516 + */ + public function test_get_theme_item_invalid() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/invalid' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_theme_not_found', $response, 404 ); + } + + /** + * @dataProvider data_get_theme_item_invalid_theme_dirname + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54596 + * + * @param string $theme_dirname Theme directory to test. + * @param string $expected Expected error code. + */ + public function test_get_theme_item_invalid_theme_dirname( $theme_dirname, $expected ) { + wp_set_current_user( self::$admin_id ); + switch_theme( $theme_dirname ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/' . $theme_dirname ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected, $response, 404 ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_theme_item_invalid_theme_dirname() { + return array( + '+' => array( + 'theme_dirname' => 'my+theme+', + 'expected' => 'rest_theme_not_found', + ), + ':' => array( + 'theme_dirname' => 'my:theme:', + 'expected' => 'rest_no_route', + ), + '<>' => array( + 'theme_dirname' => 'my', + 'expected' => 'rest_no_route', + ), + '*' => array( + 'theme_dirname' => 'my*theme*', + 'expected' => 'rest_no_route', + ), + '?' => array( + 'theme_dirname' => 'my?theme?', + 'expected' => 'rest_no_route', + ), + '"' => array( + 'theme_dirname' => 'my"theme?"', + 'expected' => 'rest_no_route', + ), + '| (invalid on Windows)' => array( + 'theme_dirname' => 'my|theme|', + 'expected' => 'rest_no_route', + ), + // Themes deep in subdirectories. + '2 subdirectories deep' => array( + 'theme_dirname' => 'subdir/subsubdir/mytheme', + 'expected' => 'rest_no_route', + ), + ); + } + + /** + * @dataProvider data_get_theme_item + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54596 + * + * @param string $theme Theme directory to test. + */ + public function test_get_theme_item( $theme ) { + wp_set_current_user( self::$admin_id ); + switch_theme( $theme ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/' . $theme ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $links = $response->get_links(); + $this->assertArrayHasKey( 'settings', $data, 'Data does not have "settings" key' ); + $this->assertArrayHasKey( 'styles', $data, 'Data does not have "styles" key' ); + $this->assertArrayHasKey( 'self', $links, 'Links do not have a "self" key' ); + $this->assertStringContainsString( '/wp/v2/global-styles/themes/' . $theme, $links['self'][0]['href'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_theme_item() { + return array( + 'alphabetic' => array( 'mytheme' ), + 'alphanumeric' => array( 'mythemev1' ), + 'àáâãäåæç' => array( 'àáâãäåæç' ), + 'space' => array( 'my theme' ), + '-_.' => array( 'my_theme-0.1' ), + '[]' => array( 'my[theme]' ), + '()' => array( 'my(theme)' ), + '{}' => array( 'my{theme}' ), + '&=#@!$,^~%' => array( 'theme &=#@!$,^~%' ), + 'all combined' => array( 'thémé {}&=@!$,^~%[0.1](-_-)' ), + + // Themes in a subdirectory. + 'subdir: alphabetic' => array( 'subdir/mytheme' ), + 'subdir: alphanumeric in theme' => array( 'subdir/mythemev1' ), + 'subdir: alphanumeric in subdir' => array( 'subdirv1/mytheme' ), + 'subdir: alphanumeric in both' => array( 'subdirv1/mythemev1' ), + 'subdir: àáâãäåæç in theme' => array( 'subdir/àáâãäåæç' ), + 'subdir: àáâãäåæç in subdir' => array( 'àáâãäåæç/mythemev1' ), + 'subdir: àáâãäåæç in both' => array( 'àáâãäåæç/àáâãäåæç' ), + 'subdir: space in theme' => array( 'subdir/my theme' ), + 'subdir: space in subdir' => array( 'sub dir/mytheme' ), + 'subdir: space in both' => array( 'sub dir/my theme' ), + 'subdir: -_. in theme' => array( 'subdir/my_theme-0.1' ), + 'subdir: -_. in subdir' => array( 'sub_dir-0.1/mytheme' ), + 'subdir: -_. in both' => array( 'sub_dir-0.1/my_theme-0.1' ), + 'subdir: all combined in theme' => array( 'subdir/thémé {}&=@!$,^~%[0.1](-_-)' ), + 'subdir: all combined in subdir' => array( 'sűbdīr {}&=@!$,^~%[0.1](-_-)/mytheme' ), + 'subdir: all combined in both' => array( 'sűbdīr {}&=@!$,^~%[0.1](-_-)/thémé {}&=@!$,^~%[0.1](-_-)' ), + ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_theme_item + * @ticket 54595 + */ + public function test_get_theme_item_fields() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/themes/tt1-blocks' ); + $request->set_param( '_fields', 'settings' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertArrayHasKey( 'settings', $data ); + $this->assertArrayNotHasKey( 'styles', $data ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_invalid_post() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_no_user_edit() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + * @ticket 54516 + */ + public function test_get_item_permission_check_edit() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $links = $response->get_links(); + + $this->assertEqualSets( + array( + 'id' => self::$global_styles_id, + 'title' => array( + 'raw' => 'Custom Styles', + 'rendered' => 'Custom Styles', + ), + 'settings' => new stdClass(), + 'styles' => new stdClass(), + ), + $data + ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertStringContainsString( '/wp/v2/global-styles/' . self::$global_styles_id, $links['self'][0]['href'] ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_body_params( + array( + 'title' => 'My new global styles title', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'My new global styles title', $data['title']['raw'] ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_no_user() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 401 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_invalid_post() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_global_styles_not_found', $response, 404 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 54516 + */ + public function test_update_item_permission_check() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 57536 + */ + public function test_update_item_valid_styles_css() { + wp_set_current_user( self::$admin_id ); + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_body_params( + array( + 'styles' => array( 'css' => 'body { color: red; }' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'body { color: red; }', $data['styles']['css'] ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 57536 + * @ticket 64418 + */ + public function test_update_item_invalid_styles_css() { + wp_set_current_user( self::$admin_id ); + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_body_params( + array( + 'styles' => array( 'css' => '' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_custom_css_illegal_markup', $response, 400 ); + } + + /** + * Tests the submission of a custom block style variation that was defined + * within a theme style variation and wouldn't be registered at the time + * of saving via the API. + * + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 61312 + * @ticket 61451 + */ + public function test_update_item_with_custom_block_style_variations() { + wp_set_current_user( self::$admin_id ); + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + + /* + * For variations to be resolved they have to have been registered + * via either a theme.json partial or through the WP_Block_Styles_Registry. + */ + register_block_style( + 'core/group', + array( + 'name' => 'fromThemeStyleVariation', + 'label' => 'From Theme Style Variation', + ) + ); + + $group_variations = array( + 'fromThemeStyleVariation' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + ), + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_body_params( + array( + 'styles' => array( + 'variations' => array( + 'fromThemeStyleVariation' => array( + 'blockTypes' => array( 'core/group', 'core/columns' ), + 'color' => array( + 'background' => '#000000', + 'text' => '#ffffff', + ), + ), + ), + 'blocks' => array( + 'core/group' => array( + 'variations' => $group_variations, + ), + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( $group_variations, $data['styles']['blocks']['core/group']['variations'] ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_item_schema + * @ticket 54516 + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 4, $properties, 'Schema properties array does not have exactly 4 elements' ); + $this->assertArrayHasKey( 'id', $properties, 'Schema properties array does not have "id" key' ); + $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array does not have "styles" key' ); + $this->assertArrayHasKey( 'settings', $properties, 'Schema properties array does not have "settings" key' ); + $this->assertArrayHasKey( 'title', $properties, 'Schema properties array does not have "title" key' ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::get_available_actions + */ + public function test_assign_edit_css_action_admin() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $request->set_param( 'context', 'edit' ); + $response = rest_do_request( $request ); + $links = $response->get_links(); + + // Admins can only edit css on single site. + if ( is_multisite() ) { + $this->assertArrayNotHasKey( 'https://api.w.org/action-edit-css', $links ); + } else { + $this->assertArrayHasKey( 'https://api.w.org/action-edit-css', $links ); + } + } + + /** + * Test that the route accepts integer IDs. + * + * @ticket 61911 + */ + public function test_global_styles_route_accepts_integer_id() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertIsInt( $data['id'] ); + $this->assertSame( self::$global_styles_id, $data['id'] ); + } + + /** + * Test that the schema defines ID as an integer. + * + * @ticket 61911 + */ + public function test_global_styles_schema_id_type() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + $schema = $data['schema']; + + $this->assertArrayHasKey( 'properties', $schema ); + $this->assertArrayHasKey( 'id', $schema['properties'] ); + $this->assertArrayHasKey( 'type', $schema['properties']['id'] ); + $this->assertSame( 'integer', $schema['properties']['id']['type'] ); + } + + /** + * Test that the route argument schema defines ID as an integer. + * + * @ticket 61911 + */ + public function test_global_styles_route_args_schema() { + $routes = rest_get_server()->get_routes(); + $route_data = $routes['/wp/v2/global-styles/(?P[\/\d+]+)']; + + $this->assertArrayHasKey( 'args', $route_data[0] ); + $this->assertArrayHasKey( 'id', $route_data[0]['args'] ); + $this->assertArrayHasKey( 'type', $route_data[0]['args']['id'] ); + $this->assertSame( 'integer', $route_data[0]['args']['id']['type'] ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::update_item + * @ticket 64418 + */ + public function test_update_allows_valid_css_with_more_syntax() { + wp_set_current_user( self::$admin_id ); + if ( is_multisite() ) { + grant_super_admin( self::$admin_id ); + } + $request = new WP_REST_Request( 'PUT', '/wp/v2/global-styles/' . self::$global_styles_id ); + $css = <<<'CSS' +@property --animate { + syntax: ""; + inherits: true; + initial-value: false; +} +h1::before { content: "fun & games"; } +CSS; + $request->set_body_params( + array( + 'styles' => array( 'css' => $css ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( $css, $data['styles']['css'] ); + + // Compare expected API output to WP internal values. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( $css, $response->get_data()['styles']['css'] ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::validate_custom_css + * @ticket 64418 + * + * @dataProvider data_custom_css_allowed + */ + public function test_validate_custom_css_allowed( string $custom_css ) { + $controller = new WP_REST_Global_Styles_Controller(); + $validate = Closure::bind( + function ( $css ) { + return $this->validate_custom_css( $css ); + }, + $controller, + $controller + ); + + $this->assertTrue( $validate( $custom_css ) ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_custom_css_allowed(): array { + return array( + '@property declaration' => array( + '@property --prop { syntax: ""; inherits: true; initial-value: false; }', + ), + 'Different close tag' => array( '' ), + 'Not a style close tag' => array( '/* array( '/* array( '' ), + 'Short content' => array( '/**/' ), + ); + } + + /** + * @covers WP_REST_Global_Styles_Controller::validate_custom_css + * @ticket 64418 + * + * @dataProvider data_custom_css_disallowed + */ + public function test_validate_custom_css( string $custom_css, string $expected_error_message ) { + $controller = new WP_REST_Global_Styles_Controller(); + $validate = Closure::bind( + function ( $css ) { + return $this->validate_custom_css( $css ); + }, + $controller, + $controller + ); + + $result = $validate( $custom_css ); + $this->assertWPError( $result ); + $this->assertSame( $expected_error_message, $result->get_error_message() ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_custom_css_disallowed(): array { + return array( + 'style close tag' => array( 'css……css', 'The CSS must not contain "</style>".' ), + 'style close tag upper case' => array( '', 'The CSS must not contain "</STYLE>".' ), + 'style close tag mixed case' => array( '', 'The CSS must not contain "</sTyLe>".' ), + 'style close tag in comment' => array( '/**/', 'The CSS must not contain "</style>".' ), + 'style close tag (/)' => array( ' array( " array( " array( " array( " array( ' array( '<', 'The CSS must not end in "<".' ), + 'truncated " array( ' array( ' array( ' array( ' array( ' array( 'user->create( + array( + 'role' => 'administrator', + ) + ); + self::$second_admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$author_id = $factory->user->create( + array( + 'role' => 'author', + ) + ); + + wp_set_current_user( self::$admin_id ); + // This creates the global styles for the current theme. + self::$global_styles_id = $factory->post->create( + array( + 'post_content' => '{"version": ' . WP_Theme_JSON::LATEST_SCHEMA . ', "isGlobalStylesUserThemeJSON": true }', + 'post_status' => 'publish', + 'post_title' => __( 'Custom Styles', 'default' ), + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-tt1-blocks-revisions', + 'tax_input' => array( + 'wp_theme' => 'tt1-blocks', + ), + ) + ); + + // Update post to create a new revisions. + $new_styles_post = array( + 'ID' => self::$global_styles_id, + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + 'settings' => array( + 'color' => array( + 'palette' => array( + 'custom' => array( + array( + 'name' => 'Ghost', + 'slug' => 'ghost', + 'color' => 'ghost', + ), + ), + ), + ), + ), + ) + ), + ); + + wp_update_post( $new_styles_post, true ); + + $new_styles_post = array( + 'ID' => self::$global_styles_id, + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'color' => array( + 'background' => 'lemonchiffon', + ), + ), + 'settings' => array( + 'color' => array( + 'palette' => array( + 'custom' => array( + array( + 'name' => 'Gwanda', + 'slug' => 'gwanda', + 'color' => 'gwanda', + ), + ), + ), + ), + ), + ) + ), + ); + + wp_update_post( $new_styles_post, true ); + + $new_styles_post = array( + 'ID' => self::$global_styles_id, + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'color' => array( + 'background' => 'chocolate', + ), + ), + 'settings' => array( + 'color' => array( + 'palette' => array( + 'custom' => array( + array( + 'name' => 'Stacy', + 'slug' => 'stacy', + 'color' => 'stacy', + ), + ), + ), + ), + ), + ) + ), + ); + + wp_update_post( $new_styles_post, true ); + wp_set_current_user( 0 ); + } + + /** + * Removes users after our tests run. + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$second_admin_id ); + self::delete_user( self::$author_id ); + } + + /** + * Sets up before tests. + */ + public function set_up() { + parent::set_up(); + switch_theme( 'tt1-blocks' ); + $revisions = wp_get_post_revisions( self::$global_styles_id ); + $this->total_revisions = count( $revisions ); + + $this->revision_1 = array_pop( $revisions ); + $this->revision_1_id = $this->revision_1->ID; + + $this->revision_2 = array_pop( $revisions ); + $this->revision_2_id = $this->revision_2->ID; + + $this->revision_3 = array_pop( $revisions ); + $this->revision_3_id = $this->revision_3->ID; + } + + /** + * @ticket 58524 + * @ticket 59810 + * + * @covers WP_REST_Global_Styles_Controller::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/global-styles/(?P[\d]+)/revisions', + $routes, + 'Global style revisions based on the given parentID route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', + $routes, + 'Single global style revisions based on the given parentID and revision ID route does not exist.' + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_missing_parent( $method ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * Utility function to check the items in WP_REST_Global_Styles_Controller::get_items + * against the expected values. + * + * @ticket 58524 + */ + protected function check_get_revision_response( $response_revision_item, $revision_expected_item ) { + $this->assertSame( (int) $revision_expected_item->post_author, $response_revision_item['author'], 'Check that the revision item `author` exists.' ); + $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_date ), $response_revision_item['date'], 'Check that the revision item `date` exists.' ); + $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_date_gmt ), $response_revision_item['date_gmt'], 'Check that the revision item `date_gmt` exists.' ); + $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_modified ), $response_revision_item['modified'], 'Check that the revision item `modified` exists.' ); + $this->assertSame( mysql_to_rfc3339( $revision_expected_item->post_modified_gmt ), $response_revision_item['modified_gmt'], 'Check that the revision item `modified_gmt` exists.' ); + $this->assertSame( $revision_expected_item->post_parent, $response_revision_item['parent'], 'Check that an id for the parent exists.' ); + + // Global styles. + $config = ( new WP_Theme_JSON( json_decode( $revision_expected_item->post_content, true ), 'custom' ) )->get_raw_data(); + $this->assertSame( + $config['settings'], + $response_revision_item['settings'], + 'Check that the revision settings exist in the response.' + ); + $this->assertSame( + $config['styles'], + $response_revision_item['styles'], + 'Check that the revision styles match the updated styles.' + ); + } + + /** + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertCount( $this->total_revisions, $data, 'Check that correct number of revisions exists.' ); + + // Reverse chronology. + $this->assertSame( $this->revision_3_id, $data[0]['id'] ); + $this->check_get_revision_response( $data[0], $this->revision_3 ); + + $this->assertSame( $this->revision_2_id, $data[1]['id'] ); + $this->check_get_revision_response( $data[1], $this->revision_2 ); + + $this->assertSame( $this->revision_1_id, $data[2]['id'] ); + $this->check_get_revision_response( $data[2], $this->revision_1 ); + } + + /** + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::prepare_item_for_response + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', sprintf( $path, self::$global_styles_id, $this->revision_1_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/global-styles/%d/revisions/%d' ), + 'get_items request' => array( '/wp/v2/global-styles/%d/revisions' ), + ); + } + + /** + * @ticket 59810 + * + * @covers WP_REST_Global_Styles_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/' . $this->revision_1_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->check_get_revision_response( $data, $this->revision_1 ); + } + + /** + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_item + * @covers WP_REST_Global_Styles_Controller::prepare_item_for_response + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/' . $this->revision_1_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 59810 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_revision + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_revision_id_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $expected_error = 'rest_post_invalid_id'; + $expected_status = 404; + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions/20000001' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_eligible_roles() { + wp_set_current_user( self::$second_admin_id ); + $config = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'color' => array( + 'background' => 'whitesmoke', + ), + ), + 'settings' => array(), + ); + $updated_styles_post = array( + 'ID' => self::$global_styles_id, + 'post_content' => wp_json_encode( $config ), + ); + + wp_update_post( $updated_styles_post, true ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( $this->total_revisions + 1, $data, 'Check that extra revision exist' ); + $this->assertSame( self::$second_admin_id, $data[0]['author'], 'Check that second author id returns expected value.' ); + } + + /** + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items with context arg. + */ + public function test_get_item_embed_context() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'context', 'embed' ); + $response = rest_get_server()->dispatch( $request ); + $fields = array( + 'author', + 'date', + 'id', + 'parent', + ); + $data = $response->get_data(); + $this->assertSameSets( $fields, array_keys( $data[0] ) ); + } + + /** + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertCount( 9, $properties, 'Schema properties array has exactly 9 elements.' ); + $this->assertArrayHasKey( 'id', $properties, 'Schema properties array has "id" key.' ); + $this->assertArrayHasKey( 'styles', $properties, 'Schema properties array has "styles" key.' ); + $this->assertArrayHasKey( 'settings', $properties, 'Schema properties array has "settings" key.' ); + $this->assertArrayHasKey( 'parent', $properties, 'Schema properties array has "parent" key.' ); + $this->assertArrayHasKey( 'author', $properties, 'Schema properties array has "author" key.' ); + $this->assertArrayHasKey( 'date', $properties, 'Schema properties array has "date" key.' ); + $this->assertArrayHasKey( 'date_gmt', $properties, 'Schema properties array has "date_gmt" key.' ); + $this->assertArrayHasKey( 'modified', $properties, 'Schema properties array has "modified" key.' ); + $this->assertArrayHasKey( 'modified_gmt', $properties, 'Schema properties array has "modified_gmt" key.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 60131 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_item_permissions_check + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_permissions_check( $method ) { + wp_set_current_user( self::$author_id ); + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); + } + + /** + * Tests the pagination header of the first page. + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_pagination_header_of_the_first_page( $method ) { + wp_set_current_user( self::$admin_id ); + + $rest_route = '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions'; + $per_page = 2; + $total_pages = (int) ceil( $this->total_revisions / $per_page ); + $page = 1; // First page. + + $request = new WP_REST_Request( $method, $rest_route ); + $request->set_query_params( + array( + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $headers = $response->get_headers(); + $this->assertSame( $this->total_revisions, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $next_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => $page + 1, + ), + rest_url( $rest_route ) + ); + $this->assertStringNotContainsString( 'rel="prev"', $headers['Link'] ); + $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); + } + + /** + * Tests the pagination header of the last page. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_pagination_header_of_the_last_page + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_pagination_header_of_the_last_page( $method ) { + wp_set_current_user( self::$admin_id ); + + $rest_route = '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions'; + $per_page = 2; + $total_pages = (int) ceil( $this->total_revisions / $per_page ); + $page = 2; // Last page. + + $request = new WP_REST_Request( $method, $rest_route ); + $request->set_query_params( + array( + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $headers = $response->get_headers(); + $this->assertSame( $this->total_revisions, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $prev_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => $page - 1, + ), + rest_url( $rest_route ) + ); + $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); + } + + /** + * Tests that invalid 'per_page' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_per_page_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_per_page_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = -1; // Invalid number. + $expected_error = 'rest_invalid_param'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'per_page', $per_page ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that out of bounds 'page' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_out_of_bounds_page_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_out_of_bounds_page_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $total_pages = (int) ceil( $this->total_revisions / $per_page ); + $page = $total_pages + 1; // Out of bound page. + $expected_error = 'rest_revision_invalid_page_number'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that impossibly high 'page' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_max_pages_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_max_pages_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $page = REST_TESTS_IMPOSSIBLY_HIGH_NUMBER; // Invalid number. + $expected_error = 'rest_revision_invalid_page_number'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that the default query should fetch all revisions. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_default_query_should_fetch_all_revisions + * + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_default_query_should_fetch_all_revisions() { + wp_set_current_user( self::$admin_id ); + + $expected_count = $this->total_revisions; + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected_count, $response->get_data() ); + } + + /** + * Tests that 'offset' query shouldn't work without 'per_page' (fallback -1). + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_offset_should_not_work_without_per_page + * + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_offset_should_not_work_without_per_page() { + wp_set_current_user( self::$admin_id ); + + $offset = 1; + $expected_count = $this->total_revisions; + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'offset', $offset ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected_count, $response->get_data() ); + } + + /** + * Tests that 'offset' query should work with 'per_page'. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_offset_should_work_with_per_page + * + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_offset_should_work_with_per_page() { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = 1; + $expected_count = 2; + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected_count, $response->get_data() ); + } + + /** + * Tests that 'offset' query should take priority over 'page'. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_offset_should_take_priority_over_page + * + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_offset_should_take_priority_over_page() { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = 1; + $page = 1; + $expected_count = 2; + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected_count, $response->get_data() ); + } + + /** + * Tests that 'offset' query, as the total revisions count, should return empty data. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_total_revisions_offset_should_return_empty_data + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_total_revisions_offset_should_return_empty_data( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = $this->total_revisions; + $expected_error = 'rest_revision_invalid_offset_number'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that out of bound 'offset' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_out_of_bound_offset_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_out_of_bound_offset_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = $this->total_revisions + 1; + $expected_error = 'rest_revision_invalid_offset_number'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that impossible high number for 'offset' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_impossible_high_number_offset_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_impossible_high_number_offset_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = REST_TESTS_IMPOSSIBLY_HIGH_NUMBER; + $expected_error = 'rest_revision_invalid_offset_number'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that invalid 'offset' query should error. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_invalid_offset_should_error + * + * @dataProvider data_readable_http_methods + * @ticket 58524 + * @ticket 56481 + * + * @covers WP_REST_Global_Styles_Controller::get_items + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_offset_should_error( $method ) { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $offset = 'moreplease'; + $expected_error = 'rest_invalid_param'; + $expected_status = 400; + + $request = new WP_REST_Request( $method, '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => $offset, + 'per_page' => $per_page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( $expected_error, $response, $expected_status ); + } + + /** + * Tests that out of bounds 'page' query should not error when offset is provided, + * because it takes precedence. + * + * Duplicate of WP_Test_REST_Revisions_Controller::test_get_items_out_of_bounds_page_should_not_error_if_offset + * + * @ticket 58524 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_items_out_of_bounds_page_should_not_error_if_offset() { + wp_set_current_user( self::$admin_id ); + + $per_page = 2; + $total_pages = (int) ceil( $this->total_revisions / $per_page ); + $page = $total_pages + 1; // Out of bound page. + $expected_count = 2; + + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_query_params( + array( + 'offset' => 1, + 'per_page' => $per_page, + 'page' => $page, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected_count, $response->get_data() ); + } + + + /** + * Tests for the pagination. + * + * @ticket 62292 + * + * @covers WP_REST_Global_Styles_Controller::get_items + */ + public function test_get_global_styles_revisions_pagination() { + wp_set_current_user( self::$admin_id ); + + // Test offset. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'offset', 1 ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( 3, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 3, $response->get_headers()['X-WP-TotalPages'] ); + + // Test paged. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'page', 2 ); + $request->set_param( 'per_page', 2 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( 3, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 2, $response->get_headers()['X-WP-TotalPages'] ); + + // Test out of bounds. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . self::$global_styles_id . '/revisions' ); + $request->set_param( 'page', 4 ); + $request->set_param( 'per_page', 6 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_revision_invalid_page_number', $response, 400 ); + } + + /** + * Tests that block style variations in revisions are preserved. + * + * @ticket 64292 + * + * @covers WP_REST_Global_Styles_Revisions_Controller::prepare_item_for_response + */ + public function test_get_item_preserves_block_style_variations() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme-child-with-block-style-variations' ); + + // Create a global styles post for the theme. + $global_styles_id = wp_insert_post( + array( + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + ) + ), + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-block-theme-child-with-block-style-variations', + 'tax_input' => array( + 'wp_theme' => 'block-theme-child-with-block-style-variations', + ), + ), + true + ); + + // Update with block style variations to create a revision. + $config_with_variations = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => '#123456', + 'text' => '#abcdef', + ), + ), + ), + ), + ), + ), + ); + + wp_update_post( + array( + 'ID' => $global_styles_id, + 'post_content' => wp_json_encode( $config_with_variations ), + ), + true + ); + + // Get the revision. + $revisions = wp_get_post_revisions( $global_styles_id ); + $revision = array_shift( $revisions ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . $global_styles_id . '/revisions/' . $revision->ID ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status should be 200.' ); + $this->assertArrayHasKey( 'styles', $data, 'Response should contain styles.' ); + $this->assertArrayHasKey( 'blocks', $data['styles'], 'Styles should contain blocks.' ); + $this->assertArrayHasKey( 'core/group', $data['styles']['blocks'], 'Blocks should contain core/group.' ); + $this->assertArrayHasKey( 'variations', $data['styles']['blocks']['core/group'], 'core/group should contain variations.' ); + $this->assertArrayHasKey( + 'block-style-variation-a', + $data['styles']['blocks']['core/group']['variations'], + 'Variations should contain block-style-variation-a.' + ); + + // Verify the variation styles are preserved. + $variation = $data['styles']['blocks']['core/group']['variations']['block-style-variation-a']; + $this->assertSame( '#123456', $variation['color']['background'], 'Variation background color should be preserved.' ); + $this->assertSame( '#abcdef', $variation['color']['text'], 'Variation text color should be preserved.' ); + + // Clean up. + wp_delete_post( $global_styles_id, true ); + } + + /** + * Tests that multiple block style variations are preserved. + * + * @ticket 64292 + * + * @covers WP_REST_Global_Styles_Revisions_Controller::prepare_item_for_response + */ + public function test_multiple_block_variations_are_preserved() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme-child-with-block-style-variations' ); + + // Create a global styles post for the theme. + $global_styles_id = wp_insert_post( + array( + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + ) + ), + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-multiple-variations', + 'tax_input' => array( + 'wp_theme' => 'block-theme-child-with-block-style-variations', + ), + ), + true + ); + + // Update with multiple block style variations to create a revision. + $config_with_variations = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'red', + ), + ), + ), + ), + 'core/columns' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'blue', + ), + ), + ), + ), + ), + ), + ); + + wp_update_post( + array( + 'ID' => $global_styles_id, + 'post_content' => wp_json_encode( $config_with_variations ), + ), + true + ); + + // Get the revision. + $revisions = wp_get_post_revisions( $global_styles_id ); + $revision = array_shift( $revisions ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . $global_styles_id . '/revisions/' . $revision->ID ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status should be 200.' ); + + // Verify both blocks have their variations preserved. + $this->assertArrayHasKey( + 'block-style-variation-a', + $data['styles']['blocks']['core/group']['variations'], + 'core/group should have block-style-variation-a.' + ); + $this->assertArrayHasKey( + 'block-style-variation-a', + $data['styles']['blocks']['core/columns']['variations'], + 'core/columns should have block-style-variation-a.' + ); + + // Verify the styles are different for each block. + $this->assertSame( + 'red', + $data['styles']['blocks']['core/group']['variations']['block-style-variation-a']['color']['background'], + 'core/group variation should have red background.' + ); + $this->assertSame( + 'blue', + $data['styles']['blocks']['core/columns']['variations']['block-style-variation-a']['color']['background'], + 'core/columns variation should have blue background.' + ); + + // Clean up. + wp_delete_post( $global_styles_id, true ); + } + + /** + * Tests that theme-defined block style variations are registered for revisions. + * + * @ticket 64292 + * + * @covers WP_REST_Global_Styles_Revisions_Controller::prepare_item_for_response + */ + public function test_theme_variations_are_registered_for_revisions() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme-child-with-block-style-variations' ); + + // Verify the theme has a block style variation defined. + $theme_variations = WP_Theme_JSON_Resolver::get_style_variations( 'block' ); + $this->assertNotEmpty( $theme_variations, 'Theme should have block style variations defined.' ); + + // Create a global styles post for the theme. + $global_styles_id = wp_insert_post( + array( + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + ) + ), + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-theme-variations', + 'tax_input' => array( + 'wp_theme' => 'block-theme-child-with-block-style-variations', + ), + ), + true + ); + + // Update with the theme's block style variation. + $config_with_theme_variation = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'purple', + ), + ), + ), + ), + ), + ), + ); + + wp_update_post( + array( + 'ID' => $global_styles_id, + 'post_content' => wp_json_encode( $config_with_theme_variation ), + ), + true + ); + + // Get the revision. + $revisions = wp_get_post_revisions( $global_styles_id ); + $revision = array_shift( $revisions ); + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . $global_styles_id . '/revisions/' . $revision->ID ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status should be 200.' ); + + // Verify the theme variation is preserved in the revision. + $this->assertArrayHasKey( + 'block-style-variation-a', + $data['styles']['blocks']['core/group']['variations'], + 'Theme-defined variation should be preserved in revision.' + ); + + // Clean up. + wp_delete_post( $global_styles_id, true ); + } + + /** + * Tests that block style variations are preserved in the revisions collection endpoint. + * + * @ticket 64292 + * + * @covers WP_REST_Global_Styles_Revisions_Controller::get_items + * @covers WP_REST_Global_Styles_Revisions_Controller::prepare_item_for_response + */ + public function test_get_items_preserves_block_style_variations() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme-child-with-block-style-variations' ); + + // Create a global styles post for the theme. + $global_styles_id = wp_insert_post( + array( + 'post_content' => wp_json_encode( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + ) + ), + 'post_status' => 'publish', + 'post_title' => 'Custom Styles', + 'post_type' => 'wp_global_styles', + 'post_name' => 'wp-global-styles-variations-collection', + 'tax_input' => array( + 'wp_theme' => 'block-theme-child-with-block-style-variations', + ), + ), + true + ); + + // Create first revision with variations. + $config_variation_1 = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'green', + ), + ), + ), + ), + ), + ), + ); + + wp_update_post( + array( + 'ID' => $global_styles_id, + 'post_content' => wp_json_encode( $config_variation_1 ), + ), + true + ); + + // Create second revision with different variation styles. + $config_variation_2 = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'isGlobalStylesUserThemeJSON' => true, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'orange', + ), + ), + ), + ), + ), + ), + ); + + wp_update_post( + array( + 'ID' => $global_styles_id, + 'post_content' => wp_json_encode( $config_variation_2 ), + ), + true + ); + + // Get all revisions. + $request = new WP_REST_Request( 'GET', '/wp/v2/global-styles/' . $global_styles_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Response status should be 200.' ); + $this->assertCount( 2, $data, 'Should have 2 revisions.' ); + + // Verify first revision (most recent - orange). + $this->assertArrayHasKey( + 'block-style-variation-a', + $data[0]['styles']['blocks']['core/group']['variations'], + 'First revision should have block-style-variation-a.' + ); + $this->assertSame( + 'orange', + $data[0]['styles']['blocks']['core/group']['variations']['block-style-variation-a']['color']['background'], + 'First revision should have orange background.' + ); + + // Verify second revision (older - green). + $this->assertArrayHasKey( + 'block-style-variation-a', + $data[1]['styles']['blocks']['core/group']['variations'], + 'Second revision should have block-style-variation-a.' + ); + $this->assertSame( + 'green', + $data[1]['styles']['blocks']['core/group']['variations']['block-style-variation-a']['color']['background'], + 'Second revision should have green background.' + ); + + // Clean up. + wp_delete_post( $global_styles_id, true ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not implement get_context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } +} diff --git a/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php b/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php new file mode 100644 index 0000000000000..3be0bba59f26f --- /dev/null +++ b/tests/phpunit/tests/rest-api/rest-navigation-fallback-controller.php @@ -0,0 +1,234 @@ +user->create( array( 'role' => 'administrator' ) ); + + self::$editor_user = $factory->user->create( array( 'role' => 'editor' ) ); + } + + public function set_up() { + parent::set_up(); + + wp_set_current_user( self::$admin_user ); + } + + /** + * @ticket 58557 + * @covers WP_REST_Navigation_Fallback_Controller::register_routes + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wp-block-editor/v1/navigation-fallback', $routes, 'Fallback route should be registered.' ); + } + + /** + * @ticket 58557 + * @covers WP_REST_Navigation_Fallback_Controller + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_should_not_return_menus_for_users_without_permissions() { + + wp_set_current_user( self::$editor_user ); + + $request = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 403, $response->get_status(), 'Response should indicate user does not have permission.' ); + + $this->assertSame( 'rest_cannot_create', $data['code'], 'Response should indicate user cannot create.' ); + + $this->assertSame( 'Sorry, you are not allowed to create Navigation Menus as this user.', $data['message'], 'Response should indicate failed request status.' ); + } + + /** + * @ticket 58557 + * @covers WP_REST_Navigation_Fallback_Controller + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_get_item() { + + $request = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Status should indicate successful request.' ); + + $this->assertIsArray( $data, 'Response should be of correct type.' ); + + $this->assertArrayHasKey( 'id', $data, 'Response should contain expected fields.' ); + + $this->assertSame( 'wp_navigation', get_post_type( $data['id'] ), '"id" field should represent a post of type "wp_navigation"' ); + + // Check that only a single Navigation fallback was created. + $navs_in_db = $this->get_navigations_in_database(); + + $this->assertCount( 1, $navs_in_db, 'Only a single Navigation menu should be present in the database.' ); + } + + /** + * @ticket 58557 + * @covers WP_REST_Navigation_Fallback_Controller + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp-block-editor/v1/navigation-fallback' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( 200, $response->get_status(), 'Status should indicate successful request.' ); + + $this->assertArrayHasKey( 'schema', $data, '"schema" key should exist in response.' ); + + $schema = $data['schema']; + + $this->assertSame( 'object', $schema['type'], 'The schema type should match the expected type.' ); + + $this->assertArrayHasKey( 'id', $schema['properties'], 'Schema should have an "id" property.' ); + $this->assertSame( 'integer', $schema['properties']['id']['type'], 'Schema "id" property should be an integer.' ); + $this->assertTrue( $schema['properties']['id']['readonly'], 'Schema "id" property should be readonly.' ); + } + + /** + * @ticket 58557 + * @covers WP_REST_Navigation_Fallback_Controller + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_adds_links() { + $request = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $navigation_post_id = $data['id']; + + $links = $response->get_links(); + + $this->assertNotEmpty( $links, 'Response should contain links.' ); + + $this->assertArrayHasKey( 'self', $links, 'Response should contain a "self" link.' ); + + $this->assertStringContainsString( 'wp/v2/navigation/' . $navigation_post_id, $links['self'][0]['href'], 'Self link should reference the correct Navigation Menu post resource url.' ); + + $this->assertTrue( $links['self'][0]['attributes']['embeddable'], 'Self link should be embeddable.' ); + } + + /** + * Tests that the correct filters are applied to the context parameter. + * + * By default, the REST response for the Posts Controller will not return all fields + * when the context is set to 'embed'. Assert that correct additional fields are added + * to the embedded Navigation Post, when the navigation fallback endpoint + * is called with the `_embed` param. + * + * @ticket 58557 + * + * @covers WP_Navigation_Fallback::update_wp_navigation_post_schema + * + * @since 6.3.0 Added Navigation Fallbacks endpoint. + */ + public function test_embedded_navigation_post_contains_required_fields() { + // First we'll use the navigation fallback to get a link to the navigation endpoint. + $request = new WP_REST_Request( 'GET', '/wp-block-editor/v1/navigation-fallback' ); + $response = rest_get_server()->dispatch( $request ); + $data = rest_get_server()->response_to_data( $response, true ); + $embedded = $data['_embedded']['self'][0]; + + // Verify that the additional status field is present. + $this->assertArrayHasKey( 'status', $embedded, 'Response title should contain a "status" field.' ); + + // Verify that the additional content fields are present. + $this->assertArrayHasKey( 'content', $embedded, 'Response should contain a "content" field.' ); + $this->assertArrayHasKey( 'raw', $embedded['content'], 'Response content should contain a "raw" field.' ); + $this->assertArrayHasKey( 'rendered', $embedded['content'], 'Response content should contain a "rendered" field.' ); + $this->assertArrayHasKey( 'block_version', $embedded['content'], 'Response should contain a "block_version" field.' ); + + // Verify that the additional title.raw field is present. + $this->assertArrayHasKey( 'raw', $embedded['title'], 'Response title should contain a "raw" key.' ); + } + + private function get_navigations_in_database() { + $navs_in_db = new WP_Query( + array( + 'post_type' => 'wp_navigation', + 'post_status' => 'publish', + 'posts_per_page' => -1, + 'orderby' => 'date', + 'order' => 'DESC', + ) + ); + + return $navs_in_db->posts ? $navs_in_db->posts : array(); + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Covered by the core test. + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Covered by the core test. + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Covered by the core test. + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } +} diff --git a/tests/phpunit/tests/rest-api/rest-pages-controller.php b/tests/phpunit/tests/rest-api/rest-pages-controller.php index 00047cce80e08..9717a7fcda1c6 100644 --- a/tests/phpunit/tests/rest-api/rest-pages-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pages-controller.php @@ -5,9 +5,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Pages_Controller extends WP_Test_REST_Post_Type_Controller_Testcase { @@ -27,7 +25,6 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); - $this->has_setup_template = false; add_filter( 'theme_page_templates', array( $this, 'filter_theme_page_templates' ) ); // Re-register the route as we now have a template available. $GLOBALS['wp_rest_server']->override_by_default = true; @@ -52,7 +49,7 @@ public function test_context_param() { $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. - $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) ); + $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages/' . $page_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -64,7 +61,8 @@ public function test_registered_query_params() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/pages' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $keys = array_keys( $data['endpoints'][0]['args'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $keys = array_keys( $data['endpoints'][0]['args'] ); sort( $keys ); $this->assertSame( array( @@ -86,6 +84,8 @@ public function test_registered_query_params() { 'parent_exclude', 'per_page', 'search', + 'search_columns', + 'search_semantics', 'slug', 'status', ), @@ -94,13 +94,13 @@ public function test_registered_query_params() { } public function test_get_items() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'draft', 'post_type' => 'page', @@ -114,13 +114,13 @@ public function test_get_items() { } public function test_get_items_parent_query() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -148,26 +148,26 @@ public function test_get_items_parent_query() { } public function test_get_items_parents_query() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'post_parent' => $id1, ) ); - $id3 = $this->factory->post->create( + $id3 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id4 = $this->factory->post->create( + $id4 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -190,13 +190,13 @@ public function test_get_items_parents_query() { } public function test_get_items_parent_exclude_query() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -224,27 +224,27 @@ public function test_get_items_parent_exclude_query() { } public function test_get_items_menu_order_query() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 2, ) ); - $id3 = $this->factory->post->create( + $id3 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', 'menu_order' => 3, ) ); - $id4 = $this->factory->post->create( + $id4 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -302,13 +302,13 @@ public function test_get_items_min_max_pages_query() { public function test_get_items_private_filter_query_var() { // Private query vars inaccessible to unauthorized users. wp_set_current_user( 0 ); - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $draft_id = $this->factory->post->create( + $draft_id = self::factory()->post->create( array( 'post_status' => 'draft', 'post_type' => 'page', @@ -329,26 +329,26 @@ public function test_get_items_private_filter_query_var() { public function test_get_items_invalid_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/pages' ); - $request->set_param( 'after', rand_str() ); - $request->set_param( 'before', rand_str() ); + $request->set_param( 'after', 'foo' ); + $request->set_param( 'before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function test_get_items_valid_date() { - $post1 = $this->factory->post->create( + $post1 = self::factory()->post->create( array( 'post_date' => '2016-01-15T00:00:00Z', 'post_type' => 'page', ) ); - $post2 = $this->factory->post->create( + $post2 = self::factory()->post->create( array( 'post_date' => '2016-01-16T00:00:00Z', 'post_type' => 'page', ) ); - $post3 = $this->factory->post->create( + $post3 = self::factory()->post->create( array( 'post_date' => '2016-01-17T00:00:00Z', 'post_type' => 'page', @@ -368,8 +368,8 @@ public function test_get_items_valid_date() { */ public function test_get_items_invalid_modified_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/pages' ); - $request->set_param( 'modified_after', rand_str() ); - $request->set_param( 'modified_before', rand_str() ); + $request->set_param( 'modified_after', 'foo' ); + $request->set_param( 'modified_before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } @@ -378,19 +378,19 @@ public function test_get_items_invalid_modified_date() { * @ticket 50617 */ public function test_get_items_valid_modified_date() { - $post1 = $this->factory->post->create( + $post1 = self::factory()->post->create( array( 'post_date' => '2016-01-01 00:00:00', 'post_type' => 'page', ) ); - $post2 = $this->factory->post->create( + $post2 = self::factory()->post->create( array( 'post_date' => '2016-01-02 00:00:00', 'post_type' => 'page', ) ); - $post3 = $this->factory->post->create( + $post3 = self::factory()->post->create( array( 'post_date' => '2016-01-03 00:00:00', 'post_type' => 'page', @@ -408,19 +408,25 @@ public function test_get_items_valid_modified_date() { $this->assertSame( $post2, $data[0]['id'] ); } + /** + * @doesNotPerformAssertions + */ public function test_get_item() { - + // Controller does not implement get_item(). } public function test_get_item_invalid_post_type() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/pages/' . $post_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 404, $response->get_status() ); } + /** + * @doesNotPerformAssertions + */ public function test_create_item() { - + // Controller does not implement create_item(). } public function test_create_item_with_template() { @@ -442,7 +448,7 @@ public function test_create_item_with_template() { } public function test_create_page_with_parent() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'type' => 'page', ) @@ -484,12 +490,15 @@ public function test_create_page_with_invalid_parent() { $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 ); } + /** + * @doesNotPerformAssertions + */ public function test_update_item() { - + // Controller does not implement update_item(). } public function test_delete_item() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_title' => 'Deleted page', @@ -507,13 +516,16 @@ public function test_delete_item() { $this->assertSame( 'trash', $data['status'] ); } + /** + * @doesNotPerformAssertions + */ public function test_prepare_item() { - + // Controller does not implement prepare_item(). } public function test_prepare_item_limit_fields() { wp_set_current_user( self::$editor_id ); - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -535,7 +547,7 @@ public function test_prepare_item_limit_fields() { } public function test_get_pages_params() { - $this->factory->post->create_many( + self::factory()->post->create_many( 8, array( 'post_type' => 'page', @@ -566,7 +578,7 @@ public function test_get_pages_params() { public function test_update_page_menu_order() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', ) @@ -589,7 +601,7 @@ public function test_update_page_menu_order() { public function test_update_page_menu_order_to_zero() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'menu_order' => 1, @@ -612,12 +624,12 @@ public function test_update_page_menu_order_to_zero() { } public function test_update_page_parent_non_zero() { - $page_id1 = $this->factory->post->create( + $page_id1 = self::factory()->post->create( array( 'post_type' => 'page', ) ); - $page_id2 = $this->factory->post->create( + $page_id2 = self::factory()->post->create( array( 'post_type' => 'page', ) @@ -635,12 +647,12 @@ public function test_update_page_parent_non_zero() { } public function test_update_page_parent_zero() { - $page_id1 = $this->factory->post->create( + $page_id1 = self::factory()->post->create( array( 'post_type' => 'page', ) ); - $page_id2 = $this->factory->post->create( + $page_id2 = self::factory()->post->create( array( 'post_type' => 'page', 'post_parent' => $page_id1, @@ -659,7 +671,7 @@ public function test_update_page_parent_zero() { } public function test_get_page_with_password() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_password' => '$inthebananastand', @@ -677,7 +689,7 @@ public function test_get_page_with_password() { } public function test_get_page_with_password_using_password() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_password' => '$inthebananastand', @@ -699,7 +711,7 @@ public function test_get_page_with_password_using_password() { } public function test_get_page_with_password_using_incorrect_password() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_password' => '$inthebananastand', @@ -715,7 +727,7 @@ public function test_get_page_with_password_using_incorrect_password() { } public function test_get_page_with_password_without_permission() { - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_password' => '$inthebananastand', @@ -737,7 +749,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 24, $properties ); + $this->assertCount( 25, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'comment_status', $properties ); $this->assertArrayHasKey( 'content', $properties ); @@ -762,6 +774,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'template', $properties ); $this->assertArrayHasKey( 'title', $properties ); $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } public function filter_theme_page_templates( $page_templates ) { @@ -776,5 +789,4 @@ protected function set_post_data( $args = array() ) { $args['type'] = 'page'; return $args; } - } diff --git a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php index 4a12e663b5392..6f84306dad61f 100644 --- a/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php +++ b/tests/phpunit/tests/rest-api/rest-pattern-directory-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi * @group pattern-directory */ @@ -21,6 +19,24 @@ class WP_REST_Pattern_Directory_Controller_Test extends WP_Test_REST_Controller_ */ protected static $contributor_id; + /** + * An instance of WP_REST_Pattern_Directory_Controller class. + * + * @since 6.0.0 + * + * @var WP_REST_Pattern_Directory_Controller + */ + private static $controller; + + /** + * List of URLs captured. + * + * @since 6.2.0 + * + * @var string[] + */ + protected static $http_request_urls; + /** * Set up class test fixtures. * @@ -34,6 +50,29 @@ public static function wpSetUpBeforeClass( $factory ) { 'role' => 'contributor', ) ); + + self::$http_request_urls = array(); + + static::$controller = new WP_REST_Pattern_Directory_Controller(); + } + + /** + * Tear down after class. + * + * @since 6.2.0 + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$contributor_id ); + } + + /** + * Clear the captured request URLs after each test. + * + * @since 6.2.0 + */ + public function tear_down() { + self::$http_request_urls = array(); + parent::tear_down(); } /** @@ -42,8 +81,8 @@ public static function wpSetUpBeforeClass( $factory ) { * @param WP_REST_Response[] $pattern An individual pattern from the REST API response. */ public function assertPatternMatchesSchema( $pattern ) { - $schema = ( new WP_REST_Pattern_Directory_Controller() )->get_item_schema(); - $pattern_id = isset( $pattern->id ) ? $pattern->id : '{pattern ID is missing}'; + $schema = static::$controller->get_item_schema(); + $pattern_id = $pattern->id ?? '{pattern ID is missing}'; $this->assertTrue( rest_validate_value_from_schema( $pattern, $schema ), @@ -79,7 +118,7 @@ public function test_context_param() { $patterns = $response->get_data(); $this->assertSame( 'view', $patterns['endpoints'][0]['args']['context']['default'] ); - $this->assertSame( array( 'view', 'embed' ), $patterns['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $patterns['endpoints'][0]['args']['context']['enum'] ); } /** @@ -100,6 +139,52 @@ public function test_get_items() { $this->assertGreaterThan( 0, count( $patterns ) ); array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); + $this->assertSame( array( 'blog post' ), $patterns[0]['keywords'] ); + $this->assertSame( array( 'header', 'hero' ), $patterns[1]['keywords'] ); + $this->assertSame( array( 'call to action', 'hero section' ), $patterns[2]['keywords'] ); + } + + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_patterns_data() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', true ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/pattern-directory/patterns' ); + + $hook_name = 'rest_prepare_block_pattern'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @ticket 56481 + */ + public function test_get_items_head_request_with_specified_fields_returns_success_response() { + wp_set_current_user( self::$contributor_id ); + self::mock_successful_response( 'browse-all', true ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/pattern-directory/patterns' ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); } /** @@ -146,10 +231,6 @@ public function test_get_items_by_keyword() { $this->assertGreaterThan( 0, count( $patterns ) ); array_walk( $patterns, array( $this, 'assertPatternMatchesSchema' ) ); - - foreach ( $patterns as $pattern ) { - $this->assertContains( 'core', $pattern['keywords'] ); - } } /** @@ -181,27 +262,49 @@ public function test_get_items_search() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_wdotorg_unavailable() { + public function test_get_items_wdotorg_unavailable( $method ) { wp_set_current_user( self::$contributor_id ); self::prevent_requests_to_host( 'api.wordpress.org' ); - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $response = rest_do_request( $request ); $this->assertErrorResponse( 'patterns_api_failed', $response, 500 ); } /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_logged_out() { - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + public function test_get_items_logged_out( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $request->set_query_params( array( 'search' => 'button' ) ); $response = rest_do_request( $request ); @@ -245,15 +348,20 @@ public function test_get_items_search_no_results() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * * @covers WP_REST_Pattern_Directory_Controller::get_items * * @since 5.8.0 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_response_data() { + public function test_get_items_invalid_response_data( $method ) { wp_set_current_user( self::$contributor_id ); self::mock_successful_response( 'invalid-data', true ); - $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + $request = new WP_REST_Request( $method, '/wp/v2/pattern-directory/patterns' ); $response = rest_do_request( $request ); $this->assertSame( 500, $response->status ); @@ -272,7 +380,7 @@ public function test_get_items_prepare_filter() { // Test that filter changes uncached values. add_filter( 'rest_prepare_block_pattern', - static function( $response ) { + static function ( $response ) { return 'initial value'; } ); @@ -286,7 +394,7 @@ static function( $response ) { // Test that filter changes cached values (the previous request primed the cache). add_filter( 'rest_prepare_block_pattern', - static function( $response ) { + static function ( $response ) { return 'modified the cache'; }, 11 @@ -300,20 +408,202 @@ static function( $response ) { $this->assertSame( 'modified the cache', $patterns[0] ); } + /** + * Tests if the provided query args are passed through to the wp.org API. + * + * @since 6.2.0 + * + * @ticket 57501 + * + * @covers WP_REST_Pattern_Directory_Controller::get_items + * + * @dataProvider data_get_items_query_args + * + * @param string $param Query parameter name (ex, page). + * @param mixed $value Query value to test. + * @param bool $is_error Whether this value should error or not. + * @param mixed $expected Expected value (or expected error code). + */ + public function test_get_items_query_args( $param, $value, $is_error, $expected ) { + wp_set_current_user( self::$contributor_id ); + add_filter( 'pre_http_request', array( $this, 'mock_request_to_apiwporg_url' ), 10, 3 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pattern-directory/patterns' ); + if ( $value ) { + $request->set_query_params( array( $param => $value ) ); + } + + $response = rest_do_request( $request ); + $data = $response->get_data(); + if ( $is_error ) { + $this->assertSame( $expected, $data['code'], 'Response error code does not match' ); + $this->assertStringContainsString( $param, $data['message'], 'Response error message does not match' ); + } else { + $this->assertCount( 1, self::$http_request_urls, 'The number of HTTP Request URLs is not 1' ); + $this->assertStringContainsString( $param . '=' . $expected, self::$http_request_urls[0], 'The param and/or value do not match' ); + } + } + + /** + * Data provider. + * + * return array[] + */ + public function data_get_items_query_args() { + return array( + 'per_page default' => array( + 'param' => 'per_page', + 'value' => false, + 'is_error' => false, + 'expected' => 100, + ), + 'per_page custom-1' => array( + 'param' => 'per_page', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'per_page custom-2' => array( + 'param' => 'per_page', + 'value' => 50, + 'is_error' => false, + 'expected' => 50, + ), + 'per_page invalid-1' => array( + 'param' => 'per_page', + 'value' => 200, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'per_page invalid-2' => array( + 'param' => 'per_page', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'page default' => array( + 'param' => 'page', + 'value' => false, + 'is_error' => false, + 'expected' => 1, + ), + 'page custom' => array( + 'param' => 'page', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'page invalid' => array( + 'param' => 'page', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'offset custom' => array( + 'param' => 'offset', + 'value' => 5, + 'is_error' => false, + 'expected' => 5, + ), + 'offset invalid-1' => array( + 'param' => 'offset', + 'value' => 'abc', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'order default' => array( + 'param' => 'order', + 'value' => false, + 'is_error' => false, + 'expected' => 'desc', + ), + 'order custom' => array( + 'param' => 'order', + 'value' => 'asc', + 'is_error' => false, + 'expected' => 'asc', + ), + 'order invalid-1' => array( + 'param' => 'order', + 'value' => 10, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'order invalid-2' => array( + 'param' => 'order', + 'value' => 'fake', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + + 'orderby default' => array( + 'param' => 'orderby', + 'value' => false, + 'is_error' => false, + 'expected' => 'date', + ), + 'orderby custom-1' => array( + 'param' => 'orderby', + 'value' => 'title', + 'is_error' => false, + 'expected' => 'title', + ), + 'orderby custom-2' => array( + 'param' => 'orderby', + 'value' => 'date', + 'is_error' => false, + 'expected' => 'date', + ), + 'orderby custom-3' => array( + 'param' => 'orderby', + 'value' => 'favorite_count', + 'is_error' => false, + 'expected' => 'favorite_count', + ), + 'orderby invalid-1' => array( + 'param' => 'orderby', + 'value' => 10, + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + 'orderby invalid-2' => array( + 'param' => 'orderby', + 'value' => 'fake', + 'is_error' => true, + 'expected' => 'rest_invalid_param', + ), + ); + } + + /** + * @doesNotPerformAssertions + */ public function test_get_item() { - $this->markTestSkipped( 'Controller does not have get_item route.' ); + // Controller does not implement get_item(). } + /** + * @doesNotPerformAssertions + */ public function test_create_item() { - $this->markTestSkipped( 'Controller does not have create_item route.' ); + // Controller does not implement create_item(). } + /** + * @doesNotPerformAssertions + */ public function test_update_item() { - $this->markTestSkipped( 'Controller does not have update_item route.' ); + // Controller does not implement update_item(). } + /** + * @doesNotPerformAssertions + */ public function test_delete_item() { - $this->markTestSkipped( 'Controller does not have delete_item route.' ); + // Controller does not implement delete_item(). } /** @@ -322,12 +612,11 @@ public function test_delete_item() { * @since 5.8.0 */ public function test_prepare_item() { - $controller = new WP_REST_Pattern_Directory_Controller(); $raw_patterns = json_decode( self::get_raw_response( 'browse-all' ) ); $raw_patterns[0]->extra_field = 'this should be removed'; - $prepared_pattern = $controller->prepare_response_for_collection( - $controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) + $prepared_pattern = static::$controller->prepare_response_for_collection( + static::$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) ); $this->assertPatternMatchesSchema( $prepared_pattern ); @@ -340,12 +629,11 @@ public function test_prepare_item() { * @since 5.8.0 */ public function test_prepare_item_search() { - $controller = new WP_REST_Pattern_Directory_Controller(); $raw_patterns = json_decode( self::get_raw_response( 'search' ) ); $raw_patterns[0]->extra_field = 'this should be removed'; - $prepared_pattern = $controller->prepare_response_for_collection( - $controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) + $prepared_pattern = static::$controller->prepare_response_for_collection( + static::$controller->prepare_item_for_response( $raw_patterns[0], new WP_REST_Request() ) ); $this->assertPatternMatchesSchema( $prepared_pattern ); @@ -394,9 +682,102 @@ private static function get_raw_response( $action ) { * @covers WP_REST_Pattern_Directory_Controller::get_item_schema * * @since 5.8.0 + * + * @doesNotPerformAssertions */ public function test_get_item_schema() { - $this->markTestSkipped( "The controller's schema is hardcoded, so tests would not be meaningful." ); + // The controller's schema is hardcoded, so tests would not be meaningful. + } + + /** + * Tests if the transient key gets generated correctly. + * + * @dataProvider data_get_query_parameters + * + * @covers WP_REST_Pattern_Directory_Controller::get_transient_key + * + * @since 6.0.0 + * + * @ticket 55617 + * + * @param array $parameters_1 Expected query arguments. + * @param array $parameters_2 Actual query arguments. + * @param string $message An error message to display. + * @param bool $assert_same Assertion type (assertSame vs assertNotSame). + */ + public function test_transient_keys_get_generated_correctly( $parameters_1, $parameters_2, $message, $assert_same = true ) { + $reflection_method = new ReflectionMethod( static::$controller, 'get_transient_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection_method->setAccessible( true ); + } + + $result_1 = $reflection_method->invoke( self::$controller, $parameters_1 ); + $result_2 = $reflection_method->invoke( self::$controller, $parameters_2 ); + + $this->assertIsString( $result_1, 'Transient key #1 must be a string.' ); + $this->assertNotEmpty( $result_1, 'Transient key #1 must not be empty.' ); + + $this->assertIsString( $result_2, 'Transient key #2 must be a string.' ); + $this->assertNotEmpty( $result_2, 'Transient key #2 must not be empty.' ); + + if ( $assert_same ) { + $this->assertSame( $result_1, $result_2, $message ); + } else { + $this->assertNotSame( $result_1, $result_2, $message ); + } + } + + /** + * @since 6.0.0 + * + * @ticket 55617 + */ + public function data_get_query_parameters() { + return array( + 'same key and empty slugs' => array( + 'parameters_1' => array( + 'parameter_1' => 1, + 'slug' => array(), + ), + 'parameters_2' => array( + 'parameter_1' => 1, + ), + 'message' => 'Empty slugs should not affect the transient key.', + ), + 'same key and slugs in different order' => array( + 'parameters_1' => array( + 'parameter_1' => 1, + 'slug' => array( 0, 2 ), + ), + 'parameters_2' => array( + 'parameter_1' => 1, + 'slug' => array( 2, 0 ), + ), + 'message' => 'The order of slugs should not affect the transient key.', + ), + 'same key and different slugs' => array( + 'parameters_1' => array( + 'parameter_1' => 1, + 'slug' => array( 'some_slug' ), + ), + 'parameters_2' => array( + 'parameter_1' => 1, + 'slug' => array( 'some_other_slug' ), + ), + 'message' => 'Transient keys must not match.', + false, + ), + 'different keys' => array( + 'parameters_1' => array( + 'parameter_1' => 1, + ), + 'parameters_2' => array( + 'parameter_2' => 1, + ), + 'message' => 'Transient keys must depend on array keys.', + false, + ), + ); } /** @@ -410,10 +791,10 @@ public function test_get_item_schema() { private static function mock_successful_response( $action, $expects_results ) { add_filter( 'pre_http_request', - static function ( $preempt, $args, $url ) use ( $action, $expects_results ) { + static function ( $response, $parsed_args, $url ) use ( $action, $expects_results ) { if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) { - return $preempt; + return $response; } $response = array( @@ -444,7 +825,7 @@ static function ( $preempt, $args, $url ) use ( $action, $expects_results ) { private static function prevent_requests_to_host( $blocked_host = 'api.wordpress.org' ) { add_filter( 'pre_http_request', - static function ( $return, $args, $url ) use ( $blocked_host ) { + static function ( $response, $parsed_args, $url ) use ( $blocked_host ) { if ( wp_parse_url( $url, PHP_URL_HOST ) === $blocked_host ) { return new WP_Error( @@ -455,10 +836,39 @@ static function ( $return, $args, $url ) use ( $blocked_host ) { } - return $return; + return $response; }, 10, 3 ); } + + /** + * Mock the request to wp.org URL to capture the URLs. + * + * @since 6.2.0 + * + * @return array faux/mocked response. + */ + public function mock_request_to_apiwporg_url( $response, $args, $url ) { + if ( 'api.wordpress.org' !== wp_parse_url( $url, PHP_URL_HOST ) ) { + return $response; + } + + self::$http_request_urls[] = $url; + + // Return a response to prevent external API request. + $response = array( + 'headers' => array(), + 'response' => array( + 'code' => 200, + 'message' => 'OK', + ), + 'body' => '[]', + 'cookies' => array(), + 'filename' => null, + ); + + return $response; + } } diff --git a/tests/phpunit/tests/rest-api/rest-plugins-controller.php b/tests/phpunit/tests/rest-api/rest-plugins-controller.php index e2a61d605bc5f..d6290b071bf22 100644 --- a/tests/phpunit/tests/rest-api/rest-plugins-controller.php +++ b/tests/phpunit/tests/rest-api/rest-plugins-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_REST_Plugins_Controller_Test extends WP_Test_REST_Controller_Testcase { @@ -42,6 +40,13 @@ class WP_REST_Plugins_Controller_Test extends WP_Test_REST_Controller_Testcase { */ private static $admin; + /** + * JSON decoded response from the WordPress.org plugin API. + * + * @var stdClass + */ + private static $plugin_api_decoded_response; + /** * Set up class test fixtures. * @@ -69,6 +74,8 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { if ( is_multisite() ) { grant_super_admin( self::$super_admin ); } + + self::$plugin_api_decoded_response = json_decode( file_get_contents( DIR_TESTDATA . '/plugins/link-manager.json' ) ); } /** @@ -84,12 +91,23 @@ public static function wpTearDownAfterClass() { public function tear_down() { if ( file_exists( WP_PLUGIN_DIR . '/test-plugin/test-plugin.php' ) ) { + // Remove plugin files. $this->rmdir( WP_PLUGIN_DIR . '/test-plugin' ); + // Delete empty directory. + rmdir( WP_PLUGIN_DIR . '/test-plugin' ); } + if ( file_exists( DIR_TESTDATA . '/link-manager.zip' ) ) { unlink( DIR_TESTDATA . '/link-manager.zip' ); } + if ( file_exists( WP_PLUGIN_DIR . '/link-manager/link-manager.php' ) ) { + // Remove plugin files. + $this->rmdir( WP_PLUGIN_DIR . '/link-manager' ); + // Delete empty directory. + rmdir( WP_PLUGIN_DIR . '/link-manager' ); + } + parent::tear_down(); } @@ -369,10 +387,6 @@ public function test_get_item_invalid_plugin() { * @ticket 50321 */ public function test_create_item() { - if ( isset( get_plugins()['link-manager/link-manager.php'] ) ) { - delete_plugins( array( 'link-manager/link-manager.php' ) ); - } - wp_set_current_user( self::$super_admin ); $this->setup_plugin_download(); @@ -389,10 +403,6 @@ public function test_create_item() { * @ticket 50321 */ public function test_create_item_and_activate() { - if ( isset( get_plugins()['link-manager/link-manager.php'] ) ) { - delete_plugins( array( 'link-manager/link-manager.php' ) ); - } - wp_set_current_user( self::$super_admin ); $this->setup_plugin_download(); @@ -415,10 +425,6 @@ public function test_create_item_and_activate() { * @ticket 50321 */ public function test_create_item_and_activate_errors_if_no_permission_to_activate_plugin() { - if ( isset( get_plugins()['link-manager/link-manager.php'] ) ) { - delete_plugins( array( 'link-manager/link-manager.php' ) ); - } - wp_set_current_user( self::$super_admin ); $this->setup_plugin_download(); $this->disable_activate_permission( 'link-manager/link-manager.php' ); @@ -441,10 +447,6 @@ public function test_create_item_and_activate_errors_if_no_permission_to_activat * @ticket 50321 */ public function test_create_item_and_network_activate_rejected_if_not_multisite() { - if ( isset( get_plugins()['link-manager/link-manager.php'] ) ) { - delete_plugins( array( 'link-manager/link-manager.php' ) ); - } - wp_set_current_user( self::$super_admin ); $this->setup_plugin_download(); @@ -465,10 +467,6 @@ public function test_create_item_and_network_activate_rejected_if_not_multisite( * @ticket 50321 */ public function test_create_item_and_network_activate() { - if ( isset( get_plugins()['link-manager/link-manager.php'] ) ) { - delete_plugins( array( 'link-manager/link-manager.php' ) ); - } - wp_set_current_user( self::$super_admin ); $this->setup_plugin_download(); @@ -546,8 +544,26 @@ public function test_create_item_wdotorg_unreachable() { */ public function test_create_item_unknown_plugin() { wp_set_current_user( self::$super_admin ); + add_filter( + 'pre_http_request', + static function () { + /* + * Mocks the request to: + * https://api.wordpress.org/plugins/info/1.2/?action=plugin_information&request%5Bslug%5D=alex-says-this-block-definitely-doesnt-exist&request%5Bfields%5D%5Bsections%5D=0&request%5Bfields%5D%5Blanguage_packs%5D=1&request%5Blocale%5D=en_US&request%5Bwp_version%5D=5.9 + */ + return array( + 'headers' => array(), + 'response' => array( + 'code' => 404, + 'message' => 'Not Found', + ), + 'body' => '{"error":"Plugin not found."}', + 'cookies' => array(), + 'filename' => null, + ); + } + ); - // This will hit the live API. $request = new WP_REST_Request( 'POST', self::BASE ); $request->set_body_params( array( 'slug' => 'alex-says-this-block-definitely-doesnt-exist' ) ); $response = rest_do_request( $request ); @@ -1005,12 +1021,17 @@ protected function check_get_plugin_data( $data, $network_only = false ) { $this->assertSame( 'My ‘Cool’ Plugin By WordPress.org.', $data['description']['rendered'] ); $this->assertSame( $network_only, $data['network_only'] ); $this->assertSame( '5.6.0', $data['requires_php'] ); - $this->assertSame( '5.4.0', $data['requires_wp'] ); + $this->assertSame( '5.4', $data['requires_wp'] ); $this->assertSame( 'test-plugin', $data['textdomain'] ); } /** - * Sets up the plugin download to come locally instead of downloading it from .org + * Sets up the plugin repository requests to use local data. + * + * Requests to the plugin repository are mocked to avoid external HTTP requests so + * the test suite does not produce false negatives due to network failures. + * + * Both the plugin ZIP file and the plugin API response are mocked. * * @since 5.5.0 */ @@ -1029,8 +1050,23 @@ static function ( $reply, $package, $upgrader ) { 3 ); - // Remove upgrade hooks which are not required for plugin installation tests - // and may interfere with the results due to a timeout in external HTTP requests. + add_filter( + 'plugins_api', + function ( $bypass, $action, $args ) { + // Only mock the plugin_information (link-manager) request. + if ( 'plugin_information' !== $action || 'link-manager' !== $args->slug ) { + return $bypass; + } + return self::$plugin_api_decoded_response; + }, + 10, + 3 + ); + + /* + * Remove upgrade hooks which are not required for plugin installation tests + * and may interfere with the results due to a timeout in external HTTP requests. + */ remove_action( 'upgrader_process_complete', array( 'Language_Pack_Upgrader', 'async_upgrade' ), 20 ); remove_action( 'upgrader_process_complete', 'wp_version_check' ); remove_action( 'upgrader_process_complete', 'wp_update_plugins' ); @@ -1113,7 +1149,7 @@ private function create_test_plugin( $network_only = false ) { * Author URI: https://wordpress.org/ * Text Domain: test-plugin * Requires PHP: 5.6.0 - * Requires at least: 5.4.0{$network} + * Requires at least: 5.4{$network} */ PHP; wp_mkdir_p( WP_PLUGIN_DIR . '/test-plugin' ); @@ -1130,13 +1166,13 @@ private function create_test_plugin( $network_only = false ) { private function prevent_requests_to_host( $blocked_host = 'api.wordpress.org' ) { add_filter( 'pre_http_request', - static function ( $return, $args, $url ) use ( $blocked_host ) { + static function ( $response, $parsed_args, $url ) use ( $blocked_host ) { if ( @parse_url( $url, PHP_URL_HOST ) === $blocked_host ) { return new WP_Error( 'plugins_api_failed', "An expected error occurred connecting to $blocked_host because of a unit test", "cURL error 7: Failed to connect to $blocked_host port 80: Connection refused" ); } - return $return; + return $response; }, 10, 3 diff --git a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php index 85671eff54981..5ce72a57fa55f 100644 --- a/tests/phpunit/tests/rest-api/rest-post-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-post-meta-fields.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Post_Meta_Fields extends WP_Test_REST_TestCase { @@ -19,11 +17,11 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); - self::$wp_meta_keys_saved = isset( $GLOBALS['wp_meta_keys'] ) ? $GLOBALS['wp_meta_keys'] : array(); + self::$wp_meta_keys_saved = $GLOBALS['wp_meta_keys'] ?? array(); self::$post_id = $factory->post->create(); self::$cpt_post_id = $factory->post->create( array( 'post_type' => 'cpt' ) ); } @@ -159,7 +157,7 @@ public function set_up() { 'cpt', array( 'show_in_rest' => true, - 'supports' => array( 'custom-fields' ), + 'supports' => array( 'custom-fields', 'revisions' ), ) ); @@ -245,15 +243,27 @@ public function set_up() { ) ); + register_post_meta( + 'post', + 'with_label', + array( + 'type' => 'string', + 'single' => true, + 'show_in_rest' => true, + 'label' => 'Meta Label', + 'default' => '', + ) + ); + /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); } protected function grant_write_permission() { // Ensure we have write permission. - $user = $this->factory->user->create( + $user = self::factory()->user->create( array( 'role' => 'editor', ) @@ -380,7 +390,7 @@ public function test_get_value_types() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); add_post_meta( self::$post_id, 'test_string', 42 ); @@ -1378,8 +1388,6 @@ public function data_set_subtype_meta_value() { * @dataProvider data_update_value_return_success_with_same_value */ public function test_update_value_return_success_with_same_value( $meta_key, $meta_value ) { - add_post_meta( self::$post_id, $meta_key, $meta_value ); - $this->grant_write_permission(); $data = array( @@ -1394,6 +1402,12 @@ public function test_update_value_return_success_with_same_value( $meta_key, $me $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); + + // Verify the returned meta value is correct. + $data = $response->get_data(); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertArrayHasKey( $meta_key, $data['meta'] ); + $this->assertSame( $meta_value, $data['meta'][ $meta_key ] ); } public function data_update_value_return_success_with_same_value() { @@ -2055,7 +2069,7 @@ public function test_invalid_meta_value_are_set_to_null_in_response() { /** * @ticket 43392 * @ticket 48363 - * @dataProvider _dp_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable + * @dataProvider data_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable */ public function test_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable( $type, $stored, $expected ) { register_post_meta( @@ -2076,7 +2090,7 @@ public function test_meta_values_are_not_set_to_null_in_response_if_type_safely_ $this->assertSame( $expected, $response->get_data()['meta']['safe'] ); } - public function _dp_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable() { + public function data_meta_values_are_not_set_to_null_in_response_if_type_safely_serializable() { return array( array( 'boolean', 'true', true ), array( 'boolean', 'false', false ), @@ -2301,6 +2315,42 @@ public function test_update_meta_with_unchanged_object_values() { $this->assertSame( array( 'project' => 'WordCamp' ), $data['meta']['object'] ); } + /** + * @ticket 57745 + */ + public function test_update_meta_with_unchanged_values_and_custom_authentication() { + register_post_meta( + 'post', + 'authenticated', + array( + 'single' => true, + 'type' => 'boolean', + 'default' => false, + 'show_in_rest' => true, + 'auth_callback' => '__return_false', + ) + ); + + add_post_meta( self::$post_id, 'authenticated', false ); + + $this->grant_write_permission(); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + 'authenticated' => false, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( false, $data['meta']['authenticated'] ); + } + /** * @ticket 43392 */ @@ -2708,7 +2758,7 @@ public function test_boolean_meta_update_to_false_stores_0() { 'single' => true, 'type' => 'boolean', 'show_in_rest' => true, - 'sanitize_callback' => static function( $value ) { + 'sanitize_callback' => static function ( $value ) { return $value ? '1' : '0'; }, ) @@ -2797,7 +2847,7 @@ public function test_update_multi_meta_value_handles_boolean_types() { $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertEquals( array( 0 ), $response->get_data()['meta']['multi_boolean'] ); + $this->assertSameSetsWithIndex( array( false ), $response->get_data()['meta']['multi_boolean'] ); $this->assertFalse( get_metadata_by_mid( 'post', $mid1 ) ); $this->assertNotFalse( get_metadata_by_mid( 'post', $mid2 ) ); @@ -3053,29 +3103,915 @@ public function test_default_is_added_to_schema() { $response = rest_do_request( $request ); $schema = $response->get_data()['schema']['properties']['meta']['properties']['with_default']; - $this->assertArrayHasKey( 'default', $schema ); - $this->assertSame( 'Goodnight Moon', $schema['default'] ); + $this->assertArrayHasKey( 'default', $schema, 'Schema is expected to have the default property' ); + $this->assertSame( 'Goodnight Moon', $schema['default'], 'Schema default is expected to be defined and contain the value of the meta default argument.' ); } /** - * Internal function used to disable an insert query which - * will trigger a wpdb error for testing purposes. + * @ticket 61998 */ - public function error_insert_query( $query ) { - if ( strpos( $query, 'INSERT' ) === 0 ) { - $query = '],'; - } - return $query; + public function test_title_is_added_to_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ); + $response = rest_do_request( $request ); + + $schema = $response->get_data()['schema']['properties']['meta']['properties']['with_label']; + + $this->assertArrayHasKey( 'title', $schema, 'Schema is expected to have the title property' ); + $this->assertSame( 'Meta Label', $schema['title'], 'Schema title is expected to be defined and contain the value of the meta label argument.' ); } /** - * Internal function used to disable an insert query which - * will trigger a wpdb error for testing purposes. + * Ensures that REST API calls with post meta containing the default value for the + * registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. */ - public function error_delete_query( $query ) { - if ( strpos( $query, 'DELETE' ) === 0 ) { - $query = '],'; - } - return $query; + public function test_scalar_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => $type, + 'single' => true, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => $default_value, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing the default) + * for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_scalar_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => $type, + 'single' => false, + 'show_in_rest' => true, + 'default' => $default_value, + ) + ); + + // Write the default value as the sole value. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with string-cast version of default value.' + ); + + // Write multiple values, including the default, to ensure it remains. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + $default_value, + $alternative_value, + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( (string) $default_value, (string) $alternative_value ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored both the default and non-default string-cast values.' + ); + } + + /** + * Ensures that REST API calls with post meta containing an object as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_object_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'object', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => (object) array( $type => $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing an object as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_object_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'object', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + $type => array( 'type' => $type ), + ), + ), + ), + 'default' => (object) array( $type => $default_value ), + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( (object) array( $type => $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + (object) array( $type => $default_value ), + (object) array( $type => $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + // Objects stored into the database are read back as arrays. + $this->assertSame( + array( array( $type => $default_value ), array( $type => $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * Ensures that REST API calls with post meta containing a list array as the default + * value for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Ignored in this test. + */ + public function test_array_singular_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_single = "with_{$type}_default"; + + // Register singular post meta for type. + register_post_meta( + 'post', + $meta_key_single, + array( + 'type' => 'array', + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_single => array( $default_value ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_single, false ), + 'Should have stored a single meta value with an array containing only the default value.' + ); + } + + /** + * Ensures that REST API calls with multi post meta values (containing a list array as + * the default) for the registered meta field stores the default value into the database. + * + * When the default value isn't persisted in the database, a read of the post meta + * at some point in the future might return a different value if the code setting the + * default changed. This ensures that once a value is intentionally saved into the + * database that it will remain durably in future reads. + * + * Further, the total count of stored values may be wrong if the default value + * is culled from the results of a "multi" read. + * + * @ticket 55600 + * + * @dataProvider data_scalar_default_values + * + * @param string $type Scalar type of default value: one of `boolean`, `integer`, `number`, or `string`. + * @param mixed $default_value Appropriate default value for given type. + * @param mixed $alternative_value Appropriate value for given type that doesn't match the default value. + */ + public function test_array_multi_default_is_saved_to_db( $type, $default_value, $alternative_value ) { + $this->grant_write_permission(); + + $meta_key_multiple = "with_multi_{$type}_default"; + + // Register non-singular post meta for type. + register_post_meta( + 'post', + $meta_key_multiple, + array( + 'type' => 'array', + 'single' => false, + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'array', + 'items' => array( + 'type' => $type, + ), + ), + ), + 'default' => $default_value, + ) + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( array( $default_value ) ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_body_params( + array( + 'meta' => array( + $meta_key_multiple => array( + array( $default_value ), + array( $alternative_value ), + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( + 200, + $response->get_status(), + "API call should have returned successfully but didn't: check test setup." + ); + + $this->assertSame( + array( array( $default_value ), array( $alternative_value ) ), + get_metadata_raw( 'post', self::$post_id, $meta_key_multiple, false ), + 'Should have stored a single meta value with an object representing the default value.' + ); + } + + /** + * @ticket 48823 + */ + public function test_multiple_errors_are_returned_at_once() { + $this->grant_write_permission(); + register_post_meta( + 'post', + 'error_1', + array( + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'enum' => array( 'a', 'b' ), + ), + ), + ) + ); + register_post_meta( + 'post', + 'error_2', + array( + 'single' => true, + 'show_in_rest' => array( + 'schema' => array( + 'minLength' => 1, + ), + ), + ) + ); + + $request = new WP_REST_Request( 'PUT', '/wp/v2/posts/' . self::$post_id ); + $request->set_body_params( + array( + 'meta' => array( + 'error_1' => 'c', + 'error_2' => '', + ), + ) + ); + $response = rest_do_request( $request ); + $error = $response->as_error(); + $this->assertWPError( $error ); + $this->assertContains( 'meta.error_1 is not one of a and b.', $error->get_error_messages() ); + $this->assertContains( 'meta.error_2 must be at least 1 character long.', $error->get_error_messages() ); + } + + /** + * Internal function used to disable an insert query which + * will trigger a wpdb error for testing purposes. + */ + public function error_insert_query( $query ) { + if ( strpos( $query, 'INSERT' ) === 0 ) { + $query = '],'; + } + return $query; + } + + /** + * Internal function used to disable an insert query which + * will trigger a wpdb error for testing purposes. + */ + public function error_delete_query( $query ) { + if ( strpos( $query, 'DELETE' ) === 0 ) { + $query = '],'; + } + return $query; + } + + /** + * Test that single post meta is revisioned when saving to the posts REST API endpoint. + * + * @ticket 20564 + */ + public function test_revisioned_single_post_meta_with_posts_endpoint() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'foo', + array( + 'single' => true, + 'show_in_rest' => true, + 'revisions_enabled' => true, + ) + ); + + $post_id = self::$post_id; + + // Update the post, saving the meta. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => 'bar', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id = array_shift( $revisions )->ID; + + // Check that the revisions endpoint returns the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'bar', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'bar', get_post_meta( $revision_id, 'foo', true ) ); + + // Create two more revisions with different meta values for the foo key. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 2', + 'meta' => array( + 'foo' => 'baz', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_2 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_2 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'baz', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'baz', get_post_meta( $revision_id_2, 'foo', true ) ); + + // One more revision! + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 3', + 'meta' => array( + 'foo' => 'qux', + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_3 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_3 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'qux', $response->get_data()['meta']['foo'] ); + + // Check that the post meta is set correctly. + $this->assertSame( 'qux', get_post_meta( $revision_id_3, 'foo', true ) ); + + // Restore Revision 3 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_3 ); + $this->assertSame( 'qux', get_post_meta( $post_id, 'foo', true ) ); + + // Restore Revision 2 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_2 ); + $this->assertSame( 'baz', get_post_meta( $post_id, 'foo', true ) ); + } + + /** + * Test that multi-post meta is revisioned when saving to the posts REST API endpoint. + * + * @ticket 20564 + */ + public function test_revisioned_multiple_post_meta_with_posts_endpoint() { + $this->grant_write_permission(); + + register_post_meta( + 'post', + 'foo', + array( + 'single' => false, + 'show_in_rest' => true, + 'revisions_enabled' => true, + ) + ); + + $post_id = self::$post_id; + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => array( + 'bar', + 'bat', + 'baz', + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Log the current post meta. + $meta = get_post_meta( $post_id ); + + // Update the post. + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1 update', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_1 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_1 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array( 'bar', 'bat', 'baz' ), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( + array( 'bar', 'bat', 'baz' ), + get_post_meta( $revision_id_1, 'foo' ) + ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 2', + 'meta' => array( + 'foo' => array( + 'car', + 'cat', + ), + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_2 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_2 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array( 'car', 'cat' ), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( array( 'car', 'cat' ), get_post_meta( $revision_id_2, 'foo' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 3', + 'meta' => array( + 'foo' => null, + ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + $revision_id_3 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d/revisions/%d', $post_id, $revision_id_3 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + array(), + $response->get_data()['meta']['foo'] + ); + $this->assertSame( array(), get_post_meta( $revision_id_3, 'foo' ) ); + + // Restore Revision 3 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_3 ); + $this->assertSame( array(), get_post_meta( $post_id, 'foo' ) ); + + // Restore Revision 2 and verify the post gets the correct meta value. + wp_restore_post_revision( $revision_id_2 ); + $this->assertSame( array( 'car', 'cat' ), get_post_meta( $post_id, 'foo' ) ); + } + + /** + * Test post meta revisions with a custom post type and the page post type. + * + * @group revision + * @dataProvider data_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt_data_provider + */ + public function test_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt( $passed, $expected, $post_type ) { + + $this->grant_write_permission(); + + // Create the custom meta. + register_post_meta( + $post_type, + 'foo', + array( + 'show_in_rest' => true, + 'revisions_enabled' => true, + 'single' => true, + 'type' => 'string', + ) + ); + + // Set up a new post. + $post_id = $this->factory->post->create( + array( + 'post_content' => 'initial content', + 'post_type' => $post_type, + 'meta_input' => array( + 'foo' => 'foo', + ), + ) + ); + + $plural_mapping = array( + 'page' => 'pages', + 'cpt' => 'cpt', + ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/%s', $plural_mapping[ $post_type ] ) ); + + $response = rest_get_server()->dispatch( $request ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/%s/%d', $plural_mapping[ $post_type ], $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1', + 'meta' => array( + 'foo' => $passed, + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Update the post. + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/%s/%d', $plural_mapping[ $post_type ], $post_id ) ); + $request->set_body_params( + array( + 'title' => 'Revision 1 update', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + // Get the last revision. + $revisions = wp_get_post_revisions( $post_id, array( 'posts_per_page' => 1 ) ); + + $revision_id_1 = array_shift( $revisions )->ID; + + // Check that the revision has the correct meta value. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/%s/%d/revisions/%d', $plural_mapping[ $post_type ], $post_id, $revision_id_1 ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $this->assertSame( + $passed, + $response->get_data()['meta']['foo'] + ); + + $this->assertSame( + array( $passed ), + get_post_meta( $revision_id_1, 'foo' ) + ); + + unregister_post_meta( $post_type, 'foo' ); + wp_delete_post( $post_id, true ); + } + + /** + * Provide data for the meta revision checks. + */ + public function data_revisioned_single_post_meta_with_posts_endpoint_page_and_cpt_data_provider() { + return array( + array( + 'Test string', + 'Test string', + 'cpt', + ), + array( + 'Test string', + 'Test string', + 'page', + ), + array( + 'Test string', + false, + 'cpt', + ), + ); + } + + /** + * Data provider. + * + * Provides example default values of scalar types; + * in contrast to arrays, objects, etc... + * + * @return array[] + */ + public static function data_scalar_default_values() { + return array( + 'boolean default' => array( 'boolean', true, false ), + 'integer default' => array( 'integer', 42, 43 ), + 'number default' => array( 'number', 42.99, 43.99 ), + 'string default' => array( 'string', 'string', 'string2' ), + ); } } diff --git a/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php b/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php index 273c4e3916f48..f6bb1d795a114 100644 --- a/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-statuses-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Post_Statuses_Controller extends WP_Test_REST_Controller_Testcase { @@ -43,7 +41,7 @@ public function test_get_items() { } public function test_get_items_logged_in() { - $user_id = $this->factory->user->create( array( 'role' => 'author' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); wp_set_current_user( $user_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/statuses' ); @@ -72,7 +70,7 @@ public function test_get_items_unauthorized_context() { } public function test_get_item() { - $user_id = $this->factory->user->create( array( 'role' => 'author' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); wp_set_current_user( $user_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/publish' ); $request->set_param( 'context', 'edit' ); @@ -94,7 +92,7 @@ public function test_get_item_invalid_access() { } public function test_get_item_invalid_internal() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); wp_set_current_user( $user_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/statuses/inherit' ); @@ -125,8 +123,8 @@ public function test_delete_item() { public function test_prepare_item() { $obj = get_post_status_object( 'publish' ); - $endpoint = new WP_REST_Post_Statuses_Controller; - $request = new WP_REST_Request; + $endpoint = new WP_REST_Post_Statuses_Controller(); + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $data = $endpoint->prepare_item_for_response( $obj, $request ); $this->check_post_status_obj( $obj, $data->get_data(), $data->get_links() ); @@ -134,8 +132,8 @@ public function test_prepare_item() { public function test_prepare_item_limit_fields() { $obj = get_post_status_object( 'publish' ); - $request = new WP_REST_Request; - $endpoint = new WP_REST_Post_Statuses_Controller; + $request = new WP_REST_Request(); + $endpoint = new WP_REST_Post_Statuses_Controller(); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'id,name' ); $response = $endpoint->prepare_item_for_response( $obj, $request ); @@ -200,7 +198,7 @@ public function test_get_additional_field_registration() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { + public function additional_field_get_callback( $response_data ) { return 123; } @@ -227,5 +225,4 @@ protected function check_post_status_object_response( $response ) { $obj = get_post_status_object( 'publish' ); $this->check_post_status_obj( $obj, $data, $response->get_links() ); } - } diff --git a/tests/phpunit/tests/rest-api/rest-post-types-controller.php b/tests/phpunit/tests/rest-api/rest-post-types-controller.php index 22ff225a69a52..f230373ce27a4 100644 --- a/tests/phpunit/tests/rest-api/rest-post-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-post-types-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Post_Types_Controller extends WP_Test_REST_Controller_Testcase { @@ -38,7 +36,7 @@ public function test_get_items() { $data = $response->get_data(); $post_types = get_post_types( array( 'show_in_rest' => true ), 'objects' ); - $this->assertSame( count( $post_types ), count( $data ) ); + $this->assertCount( count( $post_types ), $data ); $this->assertSame( $post_types['post']->name, $data['post']['slug'] ); $this->check_post_type_obj( 'view', $post_types['post'], $data['post'], $data['post']['_links'] ); $this->assertSame( $post_types['page']->name, $data['page']['slug'] ); @@ -46,14 +44,32 @@ public function test_get_items() { $this->assertArrayNotHasKey( 'revision', $data ); } - public function test_get_items_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/types' ); + $request = new WP_REST_Request( $method, '/wp/v2/types' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + public function test_get_item() { $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' ); $response = rest_get_server()->dispatch( $request ); @@ -62,6 +78,72 @@ public function test_get_item() { $this->assertSame( array( 'category', 'post_tag' ), $data['taxonomies'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/types/post' ); + + $hook_name = 'rest_prepare_post_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'slug' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/types/post' ), + 'get_items request' => array( '/wp/v2/types' ), + ); + } + /** * @ticket 53656 */ @@ -79,6 +161,27 @@ public function test_get_item_cpt() { $this->check_post_type_object_response( 'view', $response, 'cpt' ); } + /** + * @ticket 61477 + */ + public function test_get_item_template_cpt() { + register_post_type( + 'cpt_template', + array( + 'show_in_rest' => true, + 'rest_base' => 'cpt_template', + 'rest_namespace' => 'wordpress/v1', + 'template' => array( + array( 'core/paragraph', array( 'placeholder' => 'Content' ) ), + ), + 'template_lock' => 'all', + ) + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/types/cpt_template' ); + $response = rest_get_server()->dispatch( $request ); + $this->check_post_type_object_response( 'view', $response, 'cpt_template' ); + } + public function test_get_item_page() { $request = new WP_REST_Request( 'GET', '/wp/v2/types/page' ); $response = rest_get_server()->dispatch( $request ); @@ -87,14 +190,20 @@ public function test_get_item_page() { $this->assertSame( array(), $data['taxonomies'] ); } - public function test_get_item_invalid_type() { - $request = new WP_REST_Request( 'GET', '/wp/v2/types/invalid' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_type( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/types/invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_type_invalid', $response, 404 ); } public function test_get_item_edit_context() { - $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + $editor_id = self::factory()->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' ); $request->set_param( 'context', 'edit' ); @@ -102,9 +211,15 @@ public function test_get_item_edit_context() { $this->check_post_type_object_response( 'edit', $response ); } - public function test_get_item_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/types/post' ); + $request = new WP_REST_Request( $method, '/wp/v2/types/post' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); @@ -133,8 +248,8 @@ public function test_delete_item() { public function test_prepare_item() { $obj = get_post_type_object( 'post' ); - $endpoint = new WP_REST_Post_Types_Controller; - $request = new WP_REST_Request; + $endpoint = new WP_REST_Post_Types_Controller(); + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $response = $endpoint->prepare_item_for_response( $obj, $request ); $this->check_post_type_obj( 'edit', $obj, $response->get_data(), $response->get_links() ); @@ -142,8 +257,8 @@ public function test_prepare_item() { public function test_prepare_item_limit_fields() { $obj = get_post_type_object( 'post' ); - $request = new WP_REST_Request; - $endpoint = new WP_REST_Post_Types_Controller; + $request = new WP_REST_Request(); + $endpoint = new WP_REST_Post_Types_Controller(); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'id,name' ); $response = $endpoint->prepare_item_for_response( $obj, $request ); @@ -156,24 +271,34 @@ public function test_prepare_item_limit_fields() { ); } + /** + * @ticket 56467 + * + * @covers WP_REST_Post_Types_Controller::get_item_schema + */ public function test_get_item_schema() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/types' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 12, $properties ); - $this->assertArrayHasKey( 'capabilities', $properties ); - $this->assertArrayHasKey( 'description', $properties ); - $this->assertArrayHasKey( 'hierarchical', $properties ); - $this->assertArrayHasKey( 'viewable', $properties ); - $this->assertArrayHasKey( 'labels', $properties ); - $this->assertArrayHasKey( 'name', $properties ); - $this->assertArrayHasKey( 'slug', $properties ); - $this->assertArrayHasKey( 'supports', $properties ); - $this->assertArrayHasKey( 'taxonomies', $properties ); - $this->assertArrayHasKey( 'rest_base', $properties ); - $this->assertArrayHasKey( 'rest_namespace', $properties ); - $this->assertArrayHasKey( 'visibility', $properties ); + + $this->assertCount( 16, $properties, 'Schema should have 16 properties' ); + $this->assertArrayHasKey( 'capabilities', $properties, '`capabilities` should be included in the schema' ); + $this->assertArrayHasKey( 'description', $properties, '`description` should be included in the schema' ); + $this->assertArrayHasKey( 'hierarchical', $properties, '`hierarchical` should be included in the schema' ); + $this->assertArrayHasKey( 'viewable', $properties, '`viewable` should be included in the schema' ); + $this->assertArrayHasKey( 'labels', $properties, '`labels` should be included in the schema' ); + $this->assertArrayHasKey( 'name', $properties, '`name` should be included in the schema' ); + $this->assertArrayHasKey( 'slug', $properties, '`slug` should be included in the schema' ); + $this->assertArrayHasKey( 'supports', $properties, '`supports` should be included in the schema' ); + $this->assertArrayHasKey( 'has_archive', $properties, '`has_archive` should be included in the schema' ); + $this->assertArrayHasKey( 'taxonomies', $properties, '`taxonomies` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_base', $properties, '`rest_base` should be included in the schema' ); + $this->assertArrayHasKey( 'rest_namespace', $properties, '`rest_namespace` should be included in the schema' ); + $this->assertArrayHasKey( 'visibility', $properties, '`visibility` should be included in the schema' ); + $this->assertArrayHasKey( 'icon', $properties, '`icon` should be included in the schema' ); + $this->assertArrayHasKey( 'template', $properties, '`template` should be included in the schema' ); + $this->assertArrayHasKey( 'template_lock', $properties, '`template_lock` should be included in the schema' ); } public function test_get_additional_field_registration() { @@ -212,10 +337,26 @@ public function test_get_additional_field_registration() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { + public function additional_field_get_callback( $response_data ) { return 123; } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_post_types_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/types' ); + $hook_name = 'rest_prepare_post_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + protected function check_post_type_obj( $context, $post_type_obj, $data, $links ) { $this->assertSame( $post_type_obj->label, $data['name'] ); $this->assertSame( $post_type_obj->name, $data['slug'] ); @@ -223,6 +364,9 @@ protected function check_post_type_obj( $context, $post_type_obj, $data, $links $this->assertSame( $post_type_obj->hierarchical, $data['hierarchical'] ); $this->assertSame( $post_type_obj->rest_base, $data['rest_base'] ); $this->assertSame( $post_type_obj->rest_namespace, $data['rest_namespace'] ); + $this->assertSame( $post_type_obj->has_archive, $data['has_archive'] ); + $this->assertSame( $post_type_obj->template ?? array(), $data['template'] ); + $this->assertSame( ! empty( $post_type_obj->template_lock ) ? $post_type_obj->template_lock : false, $data['template_lock'] ); $links = test_rest_expand_compact_links( $links ); $this->assertSame( rest_url( 'wp/v2/types' ), $links['collection'][0]['href'] ); @@ -256,5 +400,4 @@ protected function check_post_type_object_response( $context, $response, $post_t $obj = get_post_type_object( $post_type ); $this->check_post_type_obj( $context, $obj, $data, $response->get_links() ); } - } diff --git a/tests/phpunit/tests/rest-api/rest-posts-controller.php b/tests/phpunit/tests/rest-api/rest-posts-controller.php index b35921a73db19..212ddde70dd83 100644 --- a/tests/phpunit/tests/rest-api/rest-posts-controller.php +++ b/tests/phpunit/tests/rest-api/rest-posts-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Testcase { @@ -20,14 +18,19 @@ class WP_Test_REST_Posts_Controller extends WP_Test_REST_Post_Type_Controller_Te protected static $supported_formats; protected static $post_ids = array(); + protected static $terms = array(); protected static $total_posts = 30; protected static $per_page = 50; protected $forbidden_cat; protected $posts_clauses; + private $attachments_created = false; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$post_id = $factory->post->create(); + self::$terms = $factory->term->create_many( 15, array( 'taxonomy' => 'category' ) ); + wp_set_object_terms( self::$post_id, self::$terms, 'category' ); self::$superadmin_id = $factory->user->create( array( @@ -115,13 +118,24 @@ public function set_up() { add_filter( 'posts_clauses', array( $this, 'save_posts_clauses' ), 10, 2 ); } + public function tear_down() { + if ( true === $this->attachments_created ) { + $this->remove_added_uploads(); + $this->attachments_created = false; + } + + parent::tear_down(); + } + public function wpSetUpBeforeRequest( $result, $server, $request ) { $this->posts_clauses = array(); return $result; } public function save_posts_clauses( $orderby, $query ) { - array_push( $this->posts_clauses, $orderby ); + if ( 'revision' !== $query->query_vars['post_type'] ) { + array_push( $this->posts_clauses, $orderby ); + } return $orderby; } @@ -154,12 +168,14 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/posts/' . self::$post_id ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } @@ -180,6 +196,8 @@ public function test_registered_query_params() { 'categories_exclude', 'context', 'exclude', + 'format', + 'ignore_sticky', 'include', 'modified_after', 'modified_before', @@ -189,6 +207,8 @@ public function test_registered_query_params() { 'page', 'per_page', 'search', + 'search_columns', + 'search_semantics', 'slug', 'status', 'sticky', @@ -205,8 +225,22 @@ public function test_registered_get_item_params() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $keys = array_keys( $data['endpoints'][0]['args'] ); - sort( $keys ); - $this->assertSame( array( 'context', 'id', 'password' ), $keys ); + $this->assertEqualSets( array( 'context', 'id', 'password', 'excerpt_length' ), $keys ); + } + + public function test_registered_get_items_embed() { + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'include', array( self::$post_id ) ); + $response = rest_get_server()->dispatch( $request ); + $response = rest_get_server()->response_to_data( $response, true ); + $this->assertArrayHasKey( '_embedded', $response[0], 'The _embedded key must exist' ); + $this->assertArrayHasKey( 'wp:term', $response[0]['_embedded'], 'The wp:term key must exist' ); + $this->assertCount( 15, $response[0]['_embedded']['wp:term'][0], 'Should should be 15 terms and not the default 10' ); + $i = 0; + foreach ( $response[0]['_embedded']['wp:term'][0] as $term ) { + $this->assertSame( self::$terms[ $i ], $term['id'], 'Check term id existing in response' ); + ++$i; + } } /** @@ -239,13 +273,45 @@ public function test_get_items() { $this->check_get_posts_response( $response ); } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_post_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts' ); + + $hook_name = 'rest_prepare_post'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + + $headers = $response->get_headers(); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertArrayHasKey( 'Link', $headers, 'The "Link" header should be present in the response.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * A valid query that returns 0 results should return an empty JSON list. + * In case of a HEAD request, the response should not contain a body. * - * @issue 862 + * @dataProvider data_readable_http_methods + * @link https://github.com/WP-API/WP-API/issues/862 + * @ticket 56481 + * + * @covers WP_REST_Posts_Controller::get_items + * + * @param string $method The HTTP method to use. */ - public function test_get_items_empty_query() { - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + public function test_get_items_empty_query( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_query_params( array( 'author' => REST_TESTS_IMPOSSIBLY_HIGH_NUMBER, @@ -253,137 +319,213 @@ public function test_get_items_empty_query() { ); $response = rest_get_server()->dispatch( $request ); - $this->assertEmpty( $response->get_data() ); - $this->assertSame( 200, $response->get_status() ); + if ( $request->is_method( 'HEAD' ) ) { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is an empty array for GET request.' ); + } + + $headers = $response->get_headers(); + $this->assertSame( 0, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 0.' ); + $this->assertSame( 0, $headers['X-WP-TotalPages'], 'Failed asserting that X-WP-TotalPages header is 0.' ); } - public function test_get_items_author_query() { - $this->factory->post->create( array( 'post_author' => self::$editor_id ) ); - $this->factory->post->create( array( 'post_author' => self::$author_id ) ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_author_query( $method ) { + self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); + self::factory()->post->create( array( 'post_author' => self::$author_id ) ); $total_posts = self::$total_posts + 2; // All posts in the database. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $total_posts, $response->get_data() ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts, $response->get_data() ); + + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + } // Limit to editor and author. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author', array( self::$editor_id, self::$author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSameSets( array( self::$editor_id, self::$author_id ), wp_list_pluck( $data, 'author' ) ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSameSets( array( self::$editor_id, self::$author_id ), wp_list_pluck( $data, 'author' ) ); + } else { + $this->assertSame( array(), $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 2.' ); + } // Limit to editor. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author', self::$editor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( 1, $data ); - $this->assertSame( self::$editor_id, $data[0]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 1, $data ); + $this->assertSame( self::$editor_id, $data[0]['author'] ); + } else { + $this->assertSame( array(), $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 1, $headers['X-WP-Total'], 'Failed asserting that X-WP-Total header is 1.' ); + } } - public function test_get_items_author_exclude_query() { - $this->factory->post->create( array( 'post_author' => self::$editor_id ) ); - $this->factory->post->create( array( 'post_author' => self::$author_id ) ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_author_exclude_query( $method ) { + self::factory()->post->create( array( 'post_author' => self::$editor_id ) ); + self::factory()->post->create( array( 'post_author' => self::$author_id ) ); $total_posts = self::$total_posts + 2; // All posts in the database. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertCount( $total_posts, $response->get_data() ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts, $response->get_data() ); + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Exclude editor and author. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $request->set_param( 'author_exclude', array( self::$editor_id, self::$author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( $total_posts - 2, $data ); - $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); - $this->assertNotEquals( self::$author_id, $data[0]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts - 2, $data ); + $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); + $this->assertNotEquals( self::$author_id, $data[0]['author'] ); + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts - 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Exclude editor. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); $request->set_param( 'author_exclude', self::$editor_id ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); - $this->assertCount( $total_posts - 1, $data ); - $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); - $this->assertNotEquals( self::$editor_id, $data[1]['author'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( $total_posts - 1, $data ); + $this->assertNotEquals( self::$editor_id, $data[0]['author'] ); + $this->assertNotEquals( self::$editor_id, $data[1]['author'] ); + } else { + $this->assertSame( array(), $response->get_data(), 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts - 1, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } // Invalid 'author_exclude' should error. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'author_exclude', 'invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } - public function test_get_items_include_query() { - $id1 = $this->factory->post->create( + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_include_query( $method ) { + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_date' => '2001-02-03 04:05:06', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_date' => '2001-02-03 04:05:07', ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); // Order defaults to date descending. $request->set_param( 'include', array( $id1, $id2 ) ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSame( $id2, $data[0]['id'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSame( $id2, $data[0]['id'] ); + } else { + $this->assertSame( array(), $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } + $this->assertPostsOrderedBy( '{posts}.post_date DESC' ); // 'orderby' => 'include'. $request->set_param( 'orderby', 'include' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $this->assertCount( 2, $data ); - $this->assertSame( $id1, $data[0]['id'] ); + if ( $request->is_method( 'get' ) ) { + $this->assertCount( 2, $data ); + $this->assertSame( $id1, $data[0]['id'] ); + } else { + $this->assertSame( array(), $data, 'Failed asserting that response data is null for HEAD request.' ); + $headers = $response->get_headers(); + $this->assertSame( 2, $headers['X-WP-Total'], 'Failed asserting that the number of posts is correct.' ); + } + $this->assertPostsOrderedBy( "FIELD({posts}.ID,$id1,$id2)" ); // Invalid 'include' should error. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'include', 'invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function test_get_items_orderby_author_query() { - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_author' => self::$editor_id, ) ); - $id3 = $this->factory->post->create( + $id3 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_author' => self::$editor_id, ) ); - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_author' => self::$author_id, @@ -406,9 +548,9 @@ public function test_get_items_orderby_author_query() { } public function test_get_items_orderby_modified_query() { - $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id1 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $this->update_post_modified( $id1, '2016-04-20 4:26:20' ); $this->update_post_modified( $id2, '2016-02-01 20:24:02' ); @@ -430,19 +572,19 @@ public function test_get_items_orderby_modified_query() { } public function test_get_items_orderby_parent_query() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', ) ); - $id3 = $this->factory->post->create( + $id3 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_type' => 'page', @@ -468,8 +610,8 @@ public function test_get_items_orderby_parent_query() { } public function test_get_items_exclude_query() { - $id1 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id1 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $response = rest_get_server()->dispatch( $request ); @@ -498,7 +640,7 @@ public function test_get_items_exclude_query() { } public function test_get_items_search_query() { - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Search Result', 'post_status' => 'publish', @@ -519,43 +661,48 @@ public function test_get_items_search_query() { $this->assertSame( 'Search Result', $data[0]['title']['rendered'] ); } + /** + * @ticket 63307 + */ public function test_get_items_slug_query() { - $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_title' => 'Apple', 'post_status' => 'publish', ) ); - $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_title' => 'Banana', 'post_status' => 'publish', ) ); + update_option( 'sticky_posts', array( $id2 ) ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'slug', 'apple' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); $this->assertCount( 1, $data ); - $this->assertSame( 'Apple', $data[0]['title']['rendered'] ); + $this->assertSame( 'Apple', $data[0]['title']['rendered'], 'Return the post with the given slug' ); } public function test_get_items_multiple_slugs_array_query() { - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Banana', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Peach', 'post_status' => 'publish', @@ -577,19 +724,19 @@ public function test_get_items_multiple_slugs_array_query() { } public function test_get_items_multiple_slugs_string_query() { - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Banana', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Peach', 'post_status' => 'publish', @@ -613,7 +760,7 @@ public function test_get_items_multiple_slugs_string_query() { public function test_get_items_status_query() { wp_set_current_user( 0 ); - $this->factory->post->create( array( 'post_status' => 'draft' ) ); + self::factory()->post->create( array( 'post_status' => 'draft' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'per_page', self::$per_page ); @@ -639,9 +786,9 @@ public function test_get_items_status_query() { public function test_get_items_multiple_statuses_string_query() { wp_set_current_user( self::$editor_id ); - $this->factory->post->create( array( 'post_status' => 'draft' ) ); - $this->factory->post->create( array( 'post_status' => 'private' ) ); - $this->factory->post->create( array( 'post_status' => 'publish' ) ); + self::factory()->post->create( array( 'post_status' => 'draft' ) ); + self::factory()->post->create( array( 'post_status' => 'private' ) ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'context', 'edit' ); @@ -662,9 +809,9 @@ public function test_get_items_multiple_statuses_string_query() { public function test_get_items_multiple_statuses_array_query() { wp_set_current_user( self::$editor_id ); - $this->factory->post->create( array( 'post_status' => 'draft' ) ); - $this->factory->post->create( array( 'post_status' => 'pending' ) ); - $this->factory->post->create( array( 'post_status' => 'publish' ) ); + self::factory()->post->create( array( 'post_status' => 'draft' ) ); + self::factory()->post->create( array( 'post_status' => 'pending' ) ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'context', 'edit' ); @@ -694,7 +841,7 @@ public function test_get_items_multiple_statuses_one_invalid_query() { * @ticket 43701 */ public function test_get_items_multiple_statuses_custom_role_one_invalid_query() { - $private_post_id = $this->factory->post->create( array( 'post_status' => 'private' ) ); + $private_post_id = self::factory()->post->create( array( 'post_status' => 'private' ) ); wp_set_current_user( self::$private_reader_id ); @@ -715,7 +862,7 @@ public function test_get_items_invalid_status_query() { } public function test_get_items_status_without_permissions() { - $draft_id = $this->factory->post->create( + $draft_id = self::factory()->post->create( array( 'post_status' => 'draft', ) @@ -729,31 +876,92 @@ public function test_get_items_status_without_permissions() { $this->assertSame( 200, $response->get_status() ); $all_data = $response->get_data(); + + $this->assertNotEmpty( $all_data ); + foreach ( $all_data as $post ) { $this->assertNotEquals( $draft_id, $post['id'] ); } } + /** + * @ticket 56350 + * + * @dataProvider data_get_items_exact_search + * + * @param string $search_term The search term. + * @param bool $exact_search Whether the search is an exact or general search. + * @param int $expected The expected number of matching posts. + */ + public function test_get_items_exact_search( $search_term, $exact_search, $expected ) { + self::factory()->post->create( + array( + 'post_title' => 'Rye', + 'post_content' => 'This is a post about Rye Bread', + ) + ); + + self::factory()->post->create( + array( + 'post_title' => 'Types of Bread', + 'post_content' => 'Types of bread are White and Rye Bread', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request['search'] = $search_term; + if ( $exact_search ) { + $request['search_semantics'] = 'exact'; + } + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( $expected, $response->get_data() ); + } + + /** + * Data provider for test_get_items_exact_search(). + * + * @return array[] + */ + public function data_get_items_exact_search() { + return array( + 'general search, one exact match and one partial match' => array( + 'search_term' => 'Rye', + 'exact_search' => false, + 'expected' => 2, + ), + 'exact search, one exact match and one partial match' => array( + 'search_term' => 'Rye', + 'exact_search' => true, + 'expected' => 1, + ), + 'exact search, no match and one partial match' => array( + 'search_term' => 'Rye Bread', + 'exact_search' => true, + 'expected' => 0, + ), + ); + } + public function test_get_items_order_and_orderby() { - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple Pie', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple Sauce', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple Cobbler', 'post_status' => 'publish', ) ); - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => 'Apple Coffee Cake', 'post_status' => 'publish', @@ -790,7 +998,7 @@ public function test_get_items_order_and_orderby() { } public function test_get_items_with_orderby_include_without_include_param() { - $this->factory->post->create( array( 'post_status' => 'publish' ) ); + self::factory()->post->create( array( 'post_status' => 'publish' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'orderby', 'include' ); @@ -801,19 +1009,19 @@ public function test_get_items_with_orderby_include_without_include_param() { } public function test_get_items_with_orderby_id() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_date' => '2016-01-13 02:26:48', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_date' => '2016-01-12 02:26:48', ) ); - $id3 = $this->factory->post->create( + $id3 = self::factory()->post->create( array( 'post_status' => 'publish', 'post_date' => '2016-01-11 02:26:48', @@ -835,14 +1043,14 @@ public function test_get_items_with_orderby_id() { } public function test_get_items_with_orderby_slug() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_title' => 'ABC', 'post_name' => 'xyz', 'post_status' => 'publish', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_title' => 'XYZ', 'post_name' => 'abc', @@ -866,7 +1074,7 @@ public function test_get_items_with_orderby_slug() { public function test_get_items_with_orderby_slugs() { $slugs = array( 'burrito', 'taco', 'chalupa' ); foreach ( $slugs as $slug ) { - $this->factory->post->create( + self::factory()->post->create( array( 'post_title' => $slug, 'post_name' => $slug, @@ -888,14 +1096,14 @@ public function test_get_items_with_orderby_slugs() { } public function test_get_items_with_orderby_relevance() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_title' => 'Title is more relevant', 'post_content' => 'Content is', 'post_status' => 'publish', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_title' => 'Title is', 'post_content' => 'Content is less relevant', @@ -916,14 +1124,14 @@ public function test_get_items_with_orderby_relevance() { } public function test_get_items_with_orderby_relevance_two_terms() { - $id1 = $this->factory->post->create( + $id1 = self::factory()->post->create( array( 'post_title' => 'Title is more relevant', 'post_content' => 'Content is', 'post_status' => 'publish', ) ); - $id2 = $this->factory->post->create( + $id2 = self::factory()->post->create( array( 'post_title' => 'Title is', 'post_content' => 'Content is less relevant', @@ -990,9 +1198,9 @@ public function test_get_items_tags_query() { public function test_get_items_tags_exclude_query() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id4 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $tag = wp_insert_term( 'My Tag', 'post_tag' ); $total_posts = self::$total_posts + 3; @@ -1013,7 +1221,7 @@ public function test_get_items_tags_exclude_query() { public function test_get_items_tags_and_categories_query() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $tag = wp_insert_term( 'My Tag', 'post_tag' ); $category = wp_insert_term( 'My Category', 'category' ); @@ -1038,7 +1246,7 @@ public function test_get_items_tags_and_categories_query() { */ public function test_get_items_tags_or_categories_query() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $tag = wp_insert_term( 'My Tag', 'post_tag' ); $category = wp_insert_term( 'My Category', 'category' ); @@ -1060,7 +1268,7 @@ public function test_get_items_tags_or_categories_query() { public function test_get_items_tags_and_categories_exclude_query() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $tag = wp_insert_term( 'My Tag', 'post_tag' ); $category = wp_insert_term( 'My Category', 'category' ); @@ -1087,9 +1295,9 @@ public function test_get_items_tags_and_categories_exclude_query() { */ public function test_get_items_tags_or_categories_exclude_query() { $id1 = end( self::$post_ids ); - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id4 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id4 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $tag = wp_insert_term( 'My Tag', 'post_tag' ); $category = wp_insert_term( 'My Category', 'category' ); @@ -1351,7 +1559,7 @@ public function test_get_items_relation_with_no_tax_query() { public function test_get_items_sticky() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); update_option( 'sticky_posts', array( $id2 ) ); @@ -1372,7 +1580,7 @@ public function test_get_items_sticky() { public function test_get_items_sticky_with_include() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); update_option( 'sticky_posts', array( $id2 ) ); @@ -1383,8 +1591,7 @@ public function test_get_items_sticky_with_include() { $response = rest_get_server()->dispatch( $request ); $this->assertCount( 0, $response->get_data() ); - // FIXME Since this request returns zero posts, the query is executed twice. - $this->assertCount( 2, $this->posts_clauses ); + $this->assertCount( 1, $this->posts_clauses ); $this->posts_clauses = array_slice( $this->posts_clauses, 0, 1 ); $this->assertPostsWhere( " AND {posts}.ID IN (0) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); @@ -1415,8 +1622,7 @@ public function test_get_items_sticky_no_sticky_posts() { $response = rest_get_server()->dispatch( $request ); $this->assertCount( 0, $response->get_data() ); - // FIXME Since this request returns zero posts, the query is executed twice. - $this->assertCount( 2, $this->posts_clauses ); + $this->assertCount( 1, $this->posts_clauses ); $this->posts_clauses = array_slice( $this->posts_clauses, 0, 1 ); $this->assertPostsWhere( " AND {posts}.ID IN (0) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); @@ -1434,8 +1640,7 @@ public function test_get_items_sticky_with_include_no_sticky_posts() { $response = rest_get_server()->dispatch( $request ); $this->assertCount( 0, $response->get_data() ); - // FIXME Since this request returns zero posts, the query is executed twice. - $this->assertCount( 2, $this->posts_clauses ); + $this->assertCount( 1, $this->posts_clauses ); $this->posts_clauses = array_slice( $this->posts_clauses, 0, 1 ); $this->assertPostsWhere( " AND {posts}.ID IN (0) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); @@ -1443,7 +1648,7 @@ public function test_get_items_sticky_with_include_no_sticky_posts() { public function test_get_items_not_sticky() { $id1 = end( self::$post_ids ); - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $total_posts = self::$total_posts + 1; @@ -1465,8 +1670,8 @@ public function test_get_items_not_sticky() { public function test_get_items_not_sticky_with_exclude() { $id1 = end( self::$post_ids ); - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $total_posts = self::$total_posts + 2; @@ -1486,13 +1691,13 @@ public function test_get_items_not_sticky_with_exclude() { $this->assertNotContains( $id2, $ids ); $this->assertNotContains( $id3, $ids ); - $this->assertPostsWhere( " AND {posts}.ID NOT IN ($id3,$id2) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); + $this->assertPostsWhere( " AND {posts}.ID NOT IN ($id2,$id3) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); } public function test_get_items_not_sticky_with_exclude_no_sticky_posts() { $id1 = self::$post_id; - $id2 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); - $id3 = $this->factory->post->create( array( 'post_status' => 'publish' ) ); + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $id3 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); $total_posts = self::$total_posts + 2; @@ -1515,12 +1720,143 @@ public function test_get_items_not_sticky_with_exclude_no_sticky_posts() { $this->assertPostsWhere( " AND {posts}.ID NOT IN ($id3) AND {posts}.post_type = 'post' AND (({posts}.post_status = 'publish'))" ); } - public function test_get_items_pagination_headers() { + /** + * Tests that Rest Post controller supports search columns. + * + * @ticket 43867 + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_get_items_with_custom_search_columns() { + $id1 = self::factory()->post->create( + array( + 'post_title' => 'Title contain foo and bar', + 'post_content' => 'Content contain bar', + 'post_excerpt' => 'Excerpt contain baz', + ) + ); + $id2 = self::factory()->post->create( + array( + 'post_title' => 'Title contain baz', + 'post_content' => 'Content contain foo and bar', + 'post_excerpt' => 'Excerpt contain foo, bar and baz', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'search', 'foo bar' ); + $request->set_param( 'search_columns', array( 'post_title' ) ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response should have a status code 200.' ); + $data = $response->get_data(); + $this->assertCount( 1, $data, 'Response should contain one result.' ); + $this->assertSame( $id1, $data[0]['id'], 'Result should match expected value.' ); + } + + /** + * @ticket 55592 + * + * @covers WP_REST_Posts_Controller::get_items + * @covers ::update_post_thumbnail_cache + */ + public function test_get_items_primes_thumbnail_cache_for_featured_media() { + $file = DIR_TESTDATA . '/images/canola.jpg'; + $attachment_ids = array(); + $post_ids = array(); + for ( $i = 0; $i < 3; $i++ ) { + $post_ids[ $i ] = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + $attachment_ids[ $i ] = self::factory()->attachment->create_object( + $file, + $post_ids[ $i ], + array( + 'post_mime_type' => 'image/jpeg', + ) + ); + set_post_thumbnail( $post_ids[ $i ], $attachment_ids[ $i ] ); + } + + // Attachment creation warms thumbnail IDs. Needs clean up for test. + wp_cache_delete_multiple( $attachment_ids, 'posts' ); + + $filter = new MockAction(); + add_filter( 'update_post_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'include', $post_ids ); + rest_get_server()->dispatch( $request ); + + $args = $filter->get_args(); + $last = end( $args ); + $this->assertIsArray( $last, 'The last value is not an array' ); + $this->assertSameSets( $attachment_ids, $last[1] ); + } + + /** + * @ticket 55593 + * + * @covers WP_REST_Posts_Controller::get_items + * @covers ::update_post_parent_caches + */ + public function test_get_items_primes_parent_post_caches() { + $parent_id1 = self::$post_ids[0]; + $parent_id2 = self::$post_ids[1]; + $parent_ids = array( $parent_id1, $parent_id2 ); + $attachment_ids = array(); + $attachment_ids[] = self::factory()->attachment->create_object( + DIR_TESTDATA . '/images/canola.jpg', + $parent_id1, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption 1', + ) + ); + + $attachment_ids[] = self::factory()->attachment->create_object( + DIR_TESTDATA . '/images/canola.jpg', + $parent_id2, + array( + 'post_mime_type' => 'image/jpeg', + 'post_excerpt' => 'A sample caption 2', + ) + ); + + // Attachment creation warms parent IDs. Needs clean up for test. + wp_cache_delete_multiple( $parent_ids, 'posts' ); + wp_cache_delete_multiple( $attachment_ids, 'posts' ); + + $filter = new MockAction(); + add_filter( 'update_post_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/media' ); + rest_get_server()->dispatch( $request ); + + $events = $filter->get_events(); + $args = wp_list_pluck( $events, 'args' ); + $primed = false; + sort( $parent_ids ); + foreach ( $args as $arg ) { + sort( $arg[1] ); + if ( $parent_ids === $arg[1] ) { + $primed = $arg; + break; + } + } + + $this->assertIsArray( $primed, 'The last value is not an array' ); + $this->assertSameSets( $parent_ids, $primed[1] ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_pagination_headers( $method ) { $total_posts = self::$total_posts; $total_pages = (int) ceil( $total_posts / 10 ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_posts, $headers['X-WP-Total'] ); @@ -1535,10 +1871,10 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // 3rd page. - $this->factory->post->create(); - $total_posts++; - $total_pages++; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + self::factory()->post->create(); + ++$total_posts; + ++$total_pages; + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1560,7 +1896,7 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // Last page. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', $total_pages ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1576,7 +1912,7 @@ public function test_get_items_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); // Out of bounds. - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_param( 'page', 100 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -1584,7 +1920,7 @@ public function test_get_items_pagination_headers() { // With query params. $total_pages = (int) ceil( $total_posts / 5 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); $request->set_query_params( array( 'per_page' => 5, @@ -1613,8 +1949,80 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/posts' ); + + $filter = new MockAction(); + + add_filter( 'posts_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only post IDs.' ); + $this->assertArrayHasKey( 'update_post_term_cache', $query->query_vars, 'The "update_post_term_cache" parameter is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_post_term_cache'], 'The "update_post_term_cache" parameter must be false for HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_meta_cache', $query->query_vars, 'The "update_post_meta_cache" parameter is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_post_meta_cache'], 'The "update_post_meta_cache" parameter must be false for HEAD requests.' ); + } else { + $this->assertTrue( ! array_key_exists( 'fields', $query->query ) || 'ids' !== $query->query['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertTrue( ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], 'The fields parameter should not be forced to "ids" for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_term_cache', $query->query_vars, 'The "update_post_term_cache" parameter is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_post_term_cache'], 'The "update_post_term_cache" parameter must be true for non-HEAD requests.' ); + $this->assertArrayHasKey( 'update_post_meta_cache', $query->query_vars, 'The "update_post_meta_cache" parameter is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_post_meta_cache'], 'The "update_post_meta_cache" parameter must be true for non-HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $posts_table = preg_quote( $wpdb->posts, '/' ); + $pattern = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $posts_table . '\.ID\s+FROM\s+' . $posts_table . '\s+WHERE/i'; + + // Assert that the SQL query only fetches the ID column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + public function test_get_items_status_draft_permissions() { - $draft_id = $this->factory->post->create( array( 'post_status' => 'draft' ) ); + $draft_id = self::factory()->post->create( array( 'post_status' => 'draft' ) ); // Drafts status query var inaccessible to unauthorized users. wp_set_current_user( 0 ); @@ -1644,7 +2052,7 @@ public function test_get_items_status_draft_permissions() { * @ticket 43701 */ public function test_get_items_status_private_permissions() { - $private_post_id = $this->factory->post->create( array( 'post_status' => 'private' ) ); + $private_post_id = self::factory()->post->create( array( 'post_status' => 'private' ) ); wp_set_current_user( 0 ); @@ -1692,16 +2100,16 @@ public function test_get_items_invalid_context() { public function test_get_items_invalid_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'after', rand_str() ); - $request->set_param( 'before', rand_str() ); + $request->set_param( 'after', 'foo' ); + $request->set_param( 'before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } public function test_get_items_valid_date() { - $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-15T00:00:00Z' ) ); - $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-16T00:00:00Z' ) ); - $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-17T00:00:00Z' ) ); + $post1 = self::factory()->post->create( array( 'post_date' => '2016-01-15T00:00:00Z' ) ); + $post2 = self::factory()->post->create( array( 'post_date' => '2016-01-16T00:00:00Z' ) ); + $post3 = self::factory()->post->create( array( 'post_date' => '2016-01-17T00:00:00Z' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); $request->set_param( 'after', '2016-01-15T00:00:00Z' ); @@ -1717,8 +2125,8 @@ public function test_get_items_valid_date() { */ public function test_get_items_invalid_modified_date() { $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); - $request->set_param( 'modified_after', rand_str() ); - $request->set_param( 'modified_before', rand_str() ); + $request->set_param( 'modified_after', 'foo' ); + $request->set_param( 'modified_before', 'bar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } @@ -1727,9 +2135,9 @@ public function test_get_items_invalid_modified_date() { * @ticket 50617 */ public function test_get_items_valid_modified_date() { - $post1 = $this->factory->post->create( array( 'post_date' => '2016-01-01 00:00:00' ) ); - $post2 = $this->factory->post->create( array( 'post_date' => '2016-01-02 00:00:00' ) ); - $post3 = $this->factory->post->create( array( 'post_date' => '2016-01-03 00:00:00' ) ); + $post1 = self::factory()->post->create( array( 'post_date' => '2016-01-01 00:00:00' ) ); + $post2 = self::factory()->post->create( array( 'post_date' => '2016-01-02 00:00:00' ) ); + $post3 = self::factory()->post->create( array( 'post_date' => '2016-01-03 00:00:00' ) ); $this->update_post_modified( $post1, '2016-01-15 00:00:00' ); $this->update_post_modified( $post2, '2016-01-16 00:00:00' ); $this->update_post_modified( $post3, '2016-01-17 00:00:00' ); @@ -1759,6 +2167,73 @@ public function test_get_item() { $this->check_get_post_response( $response, 'view' ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + + $hook_name = 'rest_prepare_' . get_post_type( self::$post_id ); + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'Link', $headers, 'The "Link" header should be present in the response.' ); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', sprintf( $path, self::$post_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/posts/%d' ), + 'get_items request' => array( '/wp/v2/posts' ), + ); + } + public function test_get_item_links() { $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); $response = rest_get_server()->dispatch( $request ); @@ -1846,8 +2321,8 @@ public function test_get_item_links_no_author() { $this->assertSame( rest_url( '/wp/v2/users/' . self::$author_id ), $links['author'][0]['href'] ); } - public function test_get_post_draft_status_not_authenicated() { - $draft_id = $this->factory->post->create( + public function test_get_post_draft_status_not_authenticated() { + $draft_id = self::factory()->post->create( array( 'post_status' => 'draft', ) @@ -1861,9 +2336,15 @@ public function test_get_post_draft_status_not_authenicated() { $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); } + /** + * Tests that authenticated users are only allowed to read password protected content + * if they have the 'edit_post' meta capability for the post. + */ public function test_get_post_draft_edit_context() { $post_content = 'Hello World!'; - $this->factory->post->create( + + // Create a password protected post as an Editor. + self::factory()->post->create( array( 'post_title' => 'Hola', 'post_password' => 'password', @@ -1872,18 +2353,27 @@ public function test_get_post_draft_edit_context() { 'post_author' => self::$editor_id, ) ); - $draft_id = $this->factory->post->create( + + // Create a draft with the Latest Posts block as a Contributor. + $draft_id = self::factory()->post->create( array( 'post_status' => 'draft', 'post_author' => self::$contributor_id, 'post_content' => ' ', ) ); + + // Set the current user to Contributor and request the draft for editing. wp_set_current_user( self::$contributor_id ); $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $draft_id ) ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + + /* + * Verify that the content of a password protected post created by an Editor + * is not viewable by a Contributor. + */ $this->assertStringNotContainsString( $post_content, $data['content']['rendered'] ); } @@ -1938,7 +2428,7 @@ public function test_get_post_context_without_permission() { } public function test_get_post_with_password() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_password' => '$inthebananastand', ) @@ -1957,7 +2447,7 @@ public function test_get_post_with_password() { } public function test_get_post_with_password_using_password() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_password' => '$inthebananastand', 'post_content' => 'Some secret content.', @@ -1981,7 +2471,7 @@ public function test_get_post_with_password_using_password() { } public function test_get_post_with_password_using_incorrect_password() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_password' => '$inthebananastand', ) @@ -1997,7 +2487,7 @@ public function test_get_post_with_password_using_incorrect_password() { } public function test_get_post_with_password_without_permission() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_password' => '$inthebananastand', 'post_content' => 'Some secret content.', @@ -2015,13 +2505,58 @@ public function test_get_post_with_password_without_permission() { $this->assertTrue( $data['excerpt']['protected'] ); } + /** + * @ticket 61837 + */ + public function test_get_item_permissions_check_while_updating_password() { + $endpoint = new WP_REST_Posts_Controller( 'post' ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_url_params( array( 'id' => self::$post_id ) ); + $request->set_body_params( + $this->set_post_data( + array( + 'id' => self::$post_id, + 'password' => '123', + ) + ) + ); + $permission = $endpoint->get_item_permissions_check( $request ); + + // Password provided in POST data, should not be used as authentication. + $this->assertNotWPError( $permission, 'Password in post body should be ignored by permissions check.' ); + $this->assertTrue( $permission ); + } + + /** + * @ticket 61837 + */ + public function test_get_item_permissions_check_while_updating_password_with_invalid_type() { + $endpoint = new WP_REST_Posts_Controller( 'post' ); + + $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $request->set_url_params( array( 'id' => self::$post_id ) ); + $request->set_body_params( + $this->set_post_data( + array( + 'id' => self::$post_id, + 'password' => 123, + ) + ) + ); + $permission = $endpoint->get_item_permissions_check( $request ); + + $this->assertNotWPError( $permission, 'Password in post body should be ignored by permissions check even when it is an invalid type.' ); + $this->assertTrue( $permission ); + } + /** * The post response should not have `block_version` when in view context. * * @ticket 43887 */ public function test_get_post_should_not_have_block_version_when_context_view() { - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_content' => '', ) @@ -2041,7 +2576,7 @@ public function test_get_post_should_not_have_block_version_when_context_view() public function test_get_post_should_have_block_version_indicate_block_content_when_context_edit() { wp_set_current_user( self::$editor_id ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_content' => '', ) @@ -2062,7 +2597,7 @@ public function test_get_post_should_have_block_version_indicate_block_content_w public function test_get_post_should_have_block_version_indicate_no_block_content_when_context_edit() { wp_set_current_user( self::$editor_id ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_content' => '
      ', ) @@ -2137,8 +2672,8 @@ public function test_prepare_item_limit_fields() { */ public function test_prepare_item_filters_content_when_needed() { $filter_count = 0; - $filter_content = static function() use ( &$filter_count ) { - $filter_count++; + $filter_content = static function () use ( &$filter_count ) { + ++$filter_count; return '

      Filtered content.

      '; }; add_filter( 'the_content', $filter_content ); @@ -2173,8 +2708,8 @@ public function test_prepare_item_filters_content_when_needed() { */ public function test_prepare_item_skips_content_filter_if_not_needed() { $filter_count = 0; - $filter_content = static function() use ( &$filter_count ) { - $filter_count++; + $filter_content = static function () use ( &$filter_count ) { + ++$filter_count; return '

      Filtered content.

      '; }; add_filter( 'the_content', $filter_content ); @@ -2204,6 +2739,72 @@ public function test_prepare_item_skips_content_filter_if_not_needed() { $this->assertSame( 0, $filter_count ); } + /** + * @ticket 59043 + * + * @covers WP_REST_Posts_Controller::prepare_item_for_response + */ + public function test_prepare_item_override_excerpt_length() { + wp_set_current_user( self::$editor_id ); + + $post_id = self::factory()->post->create( + array( + 'post_excerpt' => '', + 'post_content' => 'Bacon ipsum dolor amet porchetta capicola sirloin prosciutto brisket shankle jerky. Ham hock filet mignon boudin ground round, prosciutto alcatra spare ribs meatball turducken pork beef ribs ham beef. Bacon pastrami short loin, venison tri-tip ham short ribs doner swine. Tenderloin pig tongue pork jowl doner. Pork loin rump t-bone, beef strip steak flank drumstick tri-tip short loin capicola jowl. Cow filet mignon hamburger doner rump. Short loin jowl drumstick, tongue tail beef ribs pancetta flank brisket landjaeger chuck venison frankfurter turkey. + +Brisket shank rump, tongue beef ribs swine fatback turducken capicola meatball picanha chicken cupim meatloaf turkey. Bacon biltong shoulder tail frankfurter boudin cupim turkey drumstick. Porchetta pig shoulder, jerky flank pork tail meatball hamburger. Doner ham hock ribeye tail jerky swine. Leberkas ribeye pancetta, tenderloin capicola doner turducken chicken venison ground round boudin pork chop. Tail pork loin pig spare ribs, biltong ribeye brisket pork chop cupim. Short loin leberkas spare ribs jowl landjaeger tongue kevin flank bacon prosciutto. + +Shankle pork chop prosciutto ribeye ham hock pastrami. T-bone shank brisket bacon pork chop. Cupim hamburger pork loin short loin. Boudin ball tip cupim ground round ham shoulder. Sausage rump cow tongue bresaola pork pancetta biltong tail chicken turkey hamburger. Kevin flank pork loin salami biltong. Alcatra landjaeger pastrami andouille kielbasa ham tenderloin drumstick sausage turducken tongue corned beef.', + ) + ); + + $endpoint = new WP_REST_Posts_Controller( 'post' ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $post_id ) ); + $request->set_param( 'context', 'edit' ); + $request->set_param( '_fields', 'excerpt' ); + $request->set_param( 'excerpt_length', 43 ); + $response = $endpoint->prepare_item_for_response( get_post( $post_id ), $request ); + $data = $response->get_data(); + $this->assertArrayHasKey( 'excerpt', $data, 'Response must contain an "excerpt" key.' ); + + // 43 words plus the ellipsis added via the 'excerpt_more' filter. + $this->assertCount( + 44, + explode( ' ', $data['excerpt']['rendered'] ), + 'Incorrect word count in the excerpt. Expected the excerpt to contain 44 words (43 words plus an ellipsis), but a different word count was found.' + ); + } + + /** + * Test that the `class_list` property is a list. + * + * @ticket 64247 + * + * @covers WP_REST_Posts_Controller::prepare_item_for_response + */ + public function test_class_list_is_list() { + $post_id = self::factory()->post->create(); + + // Filter 'post_class' to add a duplicate which should be removed by `array_unique()`. + add_filter( + 'post_class', + function ( $classes ) { + return array_merge( + array( 'duplicate-class', 'duplicate-class' ), + $classes + ); + } + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); + $response = rest_do_request( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'class_list', $data ); + $this->assertContains( 'duplicate-class', $data['class_list'] ); + $this->assertTrue( array_is_list( $data['class_list'] ), 'Expected class_list to be a list.' ); + } + public function test_create_item() { wp_set_current_user( self::$editor_id ); @@ -2216,7 +2817,7 @@ public function test_create_item() { $this->check_create_post_response( $response ); } - public function post_dates_provider() { + public function data_post_dates() { $all_statuses = array( 'draft', 'publish', @@ -2287,7 +2888,7 @@ public function post_dates_provider() { } /** - * @dataProvider post_dates_provider + * @dataProvider data_post_dates */ public function test_create_post_date( $status, $params, $results ) { wp_set_current_user( self::$editor_id ); @@ -2687,44 +3288,151 @@ public function test_create_post_with_unsupported_format() { $this->assertSame( 'link', $data['format'] ); } - public function test_create_update_post_with_featured_media() { + public function test_create_update_post_with_featured_media() { + + $file = DIR_TESTDATA . '/images/canola.jpg'; + $attachment_id = self::factory()->attachment->create_object( + $file, + 0, + array( + 'post_mime_type' => 'image/jpeg', + 'menu_order' => 1, + ) + ); + + $this->attachments_created = true; + + wp_set_current_user( self::$editor_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/posts' ); + $params = $this->set_post_data( + array( + 'featured_media' => $attachment_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $new_post = get_post( $data['id'] ); + $this->assertSame( $attachment_id, $data['featured_media'] ); + $this->assertSame( $attachment_id, (int) get_post_thumbnail_id( $new_post->ID ) ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $new_post->ID ); + $params = $this->set_post_data( + array( + 'featured_media' => 0, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 0, $data['featured_media'] ); + $this->assertSame( 0, (int) get_post_thumbnail_id( $new_post->ID ) ); + } + + /** + * Data provider for featured media link permission tests. + * + * @return array + */ + public function data_featured_media_link_permissions() { + return array( + 'unauthenticated user with draft parent attachment' => array( + 'attachment_parent_status' => 'draft', + 'attachment_status' => 'inherit', + 'user_id' => 0, + 'expect_link' => false, + ), + 'authenticated editor with draft parent attachment' => array( + 'attachment_parent_status' => 'draft', + 'attachment_status' => 'inherit', + 'user_id' => 'editor', + 'expect_link' => true, + ), + 'unauthenticated user with published attachment' => array( + 'attachment_parent_status' => null, + 'attachment_status' => 'publish', + 'user_id' => 0, + 'expect_link' => true, + ), + ); + } + + /** + * Tests that featured media links respect attachment permissions. + * + * @ticket 64183 + * @dataProvider data_featured_media_link_permissions + * + * @param string|null $attachment_parent_status Status of the attachment's parent post, or null for no parent. + * @param string $attachment_status Status to set on the attachment. + * @param int|string $user_id User ID (0 for unauthenticated) or 'editor' for editor role. + * @param bool $expect_link Whether the featured media link should be included. + */ + public function test_get_item_featured_media_link_permissions( $attachment_parent_status, $attachment_status, $user_id, $expect_link ) { + $file = DIR_TESTDATA . '/images/canola.jpg'; + + // Create attachment parent if needed. + $parent_post_id = 0; + if ( null !== $attachment_parent_status ) { + $parent_post_id = self::factory()->post->create( + array( + 'post_title' => 'Parent Post', + 'post_status' => $attachment_parent_status, + ) + ); + } - $file = DIR_TESTDATA . '/images/canola.jpg'; - $this->attachment_id = $this->factory->attachment->create_object( + // Create attachment. + $attachment_id = self::factory()->attachment->create_object( $file, - 0, + $parent_post_id, array( 'post_mime_type' => 'image/jpeg', - 'menu_order' => rand( 1, 100 ), ) ); - wp_set_current_user( self::$editor_id ); + // Set attachment status if different from default. + if ( 'publish' === $attachment_status ) { + wp_update_post( + array( + 'ID' => $attachment_id, + 'post_status' => 'publish', + ) + ); + } - $request = new WP_REST_Request( 'POST', '/wp/v2/posts' ); - $params = $this->set_post_data( + // Create published post with featured media. + $published_post_id = self::factory()->post->create( array( - 'featured_media' => $this->attachment_id, + 'post_title' => 'Published Post', + 'post_status' => 'publish', ) ); - $request->set_body_params( $params ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $new_post = get_post( $data['id'] ); - $this->assertSame( $this->attachment_id, $data['featured_media'] ); - $this->assertSame( $this->attachment_id, (int) get_post_thumbnail_id( $new_post->ID ) ); + set_post_thumbnail( $published_post_id, $attachment_id ); - $request = new WP_REST_Request( 'POST', '/wp/v2/posts/' . $new_post->ID ); - $params = $this->set_post_data( - array( - 'featured_media' => 0, - ) - ); - $request->set_body_params( $params ); + // Set current user. + if ( 'editor' === $user_id ) { + wp_set_current_user( self::$editor_id ); + } else { + wp_set_current_user( $user_id ); + } + + // Make request. + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/posts/%d', $published_post_id ) ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 0, $data['featured_media'] ); - $this->assertSame( 0, (int) get_post_thumbnail_id( $new_post->ID ) ); + $links = $response->get_links(); + + // Assert link presence based on expectation. + if ( $expect_link ) { + $this->assertArrayHasKey( 'https://api.w.org/featuredmedia', $links ); + $this->assertSame( + rest_url( '/wp/v2/media/' . $attachment_id ), + $links['https://api.w.org/featuredmedia'][0]['href'] + ); + } else { + $this->assertArrayNotHasKey( 'https://api.w.org/featuredmedia', $links ); + } } public function test_create_post_invalid_author() { @@ -3092,7 +3800,7 @@ public function test_rest_update_post() { */ public function test_rest_update_post_with_empty_date() { // Create a new test post. - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_current_user( self::$editor_id ); @@ -3308,7 +4016,7 @@ public function test_update_post_with_unsupported_format() { public function test_update_post_ignore_readonly() { wp_set_current_user( self::$editor_id ); - $new_content = rand_str(); + $new_content = 'foo bar baz'; $expected_modified = current_time( 'mysql' ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); @@ -3334,14 +4042,14 @@ public function test_update_post_ignore_readonly() { } /** - * @dataProvider post_dates_provider + * @dataProvider data_post_dates */ public function test_update_post_date( $status, $params, $results ) { wp_set_current_user( self::$editor_id ); update_option( 'timezone_string', $params['timezone_string'] ); - $post_id = $this->factory->post->create( array( 'post_status' => $status ) ); + $post_id = self::factory()->post->create( array( 'post_status' => $status ) ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', $post_id ) ); if ( isset( $params['date'] ) ) { @@ -3373,7 +4081,7 @@ public function test_update_post_with_invalid_date() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); $params = $this->set_post_data( array( - 'date' => rand_str(), + 'date' => 'foo', ) ); $request->set_body_params( $params ); @@ -3388,7 +4096,7 @@ public function test_update_post_with_invalid_date_gmt() { $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); $params = $this->set_post_data( array( - 'date_gmt' => rand_str(), + 'date_gmt' => 'foo', ) ); $request->set_body_params( $params ); @@ -3406,7 +4114,7 @@ public function test_empty_post_date_gmt_shimmed_using_post_date() { // Need to set dates using wpdb directly because `wp_update_post` and // `wp_insert_post` have additional validation on dates. - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); $wpdb->update( $wpdb->posts, array( @@ -3889,7 +4597,17 @@ public function verify_post_roundtrip( $input = array(), $expected_output = arra $this->assertSame( $expected_output['excerpt']['raw'], $post->post_excerpt ); } - public static function post_roundtrip_provider() { + /** + * @dataProvider data_post_roundtrip_as_author + */ + public function test_post_roundtrip_as_author( $raw, $expected ) { + wp_set_current_user( self::$author_id ); + + $this->assertFalse( current_user_can( 'unfiltered_html' ) ); + $this->verify_post_roundtrip( $raw, $expected ); + } + + public static function data_post_roundtrip_as_author() { return array( array( // Raw values. @@ -3974,28 +4692,18 @@ public static function post_roundtrip_provider() { 'rendered' => 'link', ), 'content' => array( - 'raw' => 'link', - 'rendered' => '

      link

      ', + 'raw' => 'link', + 'rendered' => '

      link

      ', ), 'excerpt' => array( - 'raw' => 'link', - 'rendered' => '

      link

      ', + 'raw' => 'link', + 'rendered' => '

      link

      ', ), ), ), ); } - /** - * @dataProvider post_roundtrip_provider - */ - public function test_post_roundtrip_as_author( $raw, $expected ) { - wp_set_current_user( self::$author_id ); - - $this->assertFalse( current_user_can( 'unfiltered_html' ) ); - $this->verify_post_roundtrip( $raw, $expected ); - } - public function test_post_roundtrip_as_editor_unfiltered_html() { wp_set_current_user( self::$editor_id ); @@ -4076,7 +4784,7 @@ public function test_post_roundtrip_as_superadmin_unfiltered_html() { } public function test_delete_item() { - $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) ); + $post_id = self::factory()->post->create( array( 'post_title' => 'Deleted post' ) ); wp_set_current_user( self::$editor_id ); @@ -4091,7 +4799,7 @@ public function test_delete_item() { } public function test_delete_item_skip_trash() { - $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) ); + $post_id = self::factory()->post->create( array( 'post_title' => 'Deleted post' ) ); wp_set_current_user( self::$editor_id ); @@ -4106,7 +4814,7 @@ public function test_delete_item_skip_trash() { } public function test_delete_item_already_trashed() { - $post_id = $this->factory->post->create( array( 'post_title' => 'Deleted post' ) ); + $post_id = self::factory()->post->create( array( 'post_title' => 'Deleted post' ) ); wp_set_current_user( self::$editor_id ); @@ -4127,7 +4835,7 @@ public function test_delete_post_invalid_id() { } public function test_delete_post_invalid_post_type() { - $page_id = $this->factory->post->create( array( 'post_type' => 'page' ) ); + $page_id = self::factory()->post->create( array( 'post_type' => 'page' ) ); wp_set_current_user( self::$editor_id ); @@ -4159,7 +4867,6 @@ public function test_register_post_type_invalid_controller() { $routes = rest_get_server()->get_routes(); $this->assertArrayNotHasKey( '/wp/v2/invalid-controller', $routes ); _unregister_post_type( 'invalid-controller' ); - } public function test_get_item_schema() { @@ -4167,7 +4874,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 26, $properties ); + $this->assertCount( 27, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'comment_status', $properties ); $this->assertArrayHasKey( 'content', $properties ); @@ -4194,6 +4901,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'type', $properties ); $this->assertArrayHasKey( 'tags', $properties ); $this->assertArrayHasKey( 'categories', $properties ); + $this->assertArrayHasKey( 'class_list', $properties ); } /** @@ -4223,6 +4931,7 @@ public function test_get_post_view_context_properties() { $expected_keys = array( 'author', 'categories', + 'class_list', 'comment_status', 'content', 'date', @@ -4261,6 +4970,7 @@ public function test_get_post_edit_context_properties() { $expected_keys = array( 'author', 'categories', + 'class_list', 'comment_status', 'content', 'date', @@ -4370,7 +5080,7 @@ public function test_get_additional_field_registration() { wp_set_current_user( 1 ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); $response = rest_get_server()->dispatch( $request ); @@ -4415,7 +5125,7 @@ public function test_get_additional_field_registration_null_schema() { 'update_callback' => null, ) ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); // 'my_custom_int' should appear because ?_fields= isn't set. $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . $post_id ); @@ -4474,15 +5184,15 @@ public function test_additional_field_update_errors() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { - return get_post_meta( $object['id'], 'my_custom_int', true ); + public function additional_field_get_callback( $response_data, $field_name ) { + return get_post_meta( $response_data['id'], $field_name, true ); } - public function additional_field_update_callback( $value, $post ) { + public function additional_field_update_callback( $value, $post, $field_name ) { if ( 'returnError' === $value ) { return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); } - update_post_meta( $post->ID, 'my_custom_int', $value ); + update_post_meta( $post->ID, $field_name, $value ); } public function test_publish_action_ldo_registered() { @@ -4836,7 +5546,7 @@ public function test_generated_permalink_template_generated_slug_for_non_viewabl wp_set_current_user( self::$editor_id ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_title' => 'Permalink Template', 'post_type' => 'private-post', @@ -4860,7 +5570,7 @@ public function test_generated_permalink_template_generated_slug_for_posts() { wp_set_current_user( self::$editor_id ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_title' => 'Permalink Template', 'post_type' => 'post', @@ -4885,7 +5595,6 @@ public function test_generated_permalink_template_generated_slug_for_posts() { $this->assertSame( 200, $response->get_status() ); $this->assertArrayNotHasKey( 'permalink_template', $data ); $this->assertArrayNotHasKey( 'generated_slug', $data ); - } /** @@ -5154,12 +5863,343 @@ public function test_rest_post_type_item_schema_filter_add_property_triggers_doi $GLOBALS['wp_rest_server']->override_by_default = false; } - public function tear_down() { - if ( isset( $this->attachment_id ) ) { - $this->remove_added_uploads(); + /** + * @ticket 52422 + * + * @covers WP_REST_Posts_Controller::create_item + */ + public function test_draft_post_does_not_have_the_same_slug_as_existing_post() { + wp_set_current_user( self::$editor_id ); + self::factory()->post->create( array( 'post_name' => 'sample-slug' ) ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/posts/%d', self::$post_id ) ); + $params = $this->set_post_data( + array( + 'status' => 'draft', + 'slug' => 'sample-slug', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $new_data = $response->get_data(); + $this->assertSame( + 'sample-slug-2', + $new_data['slug'], + 'The slug from the REST response did not match' + ); + + $post = get_post( $new_data['id'] ); + + $this->assertSame( + 'draft', + $post->post_status, + 'The post status is not draft' + ); + + $this->assertSame( + 'sample-slug-2', + $post->post_name, + 'The post slug was not set to "sample-slug-2"' + ); + } + + /** + * Test the REST API ignores the post format parameter for post types that do not support them. + * + * @ticket 62646 + * @ticket 62014 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_standard_post_format_ignored_for_post_types_that_do_not_support_them() { + $initial_theme_support = get_theme_support( 'post-formats' ); + add_theme_support( 'post-formats', array( 'aside', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio', 'chat' ) ); + + self::factory()->post->create( + array( + 'post_type' => 'page', + 'post_status' => 'publish', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/pages' ); + $request->set_param( 'format', 'invalid_type' ); + + $response = rest_get_server()->dispatch( $request ); + + /* + * Restore the initial post formats support. + * + * This needs to be done prior to the assertions to avoid unexpected + * results for other tests should an assertion fail. + */ + if ( $initial_theme_support ) { + add_theme_support( 'post-formats', $initial_theme_support[0] ); + } else { + remove_theme_support( 'post-formats' ); } - parent::tear_down(); + $this->assertCount( 1, $response->get_data(), 'The response should ignore the post format parameter' ); + } + + /** + * Test the REST API support for the standard post format. + * + * @ticket 62014 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_standard_post_format_support() { + $initial_theme_support = get_theme_support( 'post-formats' ); + add_theme_support( 'post-formats', array( 'aside', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio', 'chat' ) ); + + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + set_post_format( $post_id, 'aside' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'format', array( 'standard' ) ); + $request->set_param( 'per_page', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); + + $response = rest_get_server()->dispatch( $request ); + + /* + * Restore the initial post formats support. + * + * This needs to be done prior to the assertions to avoid unexpected + * results for other tests should an assertion fail. + */ + if ( $initial_theme_support ) { + add_theme_support( 'post-formats', $initial_theme_support[0] ); + } else { + remove_theme_support( 'post-formats' ); + } + + $this->assertCount( 3, $response->get_data(), 'The response should only include standard post formats' ); + } + + /** + * Test the REST API support for post formats. + * + * @ticket 62014 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_post_format_support() { + $initial_theme_support = get_theme_support( 'post-formats' ); + add_theme_support( 'post-formats', array( 'aside', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio', 'chat' ) ); + + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + set_post_format( $post_id, 'aside' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'format', array( 'aside' ) ); + + $response_aside = rest_get_server()->dispatch( $request ); + + $request->set_param( 'format', array( 'invalid_format' ) ); + $response_invalid = rest_get_server()->dispatch( $request ); + + /* + * Restore the initial post formats support. + * + * This needs to be done prior to the assertions to avoid unexpected + * results for other tests should an assertion fail. + */ + if ( $initial_theme_support ) { + add_theme_support( 'post-formats', $initial_theme_support[0] ); + } else { + remove_theme_support( 'post-formats' ); + } + + $this->assertCount( 1, $response_aside->get_data(), 'Only one post is expected to be returned.' ); + $this->assertErrorResponse( 'rest_invalid_param', $response_invalid, 400, 'An invalid post format should return an error' ); + } + + /** + * Test the REST API support for multiple post formats. + * + * @ticket 62014 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_multiple_post_format_support() { + $initial_theme_support = get_theme_support( 'post-formats' ); + add_theme_support( 'post-formats', array( 'aside', 'gallery', 'link', 'image', 'quote', 'status', 'video', 'audio', 'chat' ) ); + + $post_id = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + set_post_format( $post_id, 'aside' ); + + $post_id_2 = self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_status' => 'publish', + ) + ); + set_post_format( $post_id_2, 'gallery' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'format', array( 'aside', 'gallery' ) ); + + $response = rest_get_server()->dispatch( $request ); + + /* + * Restore the initial post formats support. + * + * This needs to be done prior to the assertions to avoid unexpected + * results for other tests should an assertion fail. + */ + if ( $initial_theme_support ) { + add_theme_support( 'post-formats', $initial_theme_support[0] ); + } else { + remove_theme_support( 'post-formats' ); + } + + $this->assertCount( 2, $response->get_data(), 'Two posts are expected to be returned' ); + } + + /** + * Tests for the pagination. + * + * @ticket 62292 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_get_posts_with_pagination() { + + // Test offset. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'offset', 1 ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( 30, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 30, $response->get_headers()['X-WP-TotalPages'] ); + + // Test paged. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'page', 2 ); + $request->set_param( 'per_page', 2 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 2, $data ); + $this->assertSame( 30, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 15, $response->get_headers()['X-WP-TotalPages'] ); + + // Test out of bounds. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'page', 4 ); + $request->set_param( 'per_page', 10 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_page_number', $response, 400 ); + } + + /** + * Test the REST API doesn't prioritize sticky posts by default. + * + * @ticket 35907 + * @ticket 63307 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_get_posts_ignore_sticky_by_default() { + $id1 = self::$post_id; + // Create more recent post to avoid automatically placing other at the top. + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + update_option( 'sticky_posts', array( $id1 ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $rest_ids = wp_list_pluck( $data, 'id' ); + + $this->assertSame( $data[0]['id'], $id2, 'Response has no sticky post at the top.' ); + + $posts_query = new WP_Query( array( 'ignore_sticky_posts' => true ) ); + $post_ids = wp_list_pluck( $posts_query->get_posts(), 'ID' ); + $this->assertSame( $rest_ids, $post_ids, 'Response is same as WP_Query with ignore_sticky_posts=true.' ); + } + + /** + * Test the REST API support for `ignore_sticky_posts`. + * + * @ticket 35907 + * @ticket 63307 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_get_posts_ignore_sticky_false_prepends_sticky_posts() { + $id1 = self::$post_id; + // Create more recent post to avoid automatically placing other at the top. + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + update_option( 'sticky_posts', array( $id1 ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'ignore_sticky', false ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $rest_ids = wp_list_pluck( $data, 'id' ); + + $this->assertSame( $data[0]['id'], $id1, 'Response has sticky post at the top.' ); + $this->assertSame( $data[1]['id'], $id2, 'It is followed by most recent post.' ); + + $posts_query = new WP_Query(); + $post_ids = wp_list_pluck( $posts_query->get_posts(), 'ID' ); + $this->assertSame( $rest_ids, $post_ids, 'Response is same as WP_Query with ignore_sticky_posts=false.' ); + } + + /** + * Test the REST API support for `ignore_sticky_posts`. + * + * @ticket 35907 + * @ticket 63307 + * + * @covers WP_REST_Posts_Controller::get_items + */ + public function test_get_posts_ignore_sticky_honors_include() { + + $id1 = self::$post_id; + $id2 = self::factory()->post->create( array( 'post_status' => 'publish' ) ); + + update_option( 'sticky_posts', array( $id1 ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/posts' ); + $request->set_param( 'include', array( $id2 ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $rest_ids = wp_list_pluck( $data, 'id' ); + + $this->assertCount( 1, $data, 'Only one post is expected to be returned.' ); + $this->assertSame( $data[0]['id'], $id2, 'Returns the included post.' ); + + $posts_query = new WP_Query( + array( + 'post__in' => array( $id2 ), + 'ignore_sticky_posts' => true, + ) + ); + $post_ids = wp_list_pluck( $posts_query->get_posts(), 'ID' ); + $this->assertSame( $rest_ids, $post_ids, 'Response is same as WP_Query with ignore_sticky_posts=truehas no sticky post at the top.' ); } /** diff --git a/tests/phpunit/tests/rest-api/rest-request-validation.php b/tests/phpunit/tests/rest-api/rest-request-validation.php index aab01f2902e5a..3d81536099494 100644 --- a/tests/phpunit/tests/rest-api/rest-request-validation.php +++ b/tests/phpunit/tests/rest-api/rest-request-validation.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Request_Validation extends WP_Test_REST_TestCase { @@ -204,5 +202,4 @@ public function test_validate_less_than_max_exclusive() { $ret = rest_validate_request_arg( 9, $request, 'lessthanmax' ); $this->assertTrue( $ret ); } - } diff --git a/tests/phpunit/tests/rest-api/rest-request.php b/tests/phpunit/tests/rest-api/rest-request.php index 836cb6159dbfe..d689f9d18d4d0 100644 --- a/tests/phpunit/tests/rest-api/rest-request.php +++ b/tests/phpunit/tests/rest-api/rest-request.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class Tests_REST_Request extends WP_UnitTestCase { @@ -18,6 +16,16 @@ public function set_up() { $this->request = new WP_REST_Request(); } + /** + * Called before setting up all tests. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + // Require files that need to load once. + require_once DIR_TESTROOT . '/includes/mock-invokable.php'; + } + public function test_header() { $value = 'application/x-wp-example'; @@ -49,19 +57,8 @@ public function test_header_multiple() { $this->assertSame( array( $value1, $value2 ), $this->request->get_header_as_array( 'Accept' ) ); } - public static function header_provider() { - return array( - array( 'Test', 'test' ), - array( 'TEST', 'test' ), - array( 'Test-Header', 'test_header' ), - array( 'test-header', 'test_header' ), - array( 'Test_Header', 'test_header' ), - array( 'test_header', 'test_header' ), - ); - } - /** - * @dataProvider header_provider + * @dataProvider data_header_canonicalization * @param string $original Original header key. * @param string $expected Expected canonicalized version. */ @@ -69,19 +66,19 @@ public function test_header_canonicalization( $original, $expected ) { $this->assertSame( $expected, $this->request->canonicalize_header_name( $original ) ); } - public static function content_type_provider() { + public static function data_header_canonicalization() { return array( - // Check basic parsing. - array( 'application/x-wp-example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), - array( 'application/x-wp-example; charset=utf-8', 'application/x-wp-example', 'application', 'x-wp-example', 'charset=utf-8' ), - - // Check case insensitivity. - array( 'APPLICATION/x-WP-Example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), + array( 'Test', 'test' ), + array( 'TEST', 'test' ), + array( 'Test-Header', 'test_header' ), + array( 'test-header', 'test_header' ), + array( 'Test_Header', 'test_header' ), + array( 'test_header', 'test_header' ), ); } /** - * @dataProvider content_type_provider + * @dataProvider data_content_type_parsing * * @param string $header Header value. * @param string $value Full type value. @@ -102,6 +99,17 @@ public function test_content_type_parsing( $header, $value, $type, $subtype, $pa $this->assertSame( $parameters, $parsed['parameters'] ); } + public static function data_content_type_parsing() { + return array( + // Check basic parsing. + array( 'application/x-wp-example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), + array( 'application/x-wp-example; charset=utf-8', 'application/x-wp-example', 'application', 'x-wp-example', 'charset=utf-8' ), + + // Check case insensitivity. + array( 'APPLICATION/x-WP-Example', 'application/x-wp-example', 'application', 'x-wp-example', '' ), + ); + } + protected function request_with_parameters() { $this->request->set_url_params( array( @@ -178,22 +186,11 @@ public function test_parameter_order_post() { $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); } - public static function alternate_json_content_type_provider() { - return array( - array( 'application/ld+json', 'json', true ), - array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'json', true ), - array( 'application/activity+json', 'json', true ), - array( 'application/json+oembed', 'json', true ), - array( 'application/nojson', 'body', false ), - array( 'application/no.json', 'body', false ), - ); - } - /** * @ticket 49404 - * @dataProvider alternate_json_content_type_provider + * @dataProvider data_alternate_json_content_type * - * @param string $content_type The content-type header. + * @param string $content_type The Content-Type header. * @param string $source The source value. * @param bool $accept_json The accept_json value. */ @@ -209,22 +206,22 @@ public function test_alternate_json_content_type( $content_type, $source, $accep $this->assertEquals( $accept_json, $this->request->get_param( 'has_json_params' ) ); } - public static function is_json_content_type_provider() { + public static function data_alternate_json_content_type() { return array( - array( 'application/ld+json', true ), - array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', true ), - array( 'application/activity+json', true ), - array( 'application/json+oembed', true ), - array( 'application/nojson', false ), - array( 'application/no.json', false ), + array( 'application/ld+json', 'json', true ), + array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', 'json', true ), + array( 'application/activity+json', 'json', true ), + array( 'application/json+oembed', 'json', true ), + array( 'application/nojson', 'body', false ), + array( 'application/no.json', 'body', false ), ); } /** * @ticket 49404 - * @dataProvider is_json_content_type_provider + * @dataProvider data_is_json_content_type * - * @param string $content_type The content-type header. + * @param string $content_type The Content-Type header. * @param bool $is_json The is_json value. */ public function test_is_json_content_type( $content_type, $is_json ) { @@ -232,10 +229,21 @@ public function test_is_json_content_type( $content_type, $is_json ) { $this->request->set_header( 'Content-Type', $content_type ); - // Check for JSON content-type. + // Check for JSON Content-Type. $this->assertSame( $is_json, $this->request->is_json_content_type() ); } + public static function data_is_json_content_type() { + return array( + array( 'application/ld+json', true ), + array( 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', true ), + array( 'application/activity+json', true ), + array( 'application/json+oembed', true ), + array( 'application/nojson', false ), + array( 'application/no.json', false ), + ); + } + /** * @ticket 49404 */ @@ -302,20 +310,12 @@ public function test_parameter_order_json_invalid() { $this->assertEmpty( $this->request->get_param( 'has_json_params' ) ); } - public function non_post_http_methods_with_request_body_provider() { - return array( - array( 'PUT' ), - array( 'PATCH' ), - array( 'DELETE' ), - ); - } - /** * Tests that methods supporting request bodies have access to the * request's body. For POST this is straightforward via `$_POST`; for * other methods `WP_REST_Request` needs to parse the body for us. * - * @dataProvider non_post_http_methods_with_request_body_provider + * @dataProvider data_non_post_body_parameters */ public function test_non_post_body_parameters( $request_method ) { $data = array( @@ -337,6 +337,14 @@ public function test_non_post_body_parameters( $request_method ) { } } + public function data_non_post_body_parameters() { + return array( + array( 'PUT' ), + array( 'PATCH' ), + array( 'DELETE' ), + ); + } + public function test_parameters_for_json_put() { $data = array( 'foo' => 'bar', @@ -351,7 +359,7 @@ public function test_parameters_for_json_put() { ); $this->request->set_method( 'PUT' ); - $this->request->add_header( 'content-type', 'application/json' ); + $this->request->add_header( 'Content-Type', 'application/json' ); $this->request->set_body( wp_json_encode( $data ) ); foreach ( $data as $key => $expected_value ) { @@ -373,7 +381,7 @@ public function test_parameters_for_json_post() { ); $this->request->set_method( 'POST' ); - $this->request->add_header( 'content-type', 'application/json' ); + $this->request->add_header( 'Content-Type', 'application/json' ); $this->request->set_body( wp_json_encode( $data ) ); foreach ( $data as $key => $expected_value ) { @@ -854,7 +862,7 @@ public function test_set_param() { public function test_set_param_follows_parameter_order() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( wp_json_encode( @@ -882,7 +890,7 @@ public function test_set_param_follows_parameter_order() { */ public function test_set_param_updates_param_in_json_and_query() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( wp_json_encode( @@ -909,7 +917,7 @@ public function test_set_param_updates_param_in_json_and_query() { */ public function test_set_param_updates_param_if_already_exists_in_query() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( wp_json_encode( @@ -943,7 +951,7 @@ public function test_set_param_updates_param_if_already_exists_in_query() { */ public function test_set_param_to_null_updates_param_in_json_and_query() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( wp_json_encode( @@ -970,7 +978,7 @@ public function test_set_param_to_null_updates_param_in_json_and_query() { */ public function test_set_param_from_null_updates_param_in_json_and_query_with_null() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( wp_json_encode( @@ -997,7 +1005,7 @@ public function test_set_param_from_null_updates_param_in_json_and_query_with_nu */ public function test_set_param_with_invalid_json() { $request = new WP_REST_Request(); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_method( 'POST' ); $request->set_body( '' ); $request->set_param( 'param', 'value' ); @@ -1014,7 +1022,7 @@ public function test_route_level_validate_callback() { $request->set_query_params( array( 'test' => 'value' ) ); $error = new WP_Error( 'error_code', __( 'Error Message' ), array( 'status' => 400 ) ); - $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback = $this->createPartialMock( 'Mock_Invokable', array( '__invoke' ) ); $callback->expects( self::once() )->method( '__invoke' )->with( self::identicalTo( $request ) )->willReturn( $error ); $request->set_attributes( array( @@ -1038,7 +1046,7 @@ public function test_route_level_validate_callback_no_parameter_callbacks() { $request->set_query_params( array( 'test' => 'value' ) ); $error = new WP_Error( 'error_code', __( 'Error Message' ), array( 'status' => 400 ) ); - $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback = $this->createPartialMock( 'Mock_Invokable', array( '__invoke' ) ); $callback->expects( self::once() )->method( '__invoke' )->with( self::identicalTo( $request ) )->willReturn( $error ); $request->set_attributes( array( @@ -1056,7 +1064,7 @@ public function test_route_level_validate_callback_is_not_executed_if_parameter_ $request = new WP_REST_Request(); $request->set_query_params( array( 'test' => 'value' ) ); - $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback = $this->createPartialMock( 'Mock_Invokable', array( '__invoke' ) ); $callback->expects( self::never() )->method( '__invoke' ); $request->set_attributes( array( @@ -1073,4 +1081,82 @@ public function test_route_level_validate_callback_is_not_executed_if_parameter_ $this->assertWPError( $valid ); $this->assertSame( 'rest_invalid_param', $valid->get_error_code() ); } + + /** + * Tests that WP_REST_Request::is_method() correctly detects the request method, + * regardless of case sensitivity. + * + * @dataProvider data_is_method_should_detect_method_ignoring_case + * @ticket 56481 + * + * @param string $method The expected HTTP method of the request. + * @param string $input_method The HTTP method to check against. + * @param bool $expected The expected result of the is_method() check. + */ + public function test_is_method_should_detect_method_ignoring_case( $method, $input_method, $expected ) { + $request = new WP_REST_Request(); + $request->set_method( $method ); + $result = $request->is_method( $input_method ); + + $this->assertSame( $expected, $result, 'Failed asserting that the WP_REST_Request::is_method() method correctly detects the request method.' ); + } + + /** + * Provides test cases for verifying HTTP method comparison is case-insensitive. + * + * @return array + */ + public function data_is_method_should_detect_method_ignoring_case() { + return array( + // GET. + 'GET same case' => array( 'GET', 'GET', true ), + 'GET different case' => array( 'GET', 'get', true ), + 'GET different case #2' => array( 'GET', 'get', true ), + 'GET different case #3' => array( 'GET', 'gEt', true ), + 'GET wrong method' => array( 'GET', 'POST', false ), + // POST. + 'POST same case' => array( 'POST', 'POST', true ), + 'POST different case' => array( 'POST', 'post', true ), + 'POST different case #2' => array( 'POST', 'pOsT', true ), + 'POST wrong method' => array( 'POST', 'GET', false ), + // HEAD. + 'HEAD same case' => array( 'HEAD', 'HEAD', true ), + 'HEAD different case' => array( 'HEAD', 'head', true ), + 'HEAD different case #2' => array( 'HEAD', 'HeAd', true ), + 'HEAD wrong method' => array( 'HEAD', 'GET', false ), + ); + } + + /** + * @ticket 62163 + */ + public function test_get_params_without_pretty_permalink() { + update_option( 'permalink_structure', '' ); + + $request = new WP_REST_Request(); + $request->set_param( 'rest_route', '/wp/v2/posts' ); + $request->set_param( 'some_param', 'foobar' ); + + $params = $request->get_params(); + + $this->assertArrayNotHasKey( 'rest_route', $params ); + $this->assertArrayHasKey( 'some_param', $params ); + } + + /** + * @ticket 62163 + */ + public function test_get_params_with_pretty_permalinks() { + update_option( 'permalink_structure', '/%postname%/' ); + + $request = new WP_REST_Request(); + + $request->set_param( 'rest_route', '/wp/v2/posts' ); + $request->set_param( 'some_param', 'foobar' ); + + $params = $request->get_params(); + + $this->assertArrayHasKey( 'rest_route', $params ); + $this->assertArrayHasKey( 'some_param', $params ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-revisions-controller.php b/tests/phpunit/tests/rest-api/rest-revisions-controller.php index 37a529f7630b6..52011afcb9318 100644 --- a/tests/phpunit/tests/rest-api/rest-revisions-controller.php +++ b/tests/phpunit/tests/rest-api/rest-revisions-controller.php @@ -4,21 +4,31 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Revisions_Controller extends WP_Test_REST_Controller_Testcase { protected static $post_id; + protected static $post_id_2; protected static $page_id; protected static $editor_id; protected static $contributor_id; + private $total_revisions; + private $revisions; + private $revision_1; + private $revision_id1; + private $revision_2; + private $revision_id2; + private $revision_3; + private $revision_id3; + private $revision_2_1_id; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$post_id = $factory->post->create(); - self::$page_id = $factory->post->create( array( 'post_type' => 'page' ) ); + self::$post_id = $factory->post->create(); + self::$post_id_2 = $factory->post->create(); + self::$page_id = $factory->post->create( array( 'post_type' => 'page' ) ); self::$editor_id = $factory->user->create( array( @@ -50,12 +60,25 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { 'ID' => self::$post_id, ) ); + wp_update_post( + array( + 'post_content' => 'A second post.', + 'ID' => self::$post_id_2, + ) + ); + wp_update_post( + array( + 'post_content' => 'A second post. How prolific.', + 'ID' => self::$post_id_2, + ) + ); wp_set_current_user( 0 ); } public static function wpTearDownAfterClass() { // Also deletes revisions. wp_delete_post( self::$post_id, true ); + wp_delete_post( self::$post_id_2, true ); wp_delete_post( self::$page_id, true ); self::delete_user( self::$editor_id ); @@ -65,6 +88,7 @@ public static function wpTearDownAfterClass() { public function set_up() { parent::set_up(); + // Set first post revision vars. $revisions = wp_get_post_revisions( self::$post_id ); $this->total_revisions = count( $revisions ); $this->revisions = $revisions; @@ -74,6 +98,11 @@ public function set_up() { $this->revision_id2 = $this->revision_2->ID; $this->revision_3 = array_pop( $revisions ); $this->revision_id3 = $this->revision_3->ID; + + // Set second post revision vars. + $revisions = wp_get_post_revisions( self::$post_id_2 ); + $post_2_revision = array_pop( $revisions ); + $this->revision_2_1_id = $post_2_revision->ID; } public function _filter_map_meta_cap_remove_no_allow_revisions( $caps, $cap, $user_id, $args ) { @@ -133,9 +162,38 @@ public function test_get_items() { $this->check_get_revision_response( $data[2], $this->revision_1 ); } - public function test_get_items_no_permission() { + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_revisions_data() { + wp_set_current_user( self::$editor_id ); + + $hook_name = 'rest_prepare_revision'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); @@ -144,16 +202,40 @@ public function test_get_items_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_items_missing_parent() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_items_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } @@ -172,6 +254,7 @@ public function test_get_item() { 'modified_gmt', 'guid', 'id', + 'meta', 'parent', 'slug', 'title', @@ -183,6 +266,75 @@ public function test_get_item() { $this->assertSame( self::$editor_id, $data['author'] ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + + $hook_name = 'rest_prepare_revision'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'GET' === $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'HEAD', sprintf( $path, self::$post_id, $this->revision_id1 ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + + 'get_item request' => array( '/wp/v2/posts/%d/revisions/%d' ), + 'get_items request' => array( '/wp/v2/posts/%d/revisions' ), + ); + } + public function test_get_item_embed_context() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); @@ -201,9 +353,15 @@ public function test_get_item_embed_context() { $this->assertSameSets( $fields, array_keys( $data ) ); } - public function test_get_item_no_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_read', $response, 401 ); @@ -212,20 +370,61 @@ public function test_get_item_no_permission() { $this->assertErrorResponse( 'rest_cannot_read', $response, 403 ); } - public function test_get_item_missing_parent() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_missing_parent( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . REST_TESTS_IMPOSSIBLY_HIGH_NUMBER . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } - public function test_get_item_invalid_parent_post_type() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_parent_post_type( $method ) { wp_set_current_user( self::$editor_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$page_id . '/revisions/' . $this->revision_id1 ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_post_invalid_parent', $response, 404 ); } + /** + * @ticket 59875 + */ + public function test_get_item_valid_parent_id() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( self::$post_id, $data['parent'], "The returned revision's id should match the parent id." ); + $this->check_get_revision_response( $response, $this->revision_1 ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 59875 + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_invalid_parent_id( $method ) { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_2_1_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 ); + + $expected_message = 'The revision does not belong to the specified parent with id of "' . self::$post_id . '"'; + $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); + } + public function test_delete_item() { wp_set_current_user( self::$editor_id ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/posts/' . self::$post_id . '/revisions/' . $this->revision_id1 ); @@ -328,7 +527,7 @@ public function test_get_item_schema() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 12, $properties ); + $this->assertCount( 13, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'content', $properties ); $this->assertArrayHasKey( 'date', $properties ); @@ -341,6 +540,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'parent', $properties ); $this->assertArrayHasKey( 'slug', $properties ); $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); } public function test_create_item() { @@ -393,12 +593,12 @@ public function test_get_additional_field_registration() { $wp_rest_additional_fields = array(); } - public function additional_field_get_callback( $object ) { - return get_post_meta( $object['id'], 'my_custom_int', true ); + public function additional_field_get_callback( $response_data, $field_name ) { + return get_post_meta( $response_data['id'], $field_name, true ); } - public function additional_field_update_callback( $value, $post ) { - update_post_meta( $post->ID, 'my_custom_int', $value ); + public function additional_field_update_callback( $value, $post, $field_name ) { + update_post_meta( $post->ID, $field_name, $value ); } protected function check_get_revision_response( $response, $revision ) { @@ -454,9 +654,13 @@ public function test_get_item_sets_up_postdata() { /** * Test the pagination header of the first page. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_first_page() { + public function test_get_items_pagination_header_of_the_first_page( $method ) { wp_set_current_user( self::$editor_id ); $rest_route = '/wp/v2/posts/' . self::$post_id . '/revisions'; @@ -464,7 +668,7 @@ public function test_get_items_pagination_header_of_the_first_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 1; // First page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -489,9 +693,13 @@ public function test_get_items_pagination_header_of_the_first_page() { /** * Test the pagination header of the last page. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_pagination_header_of_the_last_page() { + public function test_get_items_pagination_header_of_the_last_page( $method ) { wp_set_current_user( self::$editor_id ); $rest_route = '/wp/v2/posts/' . self::$post_id . '/revisions'; @@ -499,7 +707,7 @@ public function test_get_items_pagination_header_of_the_last_page() { $total_pages = (int) ceil( $this->total_revisions / $per_page ); $page = 2; // Last page. - $request = new WP_REST_Request( 'GET', $rest_route ); + $request = new WP_REST_Request( $method, $rest_route ); $request->set_query_params( array( 'per_page' => $per_page, @@ -520,19 +728,24 @@ public function test_get_items_pagination_header_of_the_last_page() { $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); } + /** * Test that invalid 'per_page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_per_page_should_error() { + public function test_get_items_invalid_per_page_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = -1; // Invalid number. $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_param( 'per_page', $per_page ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( $expected_error, $response, $expected_status ); @@ -541,9 +754,13 @@ public function test_get_items_invalid_per_page_should_error() { /** * Test that out of bounds 'page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bounds_page_should_error() { + public function test_get_items_out_of_bounds_page_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -552,7 +769,7 @@ public function test_get_items_out_of_bounds_page_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -566,9 +783,13 @@ public function test_get_items_out_of_bounds_page_should_error() { /** * Test that impossibly high 'page' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_max_pages_should_error() { + public function test_get_items_invalid_max_pages_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -576,7 +797,7 @@ public function test_get_items_invalid_max_pages_should_error() { $expected_error = 'rest_revision_invalid_page_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'per_page' => $per_page, @@ -612,7 +833,7 @@ public function test_get_items_search_query() { * * @ticket 40510 */ - public function test_get_items_default_query_should_fetch_all_revisons() { + public function test_get_items_default_query_should_fetch_all_revisions() { wp_set_current_user( self::$editor_id ); $expected_count = $this->total_revisions; @@ -714,9 +935,13 @@ public function test_get_items_total_revisions_offset_should_return_empty_data() /** * Test that out of bound 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_out_of_bound_offset_should_error() { + public function test_get_items_out_of_bound_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -724,7 +949,7 @@ public function test_get_items_out_of_bound_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -738,9 +963,13 @@ public function test_get_items_out_of_bound_offset_should_error() { /** * Test that impossible high number for 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_impossible_high_number_offset_should_error() { + public function test_get_items_impossible_high_number_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -748,7 +977,7 @@ public function test_get_items_impossible_high_number_offset_should_error() { $expected_error = 'rest_revision_invalid_offset_number'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -762,9 +991,13 @@ public function test_get_items_impossible_high_number_offset_should_error() { /** * Test that invalid 'offset' query should error. * + * @dataProvider data_readable_http_methods * @ticket 40510 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_invalid_offset_should_error() { + public function test_get_items_invalid_offset_should_error( $method ) { wp_set_current_user( self::$editor_id ); $per_page = 2; @@ -772,7 +1005,7 @@ public function test_get_items_invalid_offset_should_error() { $expected_error = 'rest_invalid_param'; $expected_status = 400; - $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request = new WP_REST_Request( $method, '/wp/v2/posts/' . self::$post_id . '/revisions' ); $request->set_query_params( array( 'offset' => $offset, @@ -808,4 +1041,44 @@ public function test_get_items_out_of_bounds_page_should_not_error_if_offset() { $response = rest_get_server()->dispatch( $request ); $this->assertCount( $expected_count, $response->get_data() ); } + + /** + * Tests for the pagination. + * + * @ticket 62292 + * + * @covers WP_REST_Revisions_Controller::get_items + */ + public function test_get_revisions_pagination() { + wp_set_current_user( self::$editor_id ); + + // Test offset. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'offset', 1 ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( $this->total_revisions, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( $this->total_revisions, $response->get_headers()['X-WP-TotalPages'] ); + + // Test paged. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'page', 2 ); + $request->set_param( 'per_page', 2 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( $this->total_revisions, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( (int) ceil( $this->total_revisions / 2 ), $response->get_headers()['X-WP-TotalPages'] ); + + // Test out of bounds. + $request = new WP_REST_Request( 'GET', '/wp/v2/posts/' . self::$post_id . '/revisions' ); + $request->set_param( 'page', $this->total_revisions + 1 ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_revision_invalid_page_number', $response, 400 ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php index 7b3c076c6ec98..2cea494c064ea 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-sanitization.php +++ b/tests/phpunit/tests/rest-api/rest-schema-sanitization.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Schema_Sanitization extends WP_UnitTestCase { @@ -15,11 +13,11 @@ public function test_type_number() { $schema = array( 'type' => 'number', ); - $this->assertEquals( 1, rest_sanitize_value_from_schema( 1, $schema ) ); + $this->assertSame( 1.0, rest_sanitize_value_from_schema( 1, $schema ) ); $this->assertSame( 1.10, rest_sanitize_value_from_schema( '1.10', $schema ) ); - $this->assertEquals( 1, rest_sanitize_value_from_schema( '1abc', $schema ) ); - $this->assertEquals( 0, rest_sanitize_value_from_schema( 'abc', $schema ) ); - $this->assertEquals( 0, rest_sanitize_value_from_schema( array(), $schema ) ); + $this->assertSame( 1.0, rest_sanitize_value_from_schema( '1abc', $schema ) ); + $this->assertSame( 0.0, rest_sanitize_value_from_schema( 'abc', $schema ) ); + $this->assertSame( 0.0, rest_sanitize_value_from_schema( array(), $schema ) ); } public function test_type_integer() { diff --git a/tests/phpunit/tests/rest-api/rest-schema-setup.php b/tests/phpunit/tests/rest-api/rest-schema-setup.php index 8b522ef06bafd..9c6c431e5ef35 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-setup.php +++ b/tests/phpunit/tests/rest-api/rest-schema-setup.php @@ -6,9 +6,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi * @group restapi-jsclient */ @@ -20,7 +18,7 @@ public function set_up() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); add_filter( 'pre_http_request', array( $this, 'mock_embed_request' ), 10, 3 ); @@ -34,8 +32,8 @@ public function tear_down() { parent::tear_down(); } - public function mock_embed_request( $preempt, $r, $url ) { - unset( $preempt, $r ); + public function mock_embed_request( $response, $parsed_args, $url ) { + unset( $response, $parsed_args ); // Mock request to YouTube Embed. if ( false !== strpos( $url, self::YOUTUBE_VIDEO_ID ) ) { @@ -69,6 +67,9 @@ public function mock_embed_request( $preempt, $r, $url ) { } } + /** + * @ticket 54596 + */ public function test_expected_routes_in_schema() { $routes = rest_get_server()->get_routes(); @@ -89,6 +90,14 @@ public function test_expected_routes_in_schema() { '/wp/v2/posts/(?P[\\d]+)/revisions/(?P[\\d]+)', '/wp/v2/posts/(?P[\\d]+)/autosaves', '/wp/v2/posts/(?P[\\d]+)/autosaves/(?P[\\d]+)', + '/wp/v2/menu-items', + '/wp/v2/menu-items/(?P[\d]+)', + '/wp/v2/menu-items/(?P[\d]+)/autosaves', + '/wp/v2/menu-items/(?P[\d]+)/autosaves/(?P[\d]+)', + '/wp/v2/menu-locations', + '/wp/v2/menu-locations/(?P[\w-]+)', + '/wp/v2/menus', + '/wp/v2/menus/(?P[\d]+)', '/wp/v2/pages', '/wp/v2/pages/(?P[\\d]+)', '/wp/v2/pages/(?P[\\d]+)/revisions', @@ -124,37 +133,76 @@ public function test_expected_routes_in_schema() { '/wp/v2/users/(?P(?:[\\d]+|me))/application-passwords/(?P[\\w\\-]+)', '/wp/v2/comments', '/wp/v2/comments/(?P[\\d]+)', + '/wp/v2/global-styles/(?P[\/\d+]+)', + '/wp/v2/global-styles/(?P[\d]+)/revisions', + '/wp/v2/global-styles/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/global-styles/themes/(?P[\/\s%\w\.\(\)\[\]\@_\-]+)/variations', + '/wp/v2/global-styles/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/search', '/wp/v2/block-renderer/(?P[a-z0-9-]+/[a-z0-9-]+)', '/wp/v2/block-types', '/wp/v2/block-types/(?P[a-zA-Z0-9_-]+)', '/wp/v2/block-types/(?P[a-zA-Z0-9_-]+)/(?P[a-zA-Z0-9_-]+)', '/wp/v2/settings', + '/wp/v2/template-parts', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + '/wp/v2/template-parts/lookup', '/wp/v2/templates', - '/wp/v2/templates/(?P[\/\w-]+)', - '/wp/v2/templates/(?P[\d]+)/autosaves', - '/wp/v2/templates/(?P[\d]+)/autosaves/(?P[\d]+)', - '/wp/v2/templates/(?P[\d]+)/revisions', - '/wp/v2/templates/(?P[\d]+)/revisions/(?P[\d]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + '/wp/v2/templates/lookup', '/wp/v2/themes', - '/wp/v2/themes/(?P[^.\/]+(?:\/[^.\/]+)?)', + '/wp/v2/themes/(?P[^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)', '/wp/v2/plugins', '/wp/v2/plugins/(?P[^.\/]+(?:\/[^.\/]+)?)', '/wp/v2/block-directory/search', + '/wp/v2/block-patterns/categories', + '/wp/v2/block-patterns/patterns', '/wp/v2/sidebars', '/wp/v2/sidebars/(?P[\w-]+)', '/wp/v2/widget-types', '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)', '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/encode', + '/wp/v2/widget-types/(?P[a-zA-Z0-9_-]+)/render', '/wp/v2/widgets', '/wp/v2/widgets/(?P[\w\-]+)', + '/wp/v2/navigation', + '/wp/v2/navigation/(?P[\d]+)', + '/wp/v2/navigation/(?P[\d]+)/autosaves', + '/wp/v2/navigation/(?P[\d]+)/autosaves/(?P[\d]+)', + '/wp/v2/navigation/(?P[\d]+)/revisions', + '/wp/v2/navigation/(?P[\d]+)/revisions/(?P[\d]+)', '/wp-site-health/v1', '/wp-site-health/v1/tests/background-updates', '/wp-site-health/v1/tests/loopback-requests', '/wp-site-health/v1/tests/https-status', '/wp-site-health/v1/tests/dotorg-communication', '/wp-site-health/v1/tests/authorization-header', + '/wp-site-health/v1/tests/page-cache', '/wp-site-health/v1/directory-sizes', + '/wp/v2/wp_pattern_category', + '/wp/v2/wp_pattern_category/(?P[\d]+)', + '/wp/v2/font-collections', + '/wp/v2/font-collections/(?P[\/\w-]+)', + '/wp/v2/font-families', + '/wp/v2/font-families/(?P[\d]+)/font-faces', + '/wp/v2/font-families/(?P[\d]+)/font-faces/(?P[\d]+)', + '/wp/v2/font-families/(?P[\d]+)', + '/wp/v2/icons', + '/wp/v2/icons/(?P[a-z][a-z0-9-]*/[a-z][a-z0-9-]*)', + '/wp-abilities/v1', + '/wp-abilities/v1/categories', + '/wp-abilities/v1/categories/(?P[a-z0-9]+(?:-[a-z0-9]+)*)', + '/wp-abilities/v1/abilities/(?P[a-zA-Z0-9\-\/]+?)/run', + '/wp-abilities/v1/abilities/(?P[a-zA-Z0-9\-\/]+)', + '/wp-abilities/v1/abilities', ); $this->assertSameSets( $expected_routes, $routes ); @@ -165,17 +213,22 @@ private function is_builtin_route( $route ) { '/' === $route || preg_match( '#^/oembed/1\.0(/.+)?$#', $route ) || preg_match( '#^/wp/v2(/.+)?$#', $route ) || - preg_match( '#^/wp-site-health/v1(/.+)?$#', $route ) + preg_match( '#^/wp-site-health/v1(/.+)?$#', $route ) || + preg_match( '#^/wp-abilities/v1(/.+)?$#', $route ) ); } public function test_build_wp_api_client_fixtures() { + if ( 'example.org' !== WP_TESTS_DOMAIN ) { + $this->markTestSkipped( 'This test can only be run on example.org' ); + } + // Set up data for individual endpoint responses. We need to specify // lots of different fields on these objects, otherwise the generated // fixture file will be different between runs of PHPUnit tests, which // is not desirable. - $administrator_id = $this->factory->user->create( + $administrator_id = self::factory()->user->create( array( 'role' => 'administrator', 'display_name' => 'REST API Client Fixture: User', @@ -185,7 +238,7 @@ public function test_build_wp_api_client_fixtures() { ); wp_set_current_user( $administrator_id ); - $post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( 'post_name' => 'restapi-client-fixture-post', 'post_title' => 'REST API Client Fixture: Post', @@ -213,7 +266,7 @@ public function test_build_wp_api_client_fixtures() { ) ); - $page_id = $this->factory->post->create( + $page_id = self::factory()->post->create( array( 'post_type' => 'page', 'post_name' => 'restapi-client-fixture-page', @@ -243,7 +296,7 @@ public function test_build_wp_api_client_fixtures() { ) ); - $tag_id = $this->factory->tag->create( + $tag_id = self::factory()->tag->create( array( 'name' => 'REST API Client Fixture: Tag', 'slug' => 'restapi-client-fixture-tag', @@ -251,7 +304,7 @@ public function test_build_wp_api_client_fixtures() { ) ); - $media_id = $this->factory->attachment->create_object( + $media_id = self::factory()->attachment->create_object( get_temp_dir() . 'canola.jpg', 0, array( @@ -265,7 +318,7 @@ public function test_build_wp_api_client_fixtures() { ) ); - $comment_id = $this->factory->comment->create( + $comment_id = self::factory()->comment->create( array( 'comment_approved' => 1, 'comment_post_ID' => $post_id, @@ -491,7 +544,7 @@ public function test_build_wp_api_client_fixtures() { wp_delete_post( $post_id, true ); wp_delete_post( $page_id, true ); wp_delete_term( $tag_id, 'tags' ); - wp_delete_attachment( $media_id ); + wp_delete_attachment( $media_id, true ); wp_delete_comment( $comment_id ); } @@ -533,6 +586,7 @@ public function test_build_wp_api_client_fixtures() { 'oembeds.author_url' => 'http://example.org', 'oembeds.html' => '
      ...
      ...', 'PostsCollection.0.id' => 4, + 'PostsCollection.0.class_list.0' => 'post-4', 'PostsCollection.0.guid.rendered' => 'http://example.org/?p=4', 'PostsCollection.0.link' => 'http://example.org/?p=4', 'PostsCollection.0._links.self.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/posts/4', @@ -546,6 +600,7 @@ public function test_build_wp_api_client_fixtures() { 'PostsCollection.0._links.wp:term.0.href' => 'http://example.org/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=4', 'PostsCollection.0._links.wp:term.1.href' => 'http://example.org/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=4', 'PostModel.id' => 4, + 'PostModel.class_list.0' => 'post-4', 'PostModel.guid.rendered' => 'http://example.org/?p=4', 'PostModel.link' => 'http://example.org/?p=4', 'postRevisions.0.author' => 2, @@ -577,6 +632,7 @@ public function test_build_wp_api_client_fixtures() { 'autosave.slug' => '4-autosave-v1', 'autosave.guid.rendered' => 'http://example.org/?p=6', 'PagesCollection.0.id' => 7, + 'PagesCollection.0.class_list.0' => 'post-7', 'PagesCollection.0.guid.rendered' => 'http://example.org/?page_id=7', 'PagesCollection.0.link' => 'http://example.org/?page_id=7', 'PagesCollection.0._links.self.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/pages/7', @@ -588,6 +644,7 @@ public function test_build_wp_api_client_fixtures() { 'PagesCollection.0._links.predecessor-version.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/pages/7/revisions/9', 'PagesCollection.0._links.wp:attachment.0.href' => 'http://example.org/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=7', 'PageModel.id' => 7, + 'PageModel.class_list.0' => 'post-7', 'PageModel.guid.rendered' => 'http://example.org/?page_id=7', 'PageModel.link' => 'http://example.org/?page_id=7', 'pageRevisions.0.author' => 2, @@ -619,6 +676,7 @@ public function test_build_wp_api_client_fixtures() { 'pageAutosave.slug' => '7-autosave-v1', 'pageAutosave.guid.rendered' => 'http://example.org/?p=9', 'MediaCollection.0.id' => 10, + 'MediaCollection.0.class_list.0' => 'post-10', 'MediaCollection.0.guid.rendered' => 'http://example.org/?attachment_id=10', 'MediaCollection.0.link' => 'http://example.org/?attachment_id=10', 'MediaCollection.0.description.rendered' => '

      ', @@ -628,6 +686,7 @@ public function test_build_wp_api_client_fixtures() { 'MediaCollection.0._links.about.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/types/attachment', 'MediaCollection.0._links.replies.0.href' => 'http://example.org/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=10', 'MediaModel.id' => 10, + 'MediaModel.class_list.0' => 'post-10', 'MediaModel.guid.rendered' => 'http://example.org/?attachment_id=10', 'MediaModel.link' => 'http://example.org/?attachment_id=10', 'MediaModel.description.rendered' => '

      ', @@ -679,9 +738,9 @@ public function test_build_wp_api_client_fixtures() { 'TagModel.meta.test_multi' => array(), 'TagModel.meta.test_tag_meta' => '', 'UsersCollection.0.link' => 'http://example.org/?author=1', - 'UsersCollection.0.avatar_urls.24' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=24&d=mm&r=g', - 'UsersCollection.0.avatar_urls.48' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=48&d=mm&r=g', - 'UsersCollection.0.avatar_urls.96' => 'http://0.gravatar.com/avatar/96614ec98aa0c0d2ee75796dced6df54?s=96&d=mm&r=g', + 'UsersCollection.0.avatar_urls.24' => 'https://secure.gravatar.com/avatar/9387ed9432ec25ef93df84b8a0b9697ddef435a945e7f244670c4f79f88363e9?s=24&d=mm&r=g', + 'UsersCollection.0.avatar_urls.48' => 'https://secure.gravatar.com/avatar/9387ed9432ec25ef93df84b8a0b9697ddef435a945e7f244670c4f79f88363e9?s=48&d=mm&r=g', + 'UsersCollection.0.avatar_urls.96' => 'https://secure.gravatar.com/avatar/9387ed9432ec25ef93df84b8a0b9697ddef435a945e7f244670c4f79f88363e9?s=96&d=mm&r=g', 'UsersCollection.0._links.self.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/users/1', 'UsersCollection.0._links.collection.0.href' => 'http://example.org/index.php?rest_route=/wp/v2/users', 'UsersCollection.1.id' => 2, @@ -715,17 +774,15 @@ private function normalize_fixture( $data, $path ) { return $data; } + $datetime_keys = array( 'date', 'date_gmt', 'modified', 'modified_gmt' ); + foreach ( $data as $key => $value ) { - if ( is_string( $value ) && ( - 'date' === $key || - 'date_gmt' === $key || - 'modified' === $key || - 'modified_gmt' === $key - ) ) { + if ( is_string( $value ) && in_array( $key, $datetime_keys, true ) ) { $data[ $key ] = '2017-02-14T00:00:00'; - } else { - $data[ $key ] = $this->normalize_fixture( $value, "$path.$key" ); + continue; } + + $data[ $key ] = $this->normalize_fixture( $value, "$path.$key" ); } return $data; diff --git a/tests/phpunit/tests/rest-api/rest-schema-validation.php b/tests/phpunit/tests/rest-api/rest-schema-validation.php index 58c5452de9c76..ce8875c3e9339 100644 --- a/tests/phpunit/tests/rest-api/rest-schema-validation.php +++ b/tests/phpunit/tests/rest-api/rest-schema-validation.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Schema_Validation extends WP_UnitTestCase { @@ -1022,6 +1020,17 @@ public function test_nullable_date() { $this->assertSame( 'Invalid date.', $error->get_error_message() ); } + /** + * @ticket 60184 + */ + public function test_epoch() { + $schema = array( + 'type' => 'string', + 'format' => 'date-time', + ); + $this->assertTrue( rest_validate_value_from_schema( '1970-01-01T00:00:00Z', $schema ) ); + } + public function test_object_or_string() { $schema = array( 'type' => array( 'object', 'string' ), diff --git a/tests/phpunit/tests/rest-api/rest-search-controller.php b/tests/phpunit/tests/rest-api/rest-search-controller.php index f6706b18ff592..e4235fd699798 100644 --- a/tests/phpunit/tests/rest-api/rest-search-controller.php +++ b/tests/phpunit/tests/rest-api/rest-search-controller.php @@ -1,13 +1,9 @@ do_request_with_params( + array( + 'per_page' => $per_page, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + + $next_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 2, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringNotContainsString( 'rel="prev"', $headers['Link'] ); + $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); + + $response = $this->do_request_with_params( + array( + 'per_page' => $per_page, + 'page' => 3, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $prev_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 2, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); + $next_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => 4, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); + + // Last page. + $response = $this->do_request_with_params( + array( + 'per_page' => $per_page, + 'page' => $total_pages, + ), + $method + ); + $headers = $response->get_headers(); + $this->assertSame( $total_posts, $headers['X-WP-Total'] ); + $this->assertSame( $total_pages, $headers['X-WP-TotalPages'] ); + $prev_link = add_query_arg( + array( + 'per_page' => $per_page, + 'page' => $total_pages - 1, + ), + rest_url( '/wp/v2/search' ) + ); + $this->assertStringContainsString( '<' . $prev_link . '>; rel="prev"', $headers['Link'] ); + $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * Search through all content with a low limit. */ @@ -243,13 +334,19 @@ public function test_get_items_search_type_post_subtype_page() { /** * Search through an invalid type + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_search_type_invalid() { + public function test_get_items_search_type_invalid( $method ) { $response = $this->do_request_with_params( array( 'per_page' => 100, 'type' => 'invalid', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -257,6 +354,11 @@ public function test_get_items_search_type_invalid() { /** * Search through posts of an invalid post type. + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ public function test_get_items_search_type_post_subtype_invalid() { $response = $this->do_request_with_params( @@ -332,6 +434,36 @@ public function test_get_items_search_for_foocontent() { ); } + /** + * @ticket 55674 + */ + public function test_get_items_search_prime_ids() { + $action = new MockAction(); + add_filter( 'query', array( $action, 'filter' ), 10, 2 ); + + $query_args = array( + 'per_page' => 100, + 'search' => 'foocontent', + ); + $response = $this->do_request_with_params( $query_args ); + $this->assertSame( 200, $response->get_status(), 'Request Status Response is not 200.' ); + + $ids = wp_list_pluck( $response->get_data(), 'id' ); + $this->assertSameSets( self::$my_content_post_ids, $ids, 'Query result posts ids do not match with expected ones.' ); + + $args = $action->get_args(); + $primed_query_found = false; + foreach ( $args as $arg ) { + // Primed query will use WHERE ID IN clause. + if ( str_contains( $arg[0], 'WHERE ID IN (' . implode( ',', $ids ) ) ) { + $primed_query_found = true; + break; + } + } + + $this->assertTrue( $primed_query_found, 'Prime query was not executed.' ); + } + /** * Test retrieving a single item isn't possible. */ @@ -413,7 +545,6 @@ public function test_prepare_item_limit_fields() { array( 'id', 'title', - '_links', ), array_keys( $data[0] ) ); @@ -437,13 +568,19 @@ public function test_get_item_schema() { /** * Tests that non-public post types are not allowed. + * + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_non_public_post_type() { + public function test_non_public_post_type( $method ) { $response = $this->do_request_with_params( array( 'type' => 'post', 'subtype' => 'post,nav_menu_item', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); } @@ -607,15 +744,21 @@ public function test_get_items_search_type_term_subtype_category() { /** * Search through posts of an invalid post type. * + * + * @dataProvider data_readable_http_methods * @ticket 51458 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_search_term_subtype_invalid() { + public function test_get_items_search_term_subtype_invalid( $method ) { $response = $this->do_request_with_params( array( 'per_page' => 100, 'type' => 'term', 'subtype' => 'invalid', - ) + ), + $method ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -790,4 +933,96 @@ private function get_request( $params = array(), $method = 'GET' ) { return $request; } + /** + * @ticket 56546 + */ + public function test_get_items_search_posts_include_ids() { + $response = $this->do_request_with_params( + array( + 'include' => array_slice( self::$my_title_post_ids, 1, 2 ), + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_title_post_ids[1], self::$my_title_post_ids[2] ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_posts_exclude_ids() { + $response = $this->do_request_with_params( + array( + 'exclude' => self::$my_title_page_ids, + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array_merge( + self::$my_title_post_ids, + self::$my_content_post_ids + ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_terms_include_ids() { + $response = $this->do_request_with_params( + array( + 'include' => self::$my_tag_id, + 'type' => 'term', + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_tag_id ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @ticket 56546 + */ + public function test_get_items_search_terms_exclude_ids() { + $response = $this->do_request_with_params( + array( + // "1" is the default category. + 'exclude' => array( 1, self::$my_tag_id ), + 'type' => 'term', + ) + ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSameSets( + array( self::$my_category_id ), + wp_list_pluck( $response->get_data(), 'id' ) + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 60771 + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_sanitize_subtypes_validates_type( $method ) { + $response = $this->do_request_with_params( + array( + 'subtype' => 'page', + 'type' => array( 'invalid' ), + ), + $method + ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-server.php b/tests/phpunit/tests/rest-api/rest-server.php index 30977958fa911..57b7bbb38abcd 100644 --- a/tests/phpunit/tests/rest-api/rest-server.php +++ b/tests/phpunit/tests/rest-api/rest-server.php @@ -4,12 +4,43 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class Tests_REST_Server extends WP_Test_REST_TestCase { + protected static $icon_id; + protected static $admin_id; + protected static $post_id; + + /** + * Called before setting up all tests. + */ + public static function set_up_before_class() { + parent::set_up_before_class(); + + // Require files that need to load once. + require_once DIR_TESTROOT . '/includes/mock-invokable.php'; + } + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + $filename = DIR_TESTDATA . '/images/test-image-large.jpg'; + self::$icon_id = $factory->attachment->create_upload_object( $filename ); + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + self::$post_id = $factory->post->create(); + } + + public static function tear_down_after_class() { + wp_delete_attachment( self::$icon_id, true ); + self::delete_user( self::$admin_id ); + wp_delete_post( self::$post_id ); + + parent::tear_down_after_class(); + } + public function set_up() { parent::set_up(); @@ -28,6 +59,10 @@ public function tear_down() { parent::tear_down(); } + public function filter_wp_rest_server_class() { + return 'Spy_REST_Server'; + } + public function test_envelope() { $data = array( 'amount of arbitrary data' => 'alot', @@ -60,6 +95,77 @@ public function test_envelope() { $this->assertSame( $headers, $enveloped['headers'] ); } + /** + * @dataProvider data_envelope_params + * @ticket 54015 + */ + public function test_envelope_param( $_embed ) { + // Register our testing route. + rest_get_server()->register_route( + 'test', + '/test/embeddable', + array( + 'methods' => 'GET', + 'callback' => array( $this, 'embedded_response_callback' ), + ) + ); + $data = array( + 'amount of arbitrary data' => 'alot', + ); + $status = 987; + $headers = array( + 'Arbitrary-Header' => 'value', + 'Multiple' => 'maybe, yes', + ); + + $response = new WP_REST_Response( $data, $status ); + $response->header( 'Arbitrary-Header', 'value' ); + + // Check header concatenation as well. + $response->header( 'Multiple', 'maybe' ); + $response->header( 'Multiple', 'yes', false ); + + // All others should be embedded. + $response->add_link( 'alternate', rest_url( '/test/embeddable' ), array( 'embeddable' => true ) ); + + $embed = rest_parse_embed_param( $_embed ); + $envelope_response = rest_get_server()->envelope_response( $response, $embed ); + + // The envelope should still be a response, but with defaults. + $this->assertInstanceOf( WP_REST_Response::class, $envelope_response ); + $this->assertSame( 200, $envelope_response->get_status() ); + $this->assertEmpty( $envelope_response->get_headers() ); + $this->assertEmpty( $envelope_response->get_links() ); + + $enveloped = $envelope_response->get_data(); + + $this->assertArrayHasKey( 'body', $enveloped ); + $this->assertArrayHasKey( '_links', $enveloped['body'] ); + $this->assertArrayHasKey( '_embedded', $enveloped['body'] ); + $this->assertArrayHasKey( 'alternate', $enveloped['body']['_embedded'] ); + + $alternate = $enveloped['body']['_embedded']['alternate']; + $this->assertCount( 1, $alternate ); + + $this->assertSame( $status, $enveloped['status'] ); + $this->assertSame( $headers, $enveloped['headers'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_envelope_params() { + return array( + array( '1' ), + array( 'true' ), + array( false ), + array( 'alternate' ), + array( array( 'alternate' ) ), + ); + } + public function test_default_param() { register_rest_route( @@ -370,6 +476,38 @@ public function test_allow_header_send_only_permitted_methods() { $this->assertSame( $sent_headers['Allow'], 'POST' ); } + /** + * @ticket 53063 + */ + public function test_batched_options() { + register_rest_route( + 'test-ns', + '/test', + array( + array( + 'methods' => array( 'GET' ), + 'callback' => '__return_null', + 'permission_callback' => '__return_true', + ), + array( + 'methods' => array( 'POST' ), + 'callback' => '__return_null', + 'permission_callback' => '__return_null', + 'allow_batch' => false, + ), + 'allow_batch' => array( 'v1' => true ), + ) + ); + + $request = new WP_REST_Request( 'OPTIONS', '/test-ns/test' ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $this->assertArrayNotHasKey( 'allow_batch', $data['endpoints'][1] ); + } + public function test_allow_header_sent_on_options_request() { register_rest_route( 'test-ns', @@ -469,6 +607,33 @@ public function test_error_to_response_with_additional_data() { $this->assertSame( array( array( 'status' => 400 ) ), $response->get_data()['additional_data'] ); } + /** + * @ticket 64901 + */ + public function test_error_to_response_with_stdclass_data() { + $error = new WP_Error( 'test', 'test', (object) array( 'status' => 400 ) ); + + $response = rest_convert_error_to_response( $error ); + $this->assertInstanceOf( WP_REST_Response::class, $response ); + + // stdClass data should not cause a fatal, status should default to 500. + $this->assertSame( 500, $response->get_status() ); + } + + /** + * @ticket 64901 + */ + public function test_error_to_response_with_multi_status_non_numeric_status() { + $error = new WP_Error( 'test', 'test', array( 'status' => array( 'feeling' => 'happy' ) ) ); + $error->add_data( array( 'status' => 400 ), 'test' ); + $error->add_data( array( 'status' => array( 'feeling' => 'bleh' ) ), 'test' ); + + $response = rest_convert_error_to_response( $error ); + $this->assertInstanceOf( WP_REST_Response::class, $response ); + + $this->assertSame( 400, $response->get_status() ); + } + public function test_rest_error() { $data = array( 'code' => 'wp-api-test-error', @@ -482,8 +647,8 @@ public function test_rest_error() { public function test_json_error_with_status() { $stub = $this->getMockBuilder( 'Spy_REST_Server' ) - ->setMethods( array( 'set_status' ) ) - ->getMock(); + ->setMethods( array( 'set_status' ) ) + ->getMock(); $stub->expects( $this->once() ) ->method( 'set_status' ) @@ -811,11 +976,68 @@ public function test_link_embedding_without_links() { $data = array( 'untouched' => 'data', ); - $result = rest_get_server()->embed_links( $data ); + $result = rest_get_server()->embed_links( $data, true ); - $this->assertArrayNotHasKey( '_links', $data ); - $this->assertArrayNotHasKey( '_embedded', $data ); - $this->assertSame( 'data', $data['untouched'] ); + $this->assertArrayNotHasKey( '_links', $result ); + $this->assertArrayNotHasKey( '_embedded', $result ); + $this->assertSame( 'data', $result['untouched'] ); + } + + /** + * Ensure embedding is with links in the data. + * + * @ticket 43439 + */ + public function test_link_embedding_with_links() { + $data = array( + '_links' => array( + 'wp:term' => array( + array( + 'taxonomy' => 'category', + 'embeddable' => true, + 'href' => get_rest_url( 0, '/wp/v2/categories' ), + ), + array( + 'taxonomy' => 'post_tag', + 'embeddable' => true, + 'href' => get_rest_url( 0, '/wp/v2/tags' ), + ), + ), + ), + ); + + $mock = new MockAction(); + add_filter( 'rest_post_dispatch', array( $mock, 'filter' ), 10, 3 ); + + rest_get_server()->embed_links( $data, true ); + $args = $mock->get_args(); + foreach ( $args as $arg ) { + $this->assertSame( 100, $arg[2]['per_page'], 'Posts per page should be 100' ); + } + } + + /** + * Ensure embed_links handles WP_Error objects returned by dispatch + * + * @ticket 56566 + */ + public function test_link_embedding_returning_wp_error() { + $return_wp_error = static function () { + return new WP_Error( 'some-error', 'This is not valid!' ); + }; + add_filter( 'rest_pre_dispatch', $return_wp_error ); + + $mock = new MockAction(); + add_filter( 'rest_post_dispatch', array( $mock, 'filter' ) ); + + $response = new WP_REST_Response(); + $response->add_link( 'author', rest_url( 'test' ), array( 'embeddable' => true ) ); + + $data = rest_get_server()->response_to_data( $response, true ); + + $this->assertArrayHasKey( '_links', $data ); + $this->assertCount( 1, $mock->get_events() ); + $this->assertSame( 'some-error', $data['_embedded']['author'][0]['code'] ); } public function embedded_response_callback( $request ) { @@ -871,7 +1093,7 @@ public function test_removing_links_for_href() { } /** - * @dataProvider _dp_response_to_data_embedding + * @dataProvider data_response_to_data_embedding */ public function test_response_to_data_embedding( $expected, $embed ) { $response = new WP_REST_Response(); @@ -889,7 +1111,7 @@ public function test_response_to_data_embedding( $expected, $embed ) { } } - public function _dp_response_to_data_embedding() { + public function data_response_to_data_embedding() { return array( array( array( 'author', 'wp:term', 'https://wordpress.org' ), @@ -957,6 +1179,9 @@ public function test_get_index() { $this->assertArrayHasKey( 'home', $data ); $this->assertArrayHasKey( 'gmt_offset', $data ); $this->assertArrayHasKey( 'timezone_string', $data ); + $this->assertArrayHasKey( 'page_for_posts', $data ); + $this->assertArrayHasKey( 'page_on_front', $data ); + $this->assertArrayHasKey( 'show_on_front', $data ); $this->assertArrayHasKey( 'namespaces', $data ); $this->assertArrayHasKey( 'authentication', $data ); $this->assertArrayHasKey( 'routes', $data ); @@ -975,6 +1200,207 @@ public function test_get_index() { $this->assertArrayHasKey( 'help', $index->get_links() ); $this->assertArrayNotHasKey( 'wp:active-theme', $index->get_links() ); + + // Check site logo and icon. + $this->assertArrayHasKey( 'site_logo', $data ); + $this->assertArrayHasKey( 'site_icon', $data ); + $this->assertArrayHasKey( 'site_icon_url', $data ); + } + + /** + * @ticket 57902 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_fields_name() { + $server = new WP_REST_Server(); + + $request = new WP_REST_Request( 'GET', '/' ); + $request->set_param( '_fields', 'name' ); + $index = $server->dispatch( $request ); + $index = rest_filter_response_fields( $index, $server, $request ); + $data = $index->get_data(); + $links = $index->get_links(); + + $this->assertArrayHasKey( 'name', $data ); + $this->assertArrayNotHasKey( 'help', $links ); + } + + /** + * @ticket 57902 + * + * @covers WP_REST_Server::get_index + * + * @dataProvider data_get_index_should_return_help_and_not_name + * + * @param string $field The field to add to the request. + */ + public function test_get_index_should_return_help_and_not_name( $field ) { + $server = new WP_REST_Server(); + + $request = new WP_REST_Request( 'GET', '/' ); + $request->set_param( '_fields', $field ); + $index = $server->dispatch( $request ); + $index = rest_filter_response_fields( $index, $server, $request ); + $data = $index->get_data(); + $links = $index->get_links(); + + $this->assertArrayNotHasKey( 'name', $data ); + $this->assertArrayHasKey( 'help', $links ); + } + + /** + * Data provider. + * + * @throws Exception + * + * @return array + */ + public function data_get_index_should_return_help_and_not_name() { + return self::text_array_to_dataprovider( array( '_links', '_embedded' ) ); + } + + /** + * @ticket 50152 + */ + public function test_index_includes_link_to_active_theme_if_authenticated() { + $server = new WP_REST_Server(); + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + + $this->assertArrayHasKey( 'https://api.w.org/active-theme', $index->get_links() ); + } + + /** + * @ticket 52321 + * @ticket 59935 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_should_include_site_icon() { + update_option( 'site_icon', self::$icon_id ); + + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + + $this->assertArrayHasKey( 'site_logo', $data, 'The "site_logo" field is missing in the response.' ); + $this->assertArrayHasKey( 'site_icon', $data, 'The "site_icon" field is missing in the response.' ); + $this->assertArrayHasKey( 'site_icon_url', $data, 'The "site_icon_url" field is missing in the response.' ); + $this->assertSame( self::$icon_id, $data['site_icon'], 'The response "site_icon" ID does not match.' ); + $this->assertStringContainsString( 'test-image-large', $data['site_icon_url'], 'The "site_icon_url" should contain the expected image.' ); + } + /** + * @ticket 52321 + * @ticket 59935 + * + * @covers WP_REST_Server::get_index + */ + public function test_get_index_should_not_include_site_icon() { + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/' ); + $index = $server->dispatch( $request ); + $data = $index->get_data(); + + $this->assertArrayHasKey( 'site_logo', $data, 'The "site_logo" field is missing in the response.' ); + $this->assertArrayHasKey( 'site_icon', $data, 'The "site_icon" field is missing in the response.' ); + $this->assertArrayHasKey( 'site_icon_url', $data, 'The "site_icon_url" field is missing in the response.' ); + $this->assertSame( 0, $data['site_icon'], 'Response "site_icon" should be 0.' ); + $this->assertSame( '', $data['site_icon_url'], 'Response "site_icon_url" should be an empty string.' ); + } + + /** + * Test that the "get_index" method returns the expected site_icon* + * and site_logo fields based on the specified request parameters. + * + * @ticket 59935 + * + * @covers WP_REST_Server::get_index + * + * @dataProvider data_get_index_should_return_site_icon_and_site_logo_fields + * + * @param string $fields List of fields to use in the request. + * @param array $expected_fields Expected fields. + * @param array $unexpected_fields Optional. Fields that should not be in the results. Default array(). + * @param bool $is_embed Optional. Whether to use the "_embed" request parameter. Default false. + */ + public function test_get_index_should_return_site_icon_and_site_logo_fields( $fields, $expected_fields, $unexpected_fields = array(), $is_embed = false ) { + $server = new WP_REST_Server(); + $request = new WP_REST_Request( 'GET', '/', array() ); + $request->set_param( '_fields', $fields ); + if ( $is_embed ) { + $request->set_param( '_embed', true ); + } + + $response = $server->get_index( $request )->get_data(); + + foreach ( $expected_fields as $expected_field ) { + $this->assertArrayHasKey( $expected_field, $response, "Expected \"{$expected_field}\" field is missing in the response." ); + } + + foreach ( $unexpected_fields as $unexpected_field ) { + $this->assertArrayNotHasKey( $unexpected_field, $response, "Response must not contain the \"{$unexpected_field}\" field." ); + } + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_index_should_return_site_icon_and_site_logo_fields() { + return array( + 'no site_logo or site_icon fields' => array( + 'fields' => 'name', + 'expected_fields' => array(), + 'unexpected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + ), + '_links request parameter' => array( + 'fields' => '_links', + 'expected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + ), + '_embed request parameter' => array( + 'field' => '_embed', + 'expected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + 'unexpected_fields' => array(), + 'is_embed' => true, + ), + 'site_logo field' => array( + 'fields' => 'site_logo', + 'expected_fields' => array( 'site_logo' ), + 'unexpected_fields' => array( 'site_icon', 'site_icon_url' ), + ), + 'site_icon field' => array( + 'fields' => 'site_icon', + 'expected_fields' => array( 'site_icon', 'site_icon_url' ), + 'unexpected_fields' => array( 'site_logo' ), + ), + 'site_icon_url field' => array( + 'fields' => 'site_icon_url', + 'expected_fields' => array( 'site_icon', 'site_icon_url' ), + 'unexpected_fields' => array( 'site_logo' ), + ), + 'site_icon and site_icon_url field' => array( + 'fields' => 'site_icon_url', + 'expected_fields' => array( 'site_icon', 'site_icon_url' ), + 'unexpected_fields' => array( 'site_logo' ), + ), + 'site_logo and site_icon fields' => array( + 'fields' => 'site_logo,site_icon', + 'expected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + ), + 'site_logo and site_icon_url fields' => array( + 'fields' => 'site_logo,site_icon_url', + 'expected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + ), + 'site_logo, site_icon, and site_icon_url fields' => array( + 'fields' => 'site_logo,site_icon,site_icon_url', + 'expected_fields' => array( 'site_logo', 'site_icon', 'site_icon_url' ), + ), + ); } public function test_get_namespace_index() { @@ -1091,7 +1517,7 @@ public function test_rest_enable_filter_is_deprecated() { $result = json_decode( rest_get_server()->sent_body ); - $this->assertObjectNotHasAttribute( 'code', $result ); + $this->assertObjectNotHasProperty( 'code', $result ); } public function test_link_header_on_requests() { @@ -1102,7 +1528,7 @@ public function test_link_header_on_requests() { $result = rest_get_server()->serve_request( '/' ); $headers = rest_get_server()->sent_headers; - $this->assertSame( '<' . esc_url_raw( $api_root ) . '>; rel="https://api.w.org/"', $headers['Link'] ); + $this->assertSame( '<' . sanitize_url( $api_root ) . '>; rel="https://api.w.org/"', $headers['Link'] ); } public function test_nocache_headers_on_authenticated_requests() { @@ -1295,10 +1721,6 @@ public function test_serve_request_headers_are_unslashed() { $this->assertSame( 'data\\with\\slashes', rest_get_server()->last_request->get_header( 'x_my_header' ) ); } - public function filter_wp_rest_server_class() { - return 'Spy_REST_Server'; - } - /** * Refreshed nonce should not be present in header when an invalid nonce is passed for logged in user. * @@ -1314,6 +1736,32 @@ public function test_rest_send_refreshed_nonce_invalid_nonce() { $this->assertArrayNotHasKey( 'X-WP-Nonce', $headers ); } + /** + * Helper to setup a users and auth cookie global for the + * rest_send_refreshed_nonce related tests. + */ + protected function helper_setup_user_for_rest_send_refreshed_nonce_tests() { + $author = self::factory()->user->create( array( 'role' => 'author' ) ); + wp_set_current_user( $author ); + + global $wp_rest_auth_cookie; + + $wp_rest_auth_cookie = true; + } + + /** + * Helper to make the request and get the headers for the + * rest_send_refreshed_nonce related tests. + * + * @return array + */ + protected function helper_make_request_and_return_headers_for_rest_send_refreshed_nonce_tests() { + $request = new WP_REST_Request( 'GET', '/', array() ); + $result = rest_get_server()->serve_request( '/' ); + + return rest_get_server()->sent_headers; + } + /** * Refreshed nonce should be present in header when a valid nonce is * passed for logged in/anonymous user and not present when nonce is not @@ -1344,6 +1792,23 @@ public function test_rest_send_refreshed_nonce( $has_logged_in_user, $has_nonce } } + /** + * @return array { + * @type array { + * @type bool $has_logged_in_user Are we registering a user for the test. + * @type bool $has_nonce Is the nonce passed. + * } + * } + */ + public function data_rest_send_refreshed_nonce() { + return array( + array( true, true ), + array( true, false ), + array( false, true ), + array( false, false ), + ); + } + /** * Make sure that a sanitization that transforms the argument type will not * cause the validation to fail. @@ -1383,6 +1848,22 @@ public function test_rest_validate_before_sanitization() { $this->assertSame( 200, $response->get_status() ); } + public function _validate_as_integer_123( $value, $request, $key ) { + if ( ! is_int( $value ) ) { + return new WP_Error( 'some-error', 'This is not valid!' ); + } + + return true; + } + + public function _validate_as_string_foo( $value, $request, $key ) { + if ( ! is_string( $value ) ) { + return new WP_Error( 'some-error', 'This is not valid!' ); + } + + return true; + } + /** * @ticket 43691 */ @@ -1491,6 +1972,8 @@ public function test_redirect_http_authorization_with_empty_http_authorization_h public function test_get_routes_respects_namespace_parameter() { $routes = rest_get_server()->get_routes( 'oembed/1.0' ); + $this->assertNotEmpty( $routes ); + foreach ( $routes as $route => $handlers ) { $this->assertStringStartsWith( '/oembed/1.0', $route ); } @@ -1505,7 +1988,7 @@ public function test_get_routes_no_namespace_overriding() { '/test', array( 'methods' => array( 'GET' ), - 'callback' => static function() { + 'callback' => static function () { return new WP_REST_Response( 'data', 204 ); }, 'permission_callback' => '__return_true', @@ -1516,7 +1999,7 @@ public function test_get_routes_no_namespace_overriding() { '/test', array( 'methods' => array( 'GET' ), - 'callback' => static function() { + 'callback' => static function () { return new WP_REST_Response( 'data', 204 ); }, 'permission_callback' => '__return_true', @@ -1573,9 +2056,9 @@ public function test_invalid_handler() { * @ticket 50244 */ public function test_callbacks_are_not_executed_if_request_validation_fails() { - $callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $callback = $this->createPartialMock( 'Mock_Invokable', array( '__invoke' ) ); $callback->expects( self::never() )->method( '__invoke' ); - $permission_callback = $this->createPartialMock( 'stdClass', array( '__invoke' ) ); + $permission_callback = $this->createPartialMock( 'Mock_Invokable', array( '__invoke' ) ); $permission_callback->expects( self::never() )->method( '__invoke' ); register_rest_route( @@ -1635,9 +2118,9 @@ public function test_filters_are_executed_if_request_validation_fails() { /** * @ticket 50244 - * @dataProvider data_batch_v1_optin + * @dataProvider data_batch_v1_opt_in */ - public function test_batch_v1_optin( $allow_batch, $allowed ) { + public function test_batch_v1_opt_in( $allow_batch, $allowed ) { $args = array( 'methods' => 'POST', 'callback' => static function () { @@ -1678,7 +2161,7 @@ public function test_batch_v1_optin( $allow_batch, $allowed ) { } } - public function data_batch_v1_optin() { + public function data_batch_v1_opt_in() { return array( 'missing' => array( null, false ), 'invalid type' => array( true, false ), @@ -1914,7 +2397,7 @@ public function test_batch_v1_partial_error() { public function test_batch_v1_max_requests() { add_filter( 'rest_get_max_batch_size', - static function() { + static function () { return 5; } ); @@ -1947,6 +2430,30 @@ static function() { $this->assertSame( 400, $response->get_status() ); } + /** + * @ticket 63502 + */ + public function test_batch_request_with_malformed_url() { + $request = new WP_REST_Request( 'POST', '/batch/v1' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body_params( + array( + 'requests' => array( + array( + 'method' => 'POST', + 'path' => 'http://user@:80', + ), + ), + ) + ); + + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data()['responses'][0]['body'] ?? null; + + $this->assertIsArray( $data ); + $this->assertSame( 'parse_path_failed', $data['code'] ); + } + /** * @ticket 51020 */ @@ -2012,16 +2519,6 @@ public function test_get_data_for_route_includes_permitted_schema_keywords() { $this->assertSameSetsWithIndex( $expected, $args['param'] ); } - /** - * @ticket 50152 - */ - public function test_index_includes_link_to_active_theme_if_authenticated() { - wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); - - $index = rest_do_request( '/' ); - $this->assertArrayHasKey( 'https://api.w.org/active-theme', $index->get_links() ); - } - /** * @ticket 53056 */ @@ -2032,7 +2529,7 @@ public function test_json_encode_error_results_in_500_status_code() { array( array( 'methods' => \WP_REST_Server::READABLE, - 'callback' => function() { + 'callback' => static function () { return new \WP_REST_Response( INF ); }, 'permission_callback' => '__return_true', @@ -2044,62 +2541,174 @@ public function test_json_encode_error_results_in_500_status_code() { $this->assertSame( 500, rest_get_server()->status ); } - public function _validate_as_integer_123( $value, $request, $key ) { - if ( ! is_int( $value ) ) { - return new WP_Error( 'some-error', 'This is not valid!' ); - } + /** + * @ticket 57752 + */ + public function test_rest_exposed_cors_headers_filter_receives_request_object() { + $mock_hook = new MockAction(); + add_filter( 'rest_exposed_cors_headers', array( $mock_hook, 'filter' ), 10, 2 ); - return true; + rest_get_server()->serve_request( '/test-exposed-cors-headers' ); + + $this->assertCount( 1, $mock_hook->get_events() ); + $this->assertCount( 2, $mock_hook->get_events()[0]['args'] ); + $this->assertInstanceOf( 'WP_REST_Request', $mock_hook->get_events()[0]['args'][1] ); + $this->assertSame( '/test-exposed-cors-headers', $mock_hook->get_events()[0]['args'][1]->get_route() ); } - public function _validate_as_string_foo( $value, $request, $key ) { - if ( ! is_string( $value ) ) { - return new WP_Error( 'some-error', 'This is not valid!' ); - } + /** + * @ticket 57752 + */ + public function test_rest_allowed_cors_headers_filter_receives_request_object() { + $mock_hook = new MockAction(); + add_filter( 'rest_allowed_cors_headers', array( $mock_hook, 'filter' ), 10, 2 ); - return true; + rest_get_server()->serve_request( '/test-allowed-cors-headers' ); + + $this->assertCount( 1, $mock_hook->get_events() ); + $this->assertCount( 2, $mock_hook->get_events()[0]['args'] ); + $this->assertInstanceOf( 'WP_REST_Request', $mock_hook->get_events()[0]['args'][1] ); + $this->assertSame( '/test-allowed-cors-headers', $mock_hook->get_events()[0]['args'][1]->get_route() ); } /** - * @return array { - * @type array { - * @type bool $has_logged_in_user Are we registering a user for the test. - * @type bool $has_nonce Is the nonce passed. - * } - * } + * @ticket 61739 */ - public function data_rest_send_refreshed_nonce() { - return array( - array( true, true ), - array( true, false ), - array( false, true ), - array( false, false ), + public function test_validates_request_when_building_target_hints() { + register_rest_route( + 'test-ns/v1', + '/test/(?P\d+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return new \WP_REST_Response(); + }, + 'permission_callback' => '__return_true', + 'args' => array( + 'id' => array( + 'type' => 'integer', + ), + ), + ), + ) ); + + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( 'test-ns/v1/test/garbage' ) ); + + $links = rest_get_server()::get_response_links( $response ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); } /** - * Helper to setup a users and auth cookie global for the - * rest_send_refreshed_nonce related tests. + * @ticket 61739 */ - protected function helper_setup_user_for_rest_send_refreshed_nonce_tests() { - $author = self::factory()->user->create( array( 'role' => 'author' ) ); - wp_set_current_user( $author ); + public function test_sanitizes_request_when_building_target_hints() { + $validated_param = null; + register_rest_route( + 'test-ns/v1', + '/test/(?P\d+)', + array( + array( + 'methods' => \WP_REST_Server::READABLE, + 'callback' => static function () { + return new \WP_REST_Response(); + }, + 'permission_callback' => function ( WP_REST_Request $request ) use ( &$validated_param ) { + $validated_param = $request['id']; - global $wp_rest_auth_cookie; + return true; + }, + 'args' => array( + 'id' => array( + 'type' => 'integer', + ), + ), + ), + ) + ); - $wp_rest_auth_cookie = true; + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( 'test-ns/v1/test/5' ) ); + + $links = rest_get_server()::get_response_links( $response ); + + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'targetHints', $links['self'][0] ); + $this->assertIsInt( $validated_param ); } /** - * Helper to make the request and get the headers for the - * rest_send_refreshed_nonce related tests. - * - * @return array + * @ticket 61739 */ - protected function helper_make_request_and_return_headers_for_rest_send_refreshed_nonce_tests() { - $request = new WP_REST_Request( 'GET', '/', array() ); - $result = rest_get_server()->serve_request( '/' ); + public function test_populates_target_hints_for_administrator() { + wp_set_current_user( self::$admin_id ); + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; - return rest_get_server()->sent_headers; + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'POST', 'PUT', 'PATCH', 'DELETE' ), $link['targetHints']['allow'] ); + } + + /** + * @ticket 61739 + */ + public function test_populates_target_hints_for_logged_out_user() { + $response = rest_do_request( '/wp/v2/posts' ); + $post = $response->get_data()[0]; + + $link = $post['_links']['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET' ), $link['targetHints']['allow'] ); + } + + /** + * @ticket 61739 + */ + public function test_does_not_error_on_invalid_urls() { + $response = new WP_REST_Response(); + $response->add_link( 'self', 'this is not a real URL' ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + /** + * @ticket 61739 + */ + public function test_does_not_error_on_bad_rest_api_routes() { + $response = new WP_REST_Response(); + $response->add_link( 'self', rest_url( '/this/is/not/a/real/route' ) ); + + $links = rest_get_server()::get_response_links( $response ); + $this->assertArrayNotHasKey( 'targetHints', $links['self'][0] ); + } + + /** + * @ticket 61739 + */ + public function test_prefers_developer_defined_target_hints() { + $response = new WP_REST_Response(); + $response->add_link( + 'self', + '/wp/v2/posts/' . self::$post_id, + array( + 'targetHints' => array( + 'allow' => array( 'GET', 'PUT' ), + ), + ) + ); + + $links = rest_get_server()::get_response_links( $response ); + $link = $links['self'][0]; + $this->assertArrayHasKey( 'targetHints', $link ); + $this->assertArrayHasKey( 'allow', $link['targetHints'] ); + $this->assertSame( array( 'GET', 'PUT' ), $link['targetHints']['allow'] ); } } diff --git a/tests/phpunit/tests/rest-api/rest-settings-controller.php b/tests/phpunit/tests/rest-api/rest-settings-controller.php index bb9a4bbdb6102..e8f90b53f20f1 100644 --- a/tests/phpunit/tests/rest-api/rest-settings-controller.php +++ b/tests/phpunit/tests/rest-api/rest-settings-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase { @@ -14,6 +12,11 @@ class WP_Test_REST_Settings_Controller extends WP_Test_REST_Controller_Testcase protected static $administrator; protected static $author; + /** + * @var WP_REST_Settings_Controller + */ + private $endpoint; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$administrator = $factory->user->create( array( @@ -70,7 +73,11 @@ public function test_get_item() { $this->assertSame( 404, $response->get_status() ); } + /** + * @doesNotPerformAssertions + */ public function test_context_param() { + // Controller does not use get_context_param(). } public function test_get_item_is_not_public_not_authenticated() { @@ -106,8 +113,12 @@ public function test_get_items() { 'default_category', 'default_post_format', 'posts_per_page', + 'show_on_front', + 'page_on_front', + 'page_for_posts', 'default_ping_status', 'default_comment_status', + 'site_icon', // Registered in wp-includes/blocks/site-logo.php ); if ( ! is_multisite() ) { @@ -373,8 +384,11 @@ public function test_get_item_with_invalid_object_array_in_options() { $this->assertNull( $data['mycustomsettinginrest'] ); } - + /** + * @doesNotPerformAssertions + */ public function test_create_item() { + // Controller does not implement create_item(). } public function test_update_item() { @@ -654,10 +668,18 @@ public function test_delete_item() { $this->assertSame( 404, $response->get_status() ); } + /** + * @doesNotPerformAssertions + */ public function test_prepare_item() { + // Controller does not implement prepare_item(). } + /** + * @doesNotPerformAssertions + */ public function test_get_item_schema() { + // Controller does not implement get_item_schema(). } /** @@ -713,4 +735,65 @@ public function test_register_setting_issues_doing_it_wrong_when_show_in_rest_om ) ); } + + /** + * @ticket 56493 + */ + public function test_register_setting_with_custom_additional_properties_value() { + wp_set_current_user( self::$administrator ); + + register_setting( + 'somegroup', + 'mycustomsetting', + array( + 'type' => 'object', + 'show_in_rest' => array( + 'schema' => array( + 'type' => 'object', + 'properties' => array( + 'test1' => array( + 'type' => 'string', + ), + ), + 'additionalProperties' => array( + 'type' => 'integer', + ), + ), + ), + ) + ); + + $data = array( + 'mycustomsetting' => array( + 'test1' => 'my-string', + 'test2' => '2', + 'test3' => 3, + ), + ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/settings' ); + $request->add_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( $data ) ); + + $response = rest_do_request( $request ); + + $this->assertSame( 200, $response->get_status() ); + $this->assertSame( 'my-string', $response->data['mycustomsetting']['test1'] ); + $this->assertSame( 2, $response->data['mycustomsetting']['test2'] ); + $this->assertSame( 3, $response->data['mycustomsetting']['test3'] ); + } + + /** + * @ticket 61023 + */ + public function test_provides_setting_metadata_in_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/settings' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $title = $data['schema']['properties']['title']; + + $this->assertSame( 'string', $title['type'] ); + $this->assertSame( 'Title', $title['title'] ); + $this->assertSame( 'Site title.', $title['description'] ); + $this->assertSame( null, $title['default'] ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php index 91bdeeb342332..dd01d4f2de4ee 100644 --- a/tests/phpunit/tests/rest-api/rest-sidebars-controller.php +++ b/tests/phpunit/tests/rest-api/rest-sidebars-controller.php @@ -5,15 +5,12 @@ * @package WordPress * @subpackage REST_API * @since 5.8.0 - */ - -/** - * Tests for REST API for Widgets. + * + * @covers WP_REST_Sidebars_Controller * * @see WP_Test_REST_Controller_Testcase * @group restapi * @group widgets - * @covers WP_REST_Sidebars_Controller */ class WP_Test_REST_Sidebars_Controller extends WP_Test_REST_Controller_Testcase { @@ -46,8 +43,8 @@ public static function wpSetUpBeforeClass( $factory ) { } public static function wpTearDownAfterClass() { - wp_delete_user( self::$admin_id ); - wp_delete_user( self::$author_id ); + self::delete_user( self::$admin_id ); + self::delete_user( self::$author_id ); } public function set_up() { @@ -155,11 +152,38 @@ public function test_get_items() { } /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_sidebar_data() { + wp_widgets_init(); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/sidebars' ); + + $hook_name = 'rest_prepare_sidebar'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } @@ -314,7 +338,6 @@ public function test_get_items_basic_sidebar() { ), $data ); - } /** @@ -505,9 +528,107 @@ public function test_get_item() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $hook_name = 'rest_prepare_sidebar'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ) + ); + + $request = new WP_REST_Request( 'HEAD', $path ); + // This endpoint doesn't seem to support _fields param, but we need to set it to reproduce the fatal error. + $request->set_param( '_fields', 'name' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + + 'get_item request' => array( '/wp/v2/sidebars/sidebar-1' ), + 'get_items request' => array( '/wp/v2/sidebars' ), + ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); $this->setup_sidebar( 'sidebar-1', @@ -516,7 +637,7 @@ public function test_get_item_no_permission() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } @@ -556,9 +677,13 @@ public function test_get_item_no_permission_public() { } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_wrong_permission_author() { + public function test_get_item_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); $this->setup_sidebar( 'sidebar-1', @@ -567,15 +692,18 @@ public function test_get_item_wrong_permission_author() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/sidebars/sidebar-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } /** - * The test_create_item() method does not exist for sidebar. + * The create_item() method does not exist for sidebar. + * + * @doesNotPerformAssertions */ public function test_create_item() { + // Controller does not implement create_item(). } /** @@ -857,6 +985,73 @@ public function test_get_items_inactive_widgets() { ); } + /** + * @ticket 57531 + * @covers WP_Test_REST_Sidebars_Controller::prepare_item_for_response + */ + public function test_prepare_item_for_response_to_set_inactive_on_theme_switch() { + $request = new WP_REST_Request( 'GET', '/wp/v2/sidebars/sidebar-1' ); + + // Set up the test. + wp_widgets_init(); + $this->setup_widget( + 'widget_rss', + 1, + array( + 'title' => 'RSS test', + ) + ); + $this->setup_widget( + 'widget_text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Sidebar 1', + ), + array( 'text-1', 'rss-1' ) + ); + + // Validate the state before a theme switch. + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + + $this->assertSame( 'active', $data['status'] ); + $this->assertFalse( + get_theme_mod( 'wp_classic_sidebars' ), + 'wp_classic_sidebars theme mod should not exist before switching to block theme' + ); + + switch_theme( 'block-theme' ); + wp_widgets_init(); + + // Validate the state after a theme switch. + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); + + $this->assertSame( + 'inactive', + $data['status'], + 'Sidebar status should have changed to inactive' + ); + $this->assertSame( + array( 'text-1', 'rss-1' ), + $data['widgets'], + 'The text and rss widgets should still in sidebar-1' + ); + $this->assertArrayHasKey( + 'sidebar-1', + get_theme_mod( 'wp_classic_sidebars' ), + 'sidebar-1 should be in "wp_classic_sidebars" theme mod' + ); + } + /** * @ticket 41683 */ @@ -890,15 +1085,21 @@ public function test_update_item_wrong_permission_author() { } /** - * The test_delete_item() method does not exist for sidebar. + * The delete_item() method does not exist for sidebar. + * + * @doesNotPerformAssertions */ public function test_delete_item() { + // Controller does not implement delete_item(). } /** - * The test_prepare_item() method does not exist for sidebar. + * The prepare_item() method does not exist for sidebar. + * + * @doesNotPerformAssertions */ public function test_prepare_item() { + // Controller does not implement prepare_item(). } /** @@ -940,7 +1141,7 @@ protected function remove_links( $data ) { if ( isset( $item['_links'] ) ) { unset( $data[ $count ]['_links'] ); } - $count ++; + ++$count; } return $data; diff --git a/tests/phpunit/tests/rest-api/rest-site-health-controller.php b/tests/phpunit/tests/rest-api/rest-site-health-controller.php index 46f47de0919b9..9d67401d57d60 100644 --- a/tests/phpunit/tests/rest-api/rest-site-health-controller.php +++ b/tests/phpunit/tests/rest-api/rest-site-health-controller.php @@ -4,12 +4,10 @@ * * Also generates the fixture data used by the wp-api.js QUnit tests. * - * @package WordPress + * @package WordPress * @subpackage REST API - * @since 5.6.0 - */ - -/** + * @since 5.6.0 + * * @group restapi */ class WP_Test_REST_Site_Health_Controller extends WP_Test_REST_TestCase { @@ -94,9 +92,57 @@ static function () { $this->assertErrorResponse( 'rest_forbidden', $response, 403 ); } + /** + * @group external-http + */ public function test() { wp_set_current_user( self::$admin ); $response = rest_do_request( '/wp-site-health/v1/tests/dotorg-communication' ); $this->assertSame( 'dotorg_communication', $response->get_data()['test'] ); } + + /** + * Tests Page Cache Rest endpoint registration. + * + * @ticket 56041 + */ + public function test_page_cache_endpoint() { + $server = rest_get_server(); + $routes = $server->get_routes(); + + $endpoint = '/wp-site-health/v1/tests/page-cache'; + $this->assertArrayHasKey( $endpoint, $routes ); + + $route = $routes[ $endpoint ]; + $this->assertCount( 1, $route ); + + $route = current( $route ); + $this->assertSame( + array( WP_REST_Server::READABLE => true ), + $route['methods'] + ); + + $this->assertSame( + 'test_page_cache', + $route['callback'][1] + ); + + $this->assertIsCallable( $route['permission_callback'] ); + + if ( current_user_can( 'view_site_health_checks' ) ) { + $this->assertTrue( call_user_func( $route['permission_callback'] ) ); + } else { + $this->assertFalse( call_user_func( $route['permission_callback'] ) ); + } + + wp_set_current_user( self::factory()->user->create( array( 'role' => 'author' ) ) ); + $this->assertFalse( call_user_func( $route['permission_callback'] ) ); + + $user = wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + if ( is_multisite() ) { + // Site health cap is only available for super admins in Multi sites. + grant_super_admin( $user->ID ); + } + $this->assertTrue( call_user_func( $route['permission_callback'] ) ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-tags-controller.php b/tests/phpunit/tests/rest-api/rest-tags-controller.php index 6ec4bbb689cbe..3b23135c93706 100644 --- a/tests/phpunit/tests/rest-api/rest-tags-controller.php +++ b/tests/phpunit/tests/rest-api/rest-tags-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Tags_Controller extends WP_Test_REST_Controller_Testcase { @@ -54,7 +52,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { // Set up tags for pagination tests. for ( $i = 0; $i < self::$total_tags; $i++ ) { - $tag_ids[] = $factory->tag->create( + self::$tag_ids[] = $factory->tag->create( array( 'name' => "Tag {$i}", ) @@ -135,13 +133,15 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. - $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) ); + $tag1 = self::factory()->tag->create( array( 'name' => 'Season 5' ) ); $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/tags/' . $tag1 ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } @@ -172,7 +172,7 @@ public function test_registered_query_params() { } public function test_get_items() { - $this->factory->tag->create(); + self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'per_page', self::$per_page ); @@ -190,9 +190,9 @@ public function test_get_items_invalid_permission_for_context() { } public function test_get_items_hide_empty_arg() { - $post_id = $this->factory->post->create(); - $tag1 = $this->factory->tag->create( array( 'name' => 'Season 5' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'The Be Sharps' ) ); + $post_id = self::factory()->post->create(); + $tag1 = self::factory()->tag->create( array( 'name' => 'Season 5' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'The Be Sharps' ) ); wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' ); @@ -211,8 +211,8 @@ public function test_get_items_hide_empty_arg() { } public function test_get_items_include_query() { - $id1 = $this->factory->tag->create(); - $id2 = $this->factory->tag->create(); + $id1 = self::factory()->tag->create(); + $id2 = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); @@ -237,8 +237,8 @@ public function test_get_items_include_query() { } public function test_get_items_exclude_query() { - $id1 = $this->factory->tag->create(); - $id2 = $this->factory->tag->create(); + $id1 = self::factory()->tag->create(); + $id2 = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'per_page', self::$per_page ); @@ -286,8 +286,8 @@ public function test_get_items_offset_query() { public function test_get_items_orderby_args() { - $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'Zucchini' ) ); + $tag1 = self::factory()->tag->create( array( 'name' => 'Apple' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'Zucchini' ) ); /* * Tests: @@ -322,9 +322,9 @@ public function test_get_items_orderby_args() { } public function test_get_items_orderby_id() { - $tag0 = $this->factory->tag->create( array( 'name' => 'Cantaloupe' ) ); - $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) ); + $tag0 = self::factory()->tag->create( array( 'name' => 'Cantaloupe' ) ); + $tag1 = self::factory()->tag->create( array( 'name' => 'Apple' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'Banana' ) ); // Defaults to 'orderby' => 'name', 'order' => 'asc'. $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); @@ -358,9 +358,9 @@ public function test_get_items_orderby_id() { } public function test_get_items_orderby_slugs() { - $this->factory->tag->create( array( 'name' => 'Burrito' ) ); - $this->factory->tag->create( array( 'name' => 'Taco' ) ); - $this->factory->tag->create( array( 'name' => 'Chalupa' ) ); + self::factory()->tag->create( array( 'name' => 'Burrito' ) ); + self::factory()->tag->create( array( 'name' => 'Taco' ) ); + self::factory()->tag->create( array( 'name' => 'Chalupa' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'orderby', 'include_slugs' ); @@ -374,10 +374,10 @@ public function test_get_items_orderby_slugs() { } public function test_get_items_post_args() { - $post_id = $this->factory->post->create(); - $tag1 = $this->factory->tag->create( array( 'name' => 'DC' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'Marvel' ) ); - $this->factory->tag->create( array( 'name' => 'Dark Horse' ) ); + $post_id = self::factory()->post->create(); + $tag1 = self::factory()->tag->create( array( 'name' => 'DC' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'Marvel' ) ); + self::factory()->tag->create( array( 'name' => 'Dark Horse' ) ); wp_set_object_terms( $post_id, array( $tag1, $tag2 ), 'post_tag' ); @@ -398,7 +398,7 @@ public function test_get_items_post_args() { } public function test_get_terms_post_args_paging() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_object_terms( $post_id, self::$tag_ids, 'post_tag' ); @@ -410,10 +410,12 @@ public function test_get_terms_post_args_paging() { $response = rest_get_server()->dispatch( $request ); $tags = $response->get_data(); + $this->assertNotEmpty( $tags ); + $i = 0; foreach ( $tags as $tag ) { $this->assertSame( $tag['name'], "Tag {$i}" ); - $i++; + ++$i; } $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); @@ -424,14 +426,16 @@ public function test_get_terms_post_args_paging() { $response = rest_get_server()->dispatch( $request ); $tags = $response->get_data(); + $this->assertNotEmpty( $tags ); + foreach ( $tags as $tag ) { $this->assertSame( $tag['name'], "Tag {$i}" ); - $i++; + ++$i; } } public function test_get_items_post_empty() { - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'post', $post_id ); @@ -446,25 +450,25 @@ public function test_get_items_custom_tax_post_args() { register_taxonomy( 'batman', 'post', array( 'show_in_rest' => true ) ); $controller = new WP_REST_Terms_Controller( 'batman' ); $controller->register_routes(); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'batman', ) ); - $term2 = $this->factory->term->create( + $term2 = self::factory()->term->create( array( 'name' => 'Mask', 'taxonomy' => 'batman', ) ); - $this->factory->term->create( + self::factory()->term->create( array( 'name' => 'Car', 'taxonomy' => 'batman', ) ); - $post_id = $this->factory->post->create(); + $post_id = self::factory()->post->create(); wp_set_object_terms( $post_id, array( $term1, $term2 ), 'batman' ); @@ -478,9 +482,58 @@ public function test_get_items_custom_tax_post_args() { $this->assertSame( 'Cape', $data[0]['name'] ); } + /** + * @ticket 62500 + */ + public function test_get_items_custom_tax_without_post_arg_respects_tax_query_args() { + register_taxonomy( + 'batman', + 'post', + array( + 'show_in_rest' => true, + 'sort' => true, + 'args' => array( + 'order' => 'DESC', + 'orderby' => 'name', + ), + ) + ); + $controller = new WP_REST_Terms_Controller( 'batman' ); + $controller->register_routes(); + $term1 = self::factory()->term->create( + array( + 'name' => 'Cycle', + 'taxonomy' => 'batman', + ) + ); + $term2 = self::factory()->term->create( + array( + 'name' => 'Pod', + 'taxonomy' => 'batman', + ) + ); + $term3 = self::factory()->term->create( + array( + 'name' => 'Cave', + 'taxonomy' => 'batman', + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/batman' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 3, $data ); + $this->assertSame( + array( 'Pod', 'Cycle', 'Cave' ), + array_column( $data, 'name' ) + ); + } + public function test_get_items_search_args() { - $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) ); + $tag1 = self::factory()->tag->create( array( 'name' => 'Apple' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'Banana' ) ); /* * Tests: @@ -503,8 +556,8 @@ public function test_get_items_search_args() { } public function test_get_items_slug_arg() { - $tag1 = $this->factory->tag->create( array( 'name' => 'Apple' ) ); - $tag2 = $this->factory->tag->create( array( 'name' => 'Banana' ) ); + $tag1 = self::factory()->tag->create( array( 'name' => 'Apple' ) ); + $tag2 = self::factory()->tag->create( array( 'name' => 'Banana' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'slug', 'apple' ); @@ -516,10 +569,10 @@ public function test_get_items_slug_arg() { } public function test_get_items_slug_array_arg() { - $id1 = $this->factory->tag->create( array( 'name' => 'Taco' ) ); - $id2 = $this->factory->tag->create( array( 'name' => 'Enchilada' ) ); - $id3 = $this->factory->tag->create( array( 'name' => 'Burrito' ) ); - $this->factory->tag->create( array( 'name' => 'Pizza' ) ); + $id1 = self::factory()->tag->create( array( 'name' => 'Taco' ) ); + $id2 = self::factory()->tag->create( array( 'name' => 'Enchilada' ) ); + $id3 = self::factory()->tag->create( array( 'name' => 'Burrito' ) ); + self::factory()->tag->create( array( 'name' => 'Pizza' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( @@ -539,10 +592,10 @@ public function test_get_items_slug_array_arg() { } public function test_get_items_slug_csv_arg() { - $id1 = $this->factory->tag->create( array( 'name' => 'Taco' ) ); - $id2 = $this->factory->tag->create( array( 'name' => 'Enchilada' ) ); - $id3 = $this->factory->tag->create( array( 'name' => 'Burrito' ) ); - $this->factory->tag->create( array( 'name' => 'Pizza' ) ); + $id1 = self::factory()->tag->create( array( 'name' => 'Taco' ) ); + $id2 = self::factory()->tag->create( array( 'name' => 'Enchilada' ) ); + $id3 = self::factory()->tag->create( array( 'name' => 'Burrito' ) ); + self::factory()->tag->create( array( 'name' => 'Pizza' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'slug', 'taco,burrito, enchilada' ); @@ -556,13 +609,13 @@ public function test_get_items_slug_csv_arg() { public function test_get_terms_private_taxonomy() { register_taxonomy( 'robin', 'post', array( 'public' => false ) ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', ) ); - $term2 = $this->factory->term->create( + $term2 = self::factory()->term->create( array( 'name' => 'Mask', 'taxonomy' => 'robin', @@ -574,12 +627,18 @@ public function test_get_terms_private_taxonomy() { $this->assertErrorResponse( 'rest_no_route', $response, 404 ); } - public function test_get_terms_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_terms_pagination_headers( $method ) { $total_tags = self::$total_tags; $total_pages = (int) ceil( $total_tags / 10 ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); + $request = new WP_REST_Request( $method, '/wp/v2/tags' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_tags, $headers['X-WP-Total'] ); @@ -594,9 +653,9 @@ public function test_get_terms_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // 3rd page. - $this->factory->tag->create(); - $total_tags++; - $total_pages++; + self::factory()->tag->create(); + ++$total_tags; + ++$total_pages; $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); @@ -659,7 +718,7 @@ public function test_get_items_invalid_context() { } public function test_get_item() { - $id = $this->factory->tag->create(); + $id = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id ); $response = rest_get_server()->dispatch( $request ); @@ -670,7 +729,7 @@ public function test_get_item() { * @ticket 39122 */ public function test_get_item_meta() { - $id = $this->factory->tag->create(); + $id = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id ); $response = rest_get_server()->dispatch( $request ); @@ -692,7 +751,7 @@ public function test_get_item_meta() { * @ticket 39122 */ public function test_get_item_meta_registered_for_different_taxonomy() { - $id = $this->factory->tag->create(); + $id = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $id ); $response = rest_get_server()->dispatch( $request ); @@ -710,7 +769,7 @@ public function test_get_term_invalid_term() { } public function test_get_item_invalid_permission_for_context() { - $id = $this->factory->tag->create(); + $id = self::factory()->tag->create(); wp_set_current_user( 0 ); @@ -722,7 +781,7 @@ public function test_get_item_invalid_permission_for_context() { public function test_get_term_private_taxonomy() { register_taxonomy( 'robin', 'post', array( 'public' => false ) ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', @@ -736,7 +795,7 @@ public function test_get_term_private_taxonomy() { public function test_get_item_incorrect_taxonomy() { register_taxonomy( 'robin', 'post' ); - $term1 = $this->factory->term->create( + $term1 = self::factory()->term->create( array( 'name' => 'Cape', 'taxonomy' => 'robin', @@ -827,7 +886,7 @@ public function test_create_item_with_meta() { public function test_create_item_with_meta_wrong_id() { wp_set_current_user( self::$administrator ); - $existing_tag_id = $this->factory->tag->create( array( 'name' => 'My Not So Awesome Term' ) ); + $existing_tag_id = self::factory()->tag->create( array( 'name' => 'My Not So Awesome Term' ) ); $request = new WP_REST_Request( 'POST', '/wp/v2/tags' ); $request->set_param( 'name', 'My Awesome Term' ); @@ -852,7 +911,7 @@ public function test_update_item() { 'slug' => 'original-slug', ); - $term = get_term_by( 'id', $this->factory->tag->create( $orig_args ), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create( $orig_args ), 'post_tag' ); $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'name', 'New Name' ); @@ -880,7 +939,7 @@ public function test_update_item() { public function test_update_item_no_change() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $request = new WP_REST_Request( 'PUT', '/wp/v2/tags/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); @@ -908,7 +967,7 @@ public function test_update_item_invalid_term() { public function test_update_item_incorrect_permissions() { wp_set_current_user( self::$subscriber ); - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'name', 'Incorrect permissions' ); @@ -922,7 +981,7 @@ public function test_update_item_incorrect_permissions() { public function test_update_item_with_edit_term_cap_granted() { wp_set_current_user( self::$subscriber ); - $term = $this->factory->tag->create_and_get(); + $term = self::factory()->tag->create_and_get(); $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'name', 'New Name' ); @@ -949,7 +1008,7 @@ public function grant_edit_term( $caps, $cap ) { public function test_update_item_with_edit_term_cap_revoked() { wp_set_current_user( self::$administrator ); - $term = $this->factory->tag->create_and_get(); + $term = self::factory()->tag->create_and_get(); $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'name', 'New Name' ); @@ -971,7 +1030,7 @@ public function revoke_edit_term( $caps, $cap ) { public function test_update_item_parent_non_hierarchical_taxonomy() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $request = new WP_REST_Request( 'POST', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'parent', REST_TESTS_IMPOSSIBLY_HIGH_NUMBER ); @@ -1098,7 +1157,7 @@ public function test_tag_roundtrip_as_superadmin_html() { public function test_delete_item() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'force', true ); @@ -1112,7 +1171,7 @@ public function test_delete_item() { public function test_delete_item_no_trash() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); @@ -1134,7 +1193,7 @@ public function test_delete_item_invalid_term() { public function test_delete_item_incorrect_permissions() { wp_set_current_user( self::$subscriber ); - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); @@ -1147,7 +1206,7 @@ public function test_delete_item_incorrect_permissions() { public function test_delete_item_with_delete_term_cap_granted() { wp_set_current_user( self::$subscriber ); - $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'force', true ); @@ -1175,7 +1234,7 @@ public function grant_delete_term( $caps, $cap ) { public function test_delete_item_with_delete_term_cap_revoked() { wp_set_current_user( self::$administrator ); - $term = get_term_by( 'id', $this->factory->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create( array( 'name' => 'Deleted Tag' ) ), 'post_tag' ); $request = new WP_REST_Request( 'DELETE', '/wp/v2/tags/' . $term->term_id ); $request->set_param( 'force', true ); @@ -1195,7 +1254,7 @@ public function revoke_delete_term( $caps, $cap ) { } public function test_prepare_item() { - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $term->term_id ); $response = rest_get_server()->dispatch( $request ); @@ -1205,10 +1264,10 @@ public function test_prepare_item() { } public function test_prepare_item_limit_fields() { - $request = new WP_REST_Request; + $request = new WP_REST_Request(); $endpoint = new WP_REST_Terms_Controller( 'post_tag' ); $request->set_param( '_fields', 'id,name' ); - $term = get_term_by( 'id', $this->factory->tag->create(), 'post_tag' ); + $term = get_term_by( 'id', self::factory()->tag->create(), 'post_tag' ); $response = $endpoint->prepare_item_for_response( $term, $request ); $this->assertSame( array( @@ -1270,7 +1329,7 @@ public function test_get_additional_field_registration() { $this->assertArrayHasKey( 'my_custom_int', $data['schema']['properties'] ); $this->assertSame( $schema, $data['schema']['properties']['my_custom_int'] ); - $tag_id = $this->factory->tag->create(); + $tag_id = self::factory()->tag->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags/' . $tag_id ); $response = rest_get_server()->dispatch( $request ); @@ -1300,7 +1359,7 @@ public function test_additional_field_update_errors() { wp_set_current_user( self::$administrator ); - $tag_id = $this->factory->tag->create(); + $tag_id = self::factory()->tag->create(); // Check for error on update. $request = new WP_REST_Request( 'POST', sprintf( '/wp/v2/tags/%d', $tag_id ) ); @@ -1318,14 +1377,22 @@ public function test_additional_field_update_errors() { $wp_rest_additional_fields = array(); } + public function additional_field_get_callback( $response_data, $field_name ) { + return 123; + } + + public function additional_field_update_callback( $value, $tag ) { + if ( 'returnError' === $value ) { + return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); + } + } + /** * @ticket 38504 */ public function test_object_term_queries_are_cached() { - global $wpdb; - - $tags = $this->factory->tag->create_many( 2 ); - $p = $this->factory->post->create(); + $tags = self::factory()->tag->create_many( 2 ); + $p = self::factory()->post->create(); wp_set_object_terms( $p, $tags[0], 'post_tag' ); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); @@ -1335,7 +1402,7 @@ public function test_object_term_queries_are_cached() { unset( $request, $response ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); $request = new WP_REST_Request( 'GET', '/wp/v2/tags' ); $request->set_param( 'post', $p ); @@ -1343,7 +1410,7 @@ public function test_object_term_queries_are_cached() { $found_2 = wp_list_pluck( $response->data, 'id' ); $this->assertSameSets( $found_1, $found_2 ); - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); } /** @@ -1390,16 +1457,6 @@ public function test_editable_response_uses_edit_context() { $this->assertArrayNotHasKey( $view_field, $data ); } - public function additional_field_get_callback( $object, $request ) { - return 123; - } - - public function additional_field_update_callback( $value, $tag ) { - if ( 'returnError' === $value ) { - return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); - } - } - protected function check_get_taxonomy_terms_response( $response ) { $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); @@ -1407,7 +1464,7 @@ protected function check_get_taxonomy_terms_response( $response ) { 'hide_empty' => false, ); $tags = get_terms( 'post_tag', $args ); - $this->assertSame( count( $tags ), count( $data ) ); + $this->assertCount( count( $tags ), $data ); $this->assertSame( $tags[0]->term_id, $data[0]['id'] ); $this->assertSame( $tags[0]->name, $data[0]['name'] ); $this->assertSame( $tags[0]->slug, $data[0]['slug'] ); @@ -1451,4 +1508,143 @@ protected function check_get_taxonomy_term_response( $response, $id ) { $tag = get_term( $id, 'post_tag' ); $this->check_taxonomy_term( $tag, $data, $response->get_links() ); } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/tags' ); + + $filter = new MockAction(); + + add_filter( 'terms_pre_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertEmpty( $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][1] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_Term_Query::class, $args[0][1], 'Query parameters were not captured.' ); + + /** @var WP_Term_Query $query */ + $query = $args[0][1]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'ids', $query->query_vars['fields'], 'The query must fetch only term IDs.' ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertFalse( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be false for HEAD requests.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'ids' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "ids" for non-HEAD requests.' + ); + $this->assertArrayHasKey( 'update_term_meta_cache', $query->query_vars, 'The update_term_meta_cache key is missing in the query vars.' ); + $this->assertTrue( $query->query_vars['update_term_meta_cache'], 'The update_term_meta_cache value should be true for HEAD requests.' ); + } + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $terms_table = preg_quote( $wpdb->terms, '/' ); + + $pattern = '/SELECT\s+t\.term_id.+FROM\s+' . $terms_table . '\s+AS\s+t\s+INNER\s+JOIN/is'; + + // Assert that the SQL query only fetches the term_id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( string $method ) { + $tag_id = self::factory()->tag->create(); + + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/tags/%d', $tag_id ) ); + + $hook_name = 'rest_prepare_post_tag'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $tag_id = self::factory()->tag->create(); + $request = new WP_REST_Request( 'HEAD', sprintf( $path, $tag_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/tags/%d' ), + 'get_items request' => array( '/wp/v2/tags' ), + ); + } } diff --git a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php index 17be251c1d352..4d8a57d5602b8 100644 --- a/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php +++ b/tests/phpunit/tests/rest-api/rest-taxonomies-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Taxonomies_Controller extends WP_Test_REST_Controller_Testcase { @@ -52,7 +50,7 @@ public function test_get_items() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $taxonomies = $this->get_public_taxonomies( get_taxonomies( '', 'objects' ) ); - $this->assertSame( count( $taxonomies ), count( $data ) ); + $this->assertCount( count( $taxonomies ), $data ); $this->assertSame( 'Categories', $data['category']['name'] ); $this->assertSame( 'category', $data['category']['slug'] ); $this->assertTrue( $data['category']['hierarchical'] ); @@ -62,14 +60,32 @@ public function test_get_items() { $this->assertSame( 'tags', $data['post_tag']['rest_base'] ); } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_taxonomy_data() { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/taxonomies' ); + $hook_name = 'rest_prepare_taxonomy'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function test_get_items_context_edit() { wp_set_current_user( self::$contributor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); - $taxonomies = $this->get_public_taxonomies( get_taxonomies( '', 'objects' ) ); - $this->assertSame( count( $taxonomies ), count( $data ) ); + $taxonomies = get_taxonomies( '', 'objects' ); + unset( $taxonomies['nav_menu'] ); // Menus are not editable by contributors. + $taxonomies = $this->get_public_taxonomies( $taxonomies ); + $this->assertCount( count( $taxonomies ), $data ); $this->assertSame( 'Categories', $data['category']['name'] ); $this->assertSame( 'category', $data['category']['slug'] ); $this->assertTrue( $data['category']['hierarchical'] ); @@ -79,14 +95,33 @@ public function test_get_items_context_edit() { $this->assertSame( 'tags', $data['post_tag']['rest_base'] ); } - public function test_get_items_invalid_permission_for_context() { + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + public function test_get_taxonomies_for_type() { $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); $request->set_param( 'type', 'post' ); @@ -94,11 +129,20 @@ public function test_get_taxonomies_for_type() { $this->check_taxonomies_for_type_response( 'post', $response ); } - public function test_get_taxonomies_for_invalid_type() { - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_taxonomies_for_invalid_type( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies' ); $request->set_param( 'type', 'wingding' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); + if ( 'HEAD' === $method ) { + return null; + } $data = $response->get_data(); $this->assertSame( '{}', json_encode( $data ) ); } @@ -109,8 +153,73 @@ public function test_get_item() { $this->check_taxonomy_object_response( 'view', $response ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $request = new WP_REST_Request( 'HEAD', '/wp/v2/taxonomies/category' ); + $hook_name = 'rest_prepare_taxonomy'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'name' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/taxonomies/category' ), + 'get_items request' => array( '/wp/v2/taxonomies' ), + ); + } + public function test_get_item_edit_context() { - $editor_id = $this->factory->user->create( array( 'role' => 'editor' ) ); + $editor_id = self::factory()->user->create( array( 'role' => 'editor' ) ); wp_set_current_user( $editor_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); $request->set_param( 'context', 'edit' ); @@ -118,33 +227,57 @@ public function test_get_item_edit_context() { $this->check_taxonomy_object_response( 'edit', $response ); } - public function test_get_item_invalid_permission_for_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_permission_for_context( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/category' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/category' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } - public function test_get_invalid_taxonomy() { - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/invalid' ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_invalid_taxonomy( $method ) { + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/invalid' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_taxonomy_invalid', $response, 404 ); } - public function test_get_non_public_taxonomy_not_authenticated() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_non_public_taxonomy_not_authenticated( $method ) { register_taxonomy( 'api-private', 'post', array( 'public' => false ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/api-private' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden', $response, 401 ); } - public function test_get_non_public_taxonomy_no_permission() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_non_public_taxonomy_no_permission( $method ) { wp_set_current_user( self::$contributor_id ); register_taxonomy( 'api-private', 'post', array( 'public' => false ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/taxonomies/api-private' ); + $request = new WP_REST_Request( $method, '/wp/v2/taxonomies/api-private' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_forbidden', $response, 403 ); } @@ -172,8 +305,8 @@ public function test_delete_item() { public function test_prepare_item() { $tax = get_taxonomy( 'category' ); - $endpoint = new WP_REST_Taxonomies_Controller; - $request = new WP_REST_Request; + $endpoint = new WP_REST_Taxonomies_Controller(); + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $response = $endpoint->prepare_item_for_response( $tax, $request ); $this->check_taxonomy_object( 'edit', $tax, $response->get_data(), $response->get_links() ); @@ -181,8 +314,8 @@ public function test_prepare_item() { public function test_prepare_item_limit_fields() { $tax = get_taxonomy( 'category' ); - $request = new WP_REST_Request; - $endpoint = new WP_REST_Taxonomies_Controller; + $request = new WP_REST_Request(); + $endpoint = new WP_REST_Taxonomies_Controller(); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'id,name' ); $response = $endpoint->prepare_item_for_response( $tax, $request ); @@ -286,7 +419,7 @@ protected function check_taxonomies_for_type_response( $type, $response ) { $this->assertSame( 200, $response->get_status() ); $data = $response->get_data(); $taxonomies = $this->get_public_taxonomies( get_object_taxonomies( $type, 'objects' ) ); - $this->assertSame( count( $taxonomies ), count( $data ) ); + $this->assertCount( count( $taxonomies ), $data ); } /** @@ -316,5 +449,4 @@ public function test_get_for_taxonomy_returns_terms_controller_if_custom_class_n get_taxonomy( 'test' )->get_rest_controller() ); } - } diff --git a/tests/phpunit/tests/rest-api/rest-templates-controller.php b/tests/phpunit/tests/rest-api/rest-templates-controller.php deleted file mode 100644 index 356c868d76178..0000000000000 --- a/tests/phpunit/tests/rest-api/rest-templates-controller.php +++ /dev/null @@ -1,197 +0,0 @@ -user->create( - array( - 'role' => 'administrator', - ) - ); - - // Set up template post. - $args = array( - 'post_type' => 'wp_template', - 'post_name' => 'my_template', - 'post_title' => 'My Template', - 'post_content' => 'Content', - 'post_excerpt' => 'Description of my template.', - 'tax_input' => array( - 'wp_theme' => array( - get_stylesheet(), - ), - ), - ); - self::$post = self::factory()->post->create_and_get( $args ); - wp_set_post_terms( self::$post->ID, get_stylesheet(), 'wp_theme' ); - } - - public static function wpTearDownAfterClass() { - wp_delete_post( self::$post->ID ); - } - - - public function test_register_routes() { - $routes = rest_get_server()->get_routes(); - $this->assertArrayHasKey( '/wp/v2/templates', $routes ); - $this->assertArrayHasKey( '/wp/v2/templates/(?P[\/\w-]+)', $routes ); - } - - public function test_context_param() { - // TODO: Implement test_context_param() method. - } - - public function test_get_items() { - function find_and_normalize_template_by_id( $templates, $id ) { - foreach ( $templates as $template ) { - if ( $template['id'] === $id ) { - unset( $template['content'] ); - unset( $template['_links'] ); - return $template; - } - } - - return null; - } - - wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, 401 ); - - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - - $this->assertSame( - array( - 'id' => 'default//my_template', - 'theme' => 'default', - 'slug' => 'my_template', - 'source' => 'custom', - 'type' => 'wp_template', - 'description' => 'Description of my template.', - 'title' => array( - 'raw' => 'My Template', - 'rendered' => 'My Template', - ), - 'status' => 'publish', - 'wp_id' => self::$post->ID, - 'has_theme_file' => false, - ), - find_and_normalize_template_by_id( $data, 'default//my_template' ) - ); - } - - public function test_get_item() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - unset( $data['content'] ); - unset( $data['_links'] ); - - $this->assertSame( - array( - 'id' => 'default//my_template', - 'theme' => 'default', - 'slug' => 'my_template', - 'source' => 'custom', - 'type' => 'wp_template', - 'description' => 'Description of my template.', - 'title' => array( - 'raw' => 'My Template', - 'rendered' => 'My Template', - ), - 'status' => 'publish', - 'wp_id' => self::$post->ID, - 'has_theme_file' => false, - ), - $data - ); - } - - public function test_create_item() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); - $request->set_body_params( - array( - 'slug' => 'my_custom_template', - 'description' => 'Just a description', - 'title' => 'My Template', - 'content' => 'Content', - ) - ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - unset( $data['_links'] ); - unset( $data['wp_id'] ); - - $this->assertSame( - array( - 'id' => 'default//my_custom_template', - 'theme' => 'default', - 'content' => array( - 'raw' => 'Content', - ), - 'slug' => 'my_custom_template', - 'source' => 'custom', - 'type' => 'wp_template', - 'description' => 'Just a description', - 'title' => array( - 'raw' => 'My Template', - 'rendered' => 'My Template', - ), - 'status' => 'publish', - 'has_theme_file' => false, - ), - $data - ); - } - - public function test_update_item() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'PUT', '/wp/v2/templates/default//my_template' ); - $request->set_body_params( - array( - 'title' => 'My new Index Title', - ) - ); - $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $this->assertSame( 'My new Index Title', $data['title']['raw'] ); - $this->assertSame( 'custom', $data['source'] ); - } - - public function test_delete_item() { - wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/justrandom//template' ); - $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_template_not_found', $response, 404 ); - } - - public function test_prepare_item() { - // TODO: Implement test_prepare_item() method. - } - - public function test_get_item_schema() { - // TODO: Implement test_get_item_schema() method. - } -} diff --git a/tests/phpunit/tests/rest-api/rest-term-meta-fields.php b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php index eb25ffc226715..737fa90d84633 100644 --- a/tests/phpunit/tests/rest-api/rest-term-meta-fields.php +++ b/tests/phpunit/tests/rest-api/rest-term-meta-fields.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Term_Meta_Fields extends WP_Test_REST_TestCase { @@ -23,7 +21,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ) ); - self::$wp_meta_keys_saved = isset( $GLOBALS['wp_meta_keys'] ) ? $GLOBALS['wp_meta_keys'] : array(); + self::$wp_meta_keys_saved = $GLOBALS['wp_meta_keys'] ?? array(); self::$category_id = $factory->category->create(); self::$customtax_term_id = $factory->term->create( array( 'taxonomy' => 'customtax' ) ); } @@ -194,13 +192,13 @@ public function set_up() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); } protected function grant_write_permission() { // Ensure we have write permission. - $user = $this->factory->user->create( + $user = self::factory()->user->create( array( 'role' => 'editor', ) @@ -327,7 +325,7 @@ public function test_get_value_types() { /** @var WP_REST_Server $wp_rest_server */ global $wp_rest_server; - $wp_rest_server = new Spy_REST_Server; + $wp_rest_server = new Spy_REST_Server(); do_action( 'rest_api_init', $wp_rest_server ); add_term_meta( self::$category_id, 'test_string', 42 ); diff --git a/tests/phpunit/tests/rest-api/rest-test-controller.php b/tests/phpunit/tests/rest-api/rest-test-controller.php index f75de9e45899e..48b326407d9f2 100644 --- a/tests/phpunit/tests/rest-api/rest-test-controller.php +++ b/tests/phpunit/tests/rest-api/rest-test-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_REST_Test_Controller extends WP_REST_Controller { @@ -182,5 +180,4 @@ public function get_item_schema() { return $this->add_additional_fields_schema( $schema ); } - } diff --git a/tests/phpunit/tests/rest-api/rest-themes-controller.php b/tests/phpunit/tests/rest-api/rest-themes-controller.php index 73a8b4c3ccb7d..aeaf92a8a27a1 100644 --- a/tests/phpunit/tests/rest-api/rest-themes-controller.php +++ b/tests/phpunit/tests/rest-api/rest-themes-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi-themes * @group restapi */ @@ -129,6 +127,9 @@ public static function wpTearDownAfterClass() { self::delete_user( self::$subscriber_id ); self::delete_user( self::$contributor_id ); self::delete_user( self::$admin_id ); + + remove_theme_support( 'editor-gradient-presets' ); + remove_theme_support( 'editor-color-palette' ); } /** @@ -161,8 +162,54 @@ public function test_register_routes() { * Test retrieving a collection of themes. * * @ticket 45016 + * @ticket 61021 + * @ticket 62574 */ public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', self::$themes_route ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + + $fields = array( + '_links', + 'author', + 'author_uri', + 'description', + 'is_block_theme', + 'name', + 'requires_php', + 'requires_wp', + 'screenshot', + 'status', + 'stylesheet', + 'stylesheet_uri', + 'tags', + 'template', + 'template_uri', + 'textdomain', + 'theme_uri', + 'version', + ); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + $this->assertSameSets( $fields, array_keys( $data[0] ) ); + + $this->assertContains( 'twentytwenty', wp_list_pluck( $data, 'stylesheet' ) ); + $this->assertContains( get_stylesheet(), wp_list_pluck( $data, 'stylesheet' ) ); + } + + /** + * Test retrieving a collection of active themes. + * + * @ticket 64719 + */ + public function test_get_items_active() { + wp_set_current_user( self::$admin_id ); + $response = self::perform_active_theme_request(); $this->assertSame( 200, $response->get_status() ); @@ -173,27 +220,36 @@ public function test_get_items() { '_links', 'author', 'author_uri', + 'default_template_part_areas', + 'default_template_types', 'description', + 'is_block_theme', 'name', 'requires_php', 'requires_wp', 'screenshot', 'status', 'stylesheet', + 'stylesheet_uri', 'tags', 'template', + 'template_uri', 'textdomain', 'theme_supports', 'theme_uri', 'version', ); + $this->assertIsArray( $data ); + $this->assertCount( 1, $data ); $this->assertSameSets( $fields, array_keys( $data[0] ) ); + $this->assertEquals( array( 'rest-api' ), wp_list_pluck( $data, 'stylesheet' ) ); } /** * Test retrieving a collection of inactive themes. * * @ticket 50152 + * @ticket 61021 */ public function test_get_items_inactive() { wp_set_current_user( self::$admin_id ); @@ -210,18 +266,23 @@ public function test_get_items_inactive() { 'author', 'author_uri', 'description', + 'is_block_theme', 'name', 'requires_php', 'requires_wp', 'screenshot', 'status', 'stylesheet', + 'stylesheet_uri', 'tags', 'template', + 'template_uri', 'textdomain', 'theme_uri', 'version', ); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); $this->assertSameSets( $fields, array_keys( $data[0] ) ); $this->assertContains( 'twentytwenty', wp_list_pluck( $data, 'stylesheet' ) ); @@ -340,12 +401,14 @@ public function test_prepare_item() { * Verify the theme schema. * * @ticket 45016 + * @ticket 61021 + * @ticket 62574 */ public function test_get_item_schema() { $response = self::perform_active_theme_request( 'OPTIONS' ); $data = $response->get_data(); $properties = $data['schema']['properties']; - $this->assertCount( 15, $properties ); + $this->assertCount( 20, $properties ); $this->assertArrayHasKey( 'author', $properties ); $this->assertArrayHasKey( 'raw', $properties['author']['properties'] ); @@ -359,6 +422,11 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'raw', $properties['description']['properties'] ); $this->assertArrayHasKey( 'rendered', $properties['description']['properties'] ); + $this->assertArrayHasKey( 'default_template_part_areas', $properties ); + $this->assertArrayHasKey( 'default_template_types', $properties ); + + $this->assertArrayHasKey( 'is_block_theme', $properties ); + $this->assertArrayHasKey( 'name', $properties ); $this->assertArrayHasKey( 'raw', $properties['name']['properties'] ); $this->assertArrayHasKey( 'rendered', $properties['name']['properties'] ); @@ -368,6 +436,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'screenshot', $properties ); $this->assertArrayHasKey( 'status', $properties ); $this->assertArrayHasKey( 'stylesheet', $properties ); + $this->assertArrayHasKey( 'stylesheet_uri', $properties ); $this->assertArrayHasKey( 'tags', $properties ); $this->assertArrayHasKey( 'raw', $properties['tags']['properties'] ); @@ -375,6 +444,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'rendered', $properties['tags']['properties'] ); $this->assertArrayHasKey( 'template', $properties ); + $this->assertArrayHasKey( 'template_uri', $properties ); $this->assertArrayHasKey( 'textdomain', $properties ); $this->assertArrayHasKey( 'theme_supports', $properties ); @@ -387,6 +457,8 @@ public function test_get_item_schema() { $theme_supports = $properties['theme_supports']['properties']; $this->assertArrayHasKey( 'align-wide', $theme_supports ); $this->assertArrayHasKey( 'automatic-feed-links', $theme_supports ); + $this->assertArrayHasKey( 'block-templates', $theme_supports ); + $this->assertArrayHasKey( 'block-template-parts', $theme_supports, "Theme supports should have 'block-template-parts' key" ); $this->assertArrayHasKey( 'custom-header', $theme_supports ); $this->assertArrayHasKey( 'custom-background', $theme_supports ); $this->assertArrayHasKey( 'custom-logo', $theme_supports ); @@ -395,9 +467,11 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'dark-editor-style', $theme_supports ); $this->assertArrayHasKey( 'disable-custom-font-sizes', $theme_supports ); $this->assertArrayHasKey( 'disable-custom-gradients', $theme_supports ); + $this->assertArrayHasKey( 'disable-layout-styles', $theme_supports ); $this->assertArrayHasKey( 'editor-color-palette', $theme_supports ); $this->assertArrayHasKey( 'editor-font-sizes', $theme_supports ); $this->assertArrayHasKey( 'editor-gradient-presets', $theme_supports ); + $this->assertArrayHasKey( 'editor-spacing-sizes', $theme_supports ); $this->assertArrayHasKey( 'editor-styles', $theme_supports ); $this->assertArrayHasKey( 'formats', $theme_supports ); $this->assertArrayHasKey( 'html5', $theme_supports ); @@ -405,7 +479,7 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'responsive-embeds', $theme_supports ); $this->assertArrayHasKey( 'title-tag', $theme_supports ); $this->assertArrayHasKey( 'wp-block-styles', $theme_supports ); - $this->assertCount( 20, $theme_supports ); + $this->assertCount( 24, $theme_supports, 'There should be 24 theme supports' ); } /** @@ -450,6 +524,32 @@ public function test_theme_description() { ); } + /** + * @ticket 62574 + */ + public function test_theme_default_template_part_areas() { + $response = self::perform_active_theme_request(); + $result = $response->get_data(); + $this->assertArrayHasKey( 'default_template_part_areas', $result[0] ); + $this->assertSame( get_allowed_block_template_part_areas(), $result[0]['default_template_part_areas'] ); + } + + /** + * @ticket 62574 + */ + public function test_theme_default_template_types() { + $response = self::perform_active_theme_request(); + $result = $response->get_data(); + $expected = array(); + foreach ( get_default_block_template_types() as $slug => $template_type ) { + $template_type['slug'] = (string) $slug; + $expected[] = $template_type; + } + + $this->assertArrayHasKey( 'default_template_types', $result[0] ); + $this->assertSame( $expected, $result[0]['default_template_types'] ); + } + /** * @ticket 49906 */ @@ -470,6 +570,27 @@ public function test_theme_requires_wp() { $this->assertSame( '5.3', $result[0]['requires_wp'] ); } + /** + * @ticket 58123 + * @covers WP_REST_Themes_Controller::prepare_item_for_response + */ + public function test_theme_is_block_theme() { + // Test classic theme, activated in test setup. + $response = self::perform_active_theme_request(); + $result = $response->get_data(); + + $this->assertArrayHasKey( 'is_block_theme', $result[0] ); + $this->assertFalse( $result[0]['is_block_theme'] ); + + // Test block theme. + switch_theme( 'block-theme' ); + $response = self::perform_active_theme_request(); + $result = $response->get_data(); + + $this->assertArrayHasKey( 'is_block_theme', $result[0] ); + $this->assertTrue( $result[0]['is_block_theme'] ); + } + /** * @ticket 49906 */ @@ -501,6 +622,37 @@ public function test_theme_stylesheet() { $this->assertSame( 'rest-api', $result[0]['stylesheet'] ); } + /** + * @ticket 61021 + */ + public function test_theme_stylesheet_uri() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', self::$themes_route ); + $request->set_param( 'status', array( 'active', 'inactive' ) ); + + $response = rest_get_server()->dispatch( $request ); + $result = $response->get_data(); + $current_theme = wp_get_theme(); + + foreach ( $result as $theme_result ) { + $this->assertArrayHasKey( 'stylesheet_uri', $theme_result ); + if ( 'active' === $theme_result['status'] ) { + $this->assertSame( + get_stylesheet_directory_uri(), + $theme_result['stylesheet_uri'], + 'stylesheet_uri for an active theme should be the same as the global get_stylesheet_directory_uri()' + ); + } else { + $theme = wp_get_theme( $theme_result['stylesheet'] ); + $this->assertSame( + $theme->get_stylesheet_directory_uri(), + $theme_result['stylesheet_uri'], + "stylesheet_uri for an inactive theme should be the same as the theme's get_stylesheet_directory_uri() method" + ); + } + } + } + /** * @ticket 49906 */ @@ -508,8 +660,8 @@ public function test_theme_tags() { $response = self::perform_active_theme_request(); $result = $response->get_data(); $this->assertArrayHasKey( 'tags', $result[0] ); - $this->assertSame( array( 'holiday', 'custom-menu' ), $result[0]['tags']['raw'] ); - $this->assertSame( 'holiday, custom-menu', $result[0]['tags']['rendered'] ); + $this->assertSame( array( 'Holiday', 'custom-menu' ), $result[0]['tags']['raw'] ); + $this->assertSame( 'Holiday, custom-menu', $result[0]['tags']['rendered'] ); } /** @@ -522,6 +674,37 @@ public function test_theme_template() { $this->assertSame( 'default', $result[0]['template'] ); } + /** + * @ticket 61021 + */ + public function test_theme_template_uri() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', self::$themes_route ); + $request->set_param( 'status', array( 'active', 'inactive' ) ); + + $response = rest_get_server()->dispatch( $request ); + $result = $response->get_data(); + $current_theme = wp_get_theme(); + + foreach ( $result as $theme_result ) { + $this->assertArrayHasKey( 'template_uri', $theme_result ); + if ( 'active' === $theme_result['status'] ) { + $this->assertSame( + get_template_directory_uri(), + $theme_result['template_uri'], + 'template_uri for an active theme should be the same as the global get_template_directory_uri()' + ); + } else { + $theme = wp_get_theme( $theme_result['stylesheet'] ); + $this->assertSame( + $theme->get_template_directory_uri(), + $theme_result['template_uri'], + "template_uri for an inactive theme should be the same as the theme's get_template_directory_uri() method" + ); + } + } + } + /** * @ticket 49906 */ @@ -625,7 +808,7 @@ public function test_theme_supports_editor_font_sizes_array() { $result = $response->get_data(); $this->assertArrayHasKey( 'theme_supports', $result[0] ); $this->assertArrayHasKey( 'editor-font-sizes', $result[0]['theme_supports'] ); - $this->assertEquals( array( $tiny ), $result[0]['theme_supports']['editor-font-sizes'] ); + $this->assertEqualSetsWithIndex( array( $tiny ), $result[0]['theme_supports']['editor-font-sizes'] ); } /** @@ -901,7 +1084,7 @@ public function test_theme_no_wp_block_styles() { /** * @ticket 49037 */ - public function test_theme_wp_block_styles_optin() { + public function test_theme_wp_block_styles_opt_in() { remove_theme_support( 'wp-block-styles' ); add_theme_support( 'wp-block-styles' ); $response = self::perform_active_theme_request(); @@ -925,7 +1108,7 @@ public function test_theme_no_align_wide() { /** * @ticket 49037 */ - public function test_theme_align_wide_optin() { + public function test_theme_align_wide_opt_in() { remove_theme_support( 'align-wide' ); add_theme_support( 'align-wide' ); $response = self::perform_active_theme_request(); @@ -949,7 +1132,7 @@ public function test_theme_no_editor_styles() { /** * @ticket 49037 */ - public function test_theme_editor_styles_optin() { + public function test_theme_editor_styles_opt_in() { remove_theme_support( 'editor-styles' ); add_theme_support( 'editor-styles' ); $response = self::perform_active_theme_request(); @@ -973,7 +1156,7 @@ public function test_theme_no_dark_editor_style() { /** * @ticket 49037 */ - public function test_theme_dark_editor_style_optin() { + public function test_theme_dark_editor_style_opt_in() { remove_theme_support( 'dark-editor-style' ); add_theme_support( 'dark-editor-style' ); $response = self::perform_active_theme_request(); @@ -1199,13 +1382,21 @@ public function additional_field_get_callback( $theme ) { /** * The create_item() method does not exist for themes. + * + * @doesNotPerformAssertions */ - public function test_create_item() {} + public function test_create_item() { + // Controller does not implement create_item(). + } /** * The update_item() method does not exist for themes. + * + * @doesNotPerformAssertions */ - public function test_update_item() {} + public function test_update_item() { + // Controller does not implement update_item(). + } /** * Test single theme. @@ -1225,14 +1416,17 @@ public function test_get_item() { 'author', 'author_uri', 'description', + 'is_block_theme', 'name', 'requires_php', 'requires_wp', 'screenshot', 'status', 'stylesheet', + 'stylesheet_uri', 'tags', 'template', + 'template_uri', 'textdomain', 'theme_uri', 'version', @@ -1285,15 +1479,86 @@ public function test_get_active_item_as_contributor() { } /** - * @ticket 54349 + * @dataProvider data_get_item_non_subdir_theme + * @ticket 54596 + * @covers WP_REST_Themes_Controller::get_item + * + * @param string $theme_dir Theme directory to test. + * @param string $expected_name Expected theme name. */ - public function test_get_item_subdirectory_theme() { + public function test_get_item_non_subdir_theme( $theme_dir, $expected_name ) { wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', self::$themes_route . '/subdir/theme2' ); + $request = new WP_REST_Request( 'GET', self::$themes_route . $theme_dir ); $response = rest_do_request( $request ); $this->assertSame( 200, $response->get_status() ); - $this->assertSame( 'My Subdir Theme', $response->get_data()['name']['raw'] ); + $this->assertSame( $expected_name, $response->get_data()['name']['raw'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_non_subdir_theme() { + return array( + 'parent theme' => array( + 'theme_dir' => '/block-theme', + 'expected_name' => 'Block Theme', + ), + 'child theme' => array( + 'theme_dir' => '/block-theme-child', + 'expected_name' => 'Block Theme Child Theme', + ), + 'theme with _-[]. characters' => array( + 'theme_dir' => '/block_theme-[0.4.0]', + 'expected_name' => 'Block Theme [0.4.0]', + ), + ); + } + + /** + * @dataProvider data_get_item_subdirectory_theme + * @ticket 54349 + * @ticket 54596 + * @covers WP_REST_Themes_Controller::get_item + * + * @param string $theme_dir Theme directory to test. + * @param string $expected_name Expected theme name. + */ + public function test_get_item_subdirectory_theme( $theme_dir, $expected_name ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', self::$themes_route . $theme_dir ); + $response = rest_do_request( $request ); + + $this->assertSame( + 200, + $response->get_status(), + 'A 200 OK status was not returned.' + ); + $this->assertSame( + $expected_name, + $response->get_data()['name']['raw'], + 'The actual theme name was not the expected theme name.' + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_subdirectory_theme() { + return array( + 'theme2' => array( + 'theme_dir' => '/subdir/theme2', + 'expected_name' => 'My Subdir Theme', + ), + 'theme with _-[]. characters' => array( + 'theme_dir' => '/subdir/block_theme-[1.0.0]', + 'expected_name' => 'Block Theme [1.0.0] in subdirectory', + ), + ); } /** @@ -1304,7 +1569,7 @@ public function test_can_support_further_routes() { 'wp/v2', sprintf( '/themes/(?P%s)//test', WP_REST_Themes_Controller::PATTERN ), array( - 'callback' => function ( WP_REST_Request $request ) { + 'callback' => static function ( WP_REST_Request $request ) { return $request['stylesheet']; }, 'permission_callback' => '__return_true', @@ -1322,11 +1587,19 @@ public function test_can_support_further_routes() { /** * The delete_item() method does not exist for themes. + * + * @doesNotPerformAssertions */ - public function test_delete_item() {} + public function test_delete_item() { + // Controller does not implement delete_item(). + } /** * Context is not supported for themes. + * + * @doesNotPerformAssertions */ - public function test_context_param() {} + public function test_context_param() { + // Controller does not use get_context_param(). + } } diff --git a/tests/phpunit/tests/rest-api/rest-users-controller.php b/tests/phpunit/tests/rest-api/rest-users-controller.php index c3f620c8cccd6..b78e95b95f48d 100644 --- a/tests/phpunit/tests/rest-api/rest-users-controller.php +++ b/tests/phpunit/tests/rest-api/rest-users-controller.php @@ -4,9 +4,7 @@ * * @package WordPress * @subpackage REST API - */ - -/** + * * @group restapi */ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { @@ -25,6 +23,11 @@ class WP_Test_REST_Users_Controller extends WP_Test_REST_Controller_Testcase { protected static $site; + /** + * @var WP_REST_Users_Controller + */ + private $endpoint; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$superadmin = $factory->user->create( array( @@ -177,12 +180,14 @@ public function test_context_param() { $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users' ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); // Single. $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/users/' . self::$user ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); } @@ -192,7 +197,7 @@ public function test_registered_query_params() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $keys = array_keys( $data['endpoints'][0]['args'] ); - $this->assertEqualSets( + $this->assertSameSets( array( 'context', 'exclude', @@ -207,6 +212,8 @@ public function test_registered_query_params() { 'search', 'slug', 'who', + 'search_columns', + 'has_published_posts', ), $keys ); @@ -227,14 +234,29 @@ public function test_get_items() { $this->check_user_data( $userdata, $data, 'view', $data['_links'] ); } - public function test_get_items_with_edit_context() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_with_edit_context( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); - $this->assertSame( 200, $response->get_status() ); + $this->assertSame( + 200, + $response->get_status(), + sprintf( 'Expected HTTP status code 200 but got %s.', $response->get_status() ) + ); + + if ( 'HEAD' === $method ) { + $this->assertSame( array(), $response->get_data(), 'Expected null response data for HEAD request, but received non-null data.' ); + return null; + } $all_data = $response->get_data(); $data = $all_data[0]; @@ -242,9 +264,27 @@ public function test_get_items_with_edit_context() { $this->check_user_data( $userdata, $data, 'edit', $data['_links'] ); } - public function test_get_items_with_edit_context_without_permission() { + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_with_edit_context_without_permission( $method ) { // Test with a user not logged in. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); @@ -254,7 +294,7 @@ public function test_get_items_with_edit_context_without_permission() { // capability in question: 'list_users'. wp_set_current_user( self::$editor ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); @@ -268,6 +308,8 @@ public function test_get_items_unauthenticated_includes_authors_of_post_types_sh $rest_post_types = array_values( get_post_types( array( 'show_in_rest' => true ), 'names' ) ); + $this->assertNotEmpty( $users ); + foreach ( $users as $user ) { $this->assertNotEmpty( count_user_posts( $user['id'], $rest_post_types ) ); @@ -312,14 +354,20 @@ public function test_get_items_unauthenticated_does_not_include_users_without_pu $this->assertNotContains( self::$user, $user_ids ); } - public function test_get_items_pagination_headers() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_pagination_headers( $method ) { $total_users = self::$total_users; $total_pages = (int) ceil( $total_users / 10 ); wp_set_current_user( self::$user ); // Start of the index. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); $this->assertSame( $total_users, $headers['X-WP-Total'] ); @@ -334,10 +382,10 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // 3rd page. - $this->factory->user->create(); - $total_users++; - $total_pages++; - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + self::factory()->user->create(); + ++$total_users; + ++$total_pages; + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', 3 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -359,7 +407,7 @@ public function test_get_items_pagination_headers() { $this->assertStringContainsString( '<' . $next_link . '>; rel="next"', $headers['Link'] ); // Last page. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', $total_pages ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -375,7 +423,7 @@ public function test_get_items_pagination_headers() { $this->assertStringNotContainsString( 'rel="next"', $headers['Link'] ); // Out of bounds. - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'page', 100 ); $response = rest_get_server()->dispatch( $request ); $headers = $response->get_headers(); @@ -404,14 +452,24 @@ public function test_get_items_per_page() { $this->assertCount( 5, $response->get_data() ); } - public function test_get_items_page() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_page( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request = new WP_REST_Request( $method, '/wp/v2/users' ); $request->set_param( 'per_page', 5 ); $request->set_param( 'page', 2 ); $response = rest_get_server()->dispatch( $request ); - $this->assertCount( 5, $response->get_data() ); + + if ( 'HEAD' !== $method ) { + $this->assertCount( 5, $response->get_data() ); + } + $prev_link = add_query_arg( array( 'per_page' => 5, @@ -426,9 +484,9 @@ public function test_get_items_page() { public function test_get_items_orderby_name() { wp_set_current_user( self::$user ); - $low_id = $this->factory->user->create( array( 'display_name' => 'AAAAA' ) ); - $mid_id = $this->factory->user->create( array( 'display_name' => 'NNNNN' ) ); - $high_id = $this->factory->user->create( array( 'display_name' => 'ZZZZ' ) ); + $low_id = self::factory()->user->create( array( 'display_name' => 'AAAAA' ) ); + $mid_id = self::factory()->user->create( array( 'display_name' => 'NNNNN' ) ); + $high_id = self::factory()->user->create( array( 'display_name' => 'ZZZZ' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'orderby', 'name' ); @@ -450,8 +508,8 @@ public function test_get_items_orderby_name() { public function test_get_items_orderby_url() { wp_set_current_user( self::$user ); - $low_id = $this->factory->user->create( array( 'user_url' => 'http://a.com' ) ); - $high_id = $this->factory->user->create( array( 'user_url' => 'http://b.com' ) ); + $low_id = self::factory()->user->create( array( 'user_url' => 'http://a.com' ) ); + $high_id = self::factory()->user->create( array( 'user_url' => 'http://b.com' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'orderby', 'url' ); @@ -475,8 +533,8 @@ public function test_get_items_orderby_url() { public function test_get_items_orderby_slug() { wp_set_current_user( self::$user ); - $high_id = $this->factory->user->create( array( 'user_nicename' => 'blogin' ) ); - $low_id = $this->factory->user->create( array( 'user_nicename' => 'alogin' ) ); + $high_id = self::factory()->user->create( array( 'user_nicename' => 'blogin' ) ); + $low_id = self::factory()->user->create( array( 'user_nicename' => 'alogin' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'orderby', 'slug' ); @@ -500,9 +558,9 @@ public function test_get_items_orderby_slug() { public function test_get_items_orderby_slugs() { wp_set_current_user( self::$user ); - $this->factory->user->create( array( 'user_nicename' => 'burrito' ) ); - $this->factory->user->create( array( 'user_nicename' => 'taco' ) ); - $this->factory->user->create( array( 'user_nicename' => 'chalupa' ) ); + self::factory()->user->create( array( 'user_nicename' => 'burrito' ) ); + self::factory()->user->create( array( 'user_nicename' => 'taco' ) ); + self::factory()->user->create( array( 'user_nicename' => 'chalupa' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'orderby', 'include_slugs' ); @@ -518,8 +576,8 @@ public function test_get_items_orderby_slugs() { public function test_get_items_orderby_email() { wp_set_current_user( self::$user ); - $high_id = $this->factory->user->create( array( 'user_email' => 'bemail@gmail.com' ) ); - $low_id = $this->factory->user->create( array( 'user_email' => 'aemail@gmail.com' ) ); + $high_id = self::factory()->user->create( array( 'user_email' => 'bemail@gmail.com' ) ); + $low_id = self::factory()->user->create( array( 'user_email' => 'aemail@gmail.com' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'orderby', 'email' ); @@ -598,8 +656,8 @@ public function test_get_items_offset() { public function test_get_items_include_query() { wp_set_current_user( self::$user ); - $id1 = $this->factory->user->create(); - $id2 = $this->factory->user->create(); + $id1 = self::factory()->user->create(); + $id2 = self::factory()->user->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); @@ -628,14 +686,13 @@ public function test_get_items_include_query() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertCount( 0, $data ); - } public function test_get_items_exclude_query() { wp_set_current_user( self::$user ); - $id1 = $this->factory->user->create(); - $id2 = $this->factory->user->create(); + $id1 = self::factory()->user->create(); + $id2 = self::factory()->user->create(); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'per_page', self::$per_page ); // There are >10 users at this point. @@ -666,14 +723,14 @@ public function test_get_items_search() { $response = rest_get_server()->dispatch( $request ); $this->assertCount( 0, $response->get_data() ); - $yolo_id = $this->factory->user->create( array( 'display_name' => 'yololololo' ) ); + $yolo_id = self::factory()->user->create( array( 'display_name' => 'yololololo' ) ); $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); $request->set_param( 'search', 'yololololo' ); $response = rest_get_server()->dispatch( $request ); $this->assertCount( 1, $response->get_data() ); // Default to wildcard search. - $adam_id = $this->factory->user->create( + $adam_id = self::factory()->user->create( array( 'role' => 'author', 'user_nicename' => 'adam', @@ -688,16 +745,92 @@ public function test_get_items_search() { $this->assertSame( $adam_id, $data[0]['id'] ); } + public function test_get_items_search_fields() { + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 0, $response->get_data() ); + + $yolo_id = self::factory()->user->create( array( 'user_email' => 'yololololo@example.localhost' ) ); + + wp_set_current_user( self::$user ); + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + + wp_set_current_user( self::$editor ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 0, $response->get_data() ); + } + + /** + * @ticket 62596 + */ + public function test_get_items_search_columns() { + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 0, $response->get_data() ); + + self::factory()->user->create( + array( + 'display_name' => 'Adam', + 'user_email' => 'yololololo@example.localhost', + ) + ); + + wp_set_current_user( self::$user ); + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $request->set_param( 'search_columns', 'email' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $request->set_param( 'search_columns', 'name' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 0, $response->get_data() ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'Adam' ); + $request->set_param( 'search_columns', 'name' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 1, $response->get_data() ); + } + + /** + * @ticket 62596 + */ + public function test_get_items_search_columns_without_permission() { + self::factory()->user->create( + array( + 'display_name' => 'Adam', + 'user_email' => 'yololololo@example.localhost', + ) + ); + + // Test user without sufficient capabilities - 'list_users'. + wp_set_current_user( self::$editor ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/users' ); + $request->set_param( 'search', 'yololololo' ); + $request->set_param( 'search_columns', 'email' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertCount( 0, $response->get_data() ); + } + public function test_get_items_slug_query() { wp_set_current_user( self::$user ); - $this->factory->user->create( + self::factory()->user->create( array( 'display_name' => 'foo', 'user_login' => 'bar', ) ); - $id2 = $this->factory->user->create( + $id2 = self::factory()->user->create( array( 'display_name' => 'Moo', 'user_login' => 'foo', @@ -715,25 +848,25 @@ public function test_get_items_slug_query() { public function test_get_items_slug_array_query() { wp_set_current_user( self::$user ); - $id1 = $this->factory->user->create( + $id1 = self::factory()->user->create( array( 'display_name' => 'Taco', 'user_login' => 'taco', ) ); - $id2 = $this->factory->user->create( + $id2 = self::factory()->user->create( array( 'display_name' => 'Enchilada', 'user_login' => 'enchilada', ) ); - $id3 = $this->factory->user->create( + $id3 = self::factory()->user->create( array( 'display_name' => 'Burrito', 'user_login' => 'burrito', ) ); - $this->factory->user->create( + self::factory()->user->create( array( 'display_name' => 'Hon Pizza', 'user_login' => 'pizza', @@ -761,25 +894,25 @@ public function test_get_items_slug_array_query() { public function test_get_items_slug_csv_query() { wp_set_current_user( self::$user ); - $id1 = $this->factory->user->create( + $id1 = self::factory()->user->create( array( 'display_name' => 'Taco', 'user_login' => 'taco', ) ); - $id2 = $this->factory->user->create( + $id2 = self::factory()->user->create( array( 'display_name' => 'Enchilada', 'user_login' => 'enchilada', ) ); - $id3 = $this->factory->user->create( + $id3 = self::factory()->user->create( array( 'display_name' => 'Burrito', 'user_login' => 'burrito', ) ); - $this->factory->user->create( + self::factory()->user->create( array( 'display_name' => 'Hon Pizza', 'user_login' => 'pizza', @@ -956,7 +1089,7 @@ public function test_get_items_who_unauthorized_query() { } public function test_get_item() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); wp_set_current_user( self::$user ); @@ -968,7 +1101,7 @@ public function test_get_item() { public function test_prepare_item() { wp_set_current_user( self::$user ); - $request = new WP_REST_Request; + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $user = get_user_by( 'id', get_current_user_id() ); $data = $this->endpoint->prepare_item_for_response( $user, $request ); @@ -978,7 +1111,7 @@ public function test_prepare_item() { public function test_prepare_item_limit_fields() { wp_set_current_user( self::$user ); - $request = new WP_REST_Request; + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $request->set_param( '_fields', 'id,name' ); $user = get_user_by( 'id', get_current_user_id() ); @@ -1023,7 +1156,7 @@ public function test_get_user_empty_capabilities() { $this->allow_user_to_manage_multisite(); - $lolz = $this->factory->user->create( + $lolz = self::factory()->user->create( array( 'display_name' => 'lolz', 'roles' => '', @@ -1108,27 +1241,27 @@ public function test_cannot_get_item_author_of_draft() { } public function test_get_item_published_author_post() { - $this->author_id = $this->factory->user->create( + $author_id = self::factory()->user->create( array( 'role' => 'author', ) ); - $this->post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( - 'post_author' => $this->author_id, + 'post_author' => $author_id, ) ); wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->check_get_user_response( $response, 'embed' ); } public function test_get_item_published_author_pages() { - $this->author_id = $this->factory->user->create( + $author_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -1136,13 +1269,13 @@ public function test_get_item_published_author_pages() { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $author_id ) ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 401, $response->get_status() ); - $this->post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( - 'post_author' => $this->author_id, + 'post_author' => $author_id, 'post_type' => 'page', ) ); @@ -1152,7 +1285,7 @@ public function test_get_item_published_author_pages() { } public function test_get_user_with_edit_context() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); @@ -1163,37 +1296,46 @@ public function test_get_user_with_edit_context() { } public function test_get_item_published_author_wrong_context() { - $this->author_id = $this->factory->user->create( + $author_id = self::factory()->user->create( array( 'role' => 'author', ) ); - $this->post_id = $this->factory->post->create( + $post_id = self::factory()->post->create( array( - 'post_author' => $this->author_id, + 'post_author' => $author_id, ) ); wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $this->author_id ) ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/users/%d', $author_id ) ); $request->set_param( 'context', 'edit' ); $response = rest_get_server()->dispatch( $request ); - $this->assertErrorResponse( 'rest_user_cannot_view', $response, 401 ); + $this->assertErrorResponse( 'rest_forbidden_context', $response, 401 ); } - public function test_get_current_user() { + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_current_user( $method ) { wp_set_current_user( self::$user ); - $request = new WP_REST_Request( 'GET', '/wp/v2/users/me' ); + $request = new WP_REST_Request( $method, '/wp/v2/users/me' ); $response = rest_get_server()->dispatch( $request ); $this->assertSame( 200, $response->get_status() ); - $this->check_get_user_response( $response, 'view' ); - $headers = $response->get_headers(); $this->assertArrayNotHasKey( 'Location', $headers ); + if ( 'HEAD' === $method ) { + // HEAD responses only contain headers. Bail. + return null; + } + $this->check_get_user_response( $response, 'view' ); $links = $response->get_links(); $this->assertSame( rest_url( 'wp/v2/users/' . self::$user ), $links['self'][0]['href'] ); } @@ -1224,7 +1366,7 @@ public function test_create_item() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1257,7 +1399,7 @@ public function test_create_item_invalid_username() { } $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); @@ -1302,7 +1444,7 @@ public function test_create_item_illegal_username() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1332,7 +1474,7 @@ public function test_create_new_network_user_on_site_does_not_add_user_to_sub_si ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1363,7 +1505,7 @@ public function test_create_new_network_user_with_add_user_to_blog_failure() { add_filter( 'can_add_user_to_blog', '__return_false' ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'user_cannot_be_added', $response ); @@ -1386,7 +1528,7 @@ public function test_create_new_network_user_on_sub_site_adds_user_to_site() { switch_to_blog( self::$site ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1416,7 +1558,7 @@ public function test_create_existing_network_user_on_sub_site_has_error() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); @@ -1425,7 +1567,7 @@ public function test_create_existing_network_user_on_sub_site_has_error() { switch_to_blog( self::$site ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $switched_response = rest_get_server()->dispatch( $request ); @@ -1444,7 +1586,10 @@ public function test_create_existing_network_user_on_sub_site_has_error() { if ( 'user_name' === $error['code'] ) { $this->assertSame( 'Sorry, that username already exists!', $error['message'] ); } else { - $this->assertSame( 'Sorry, that email address is already used!', $error['message'] ); + $expected = 'Error: This email address is already registered. ' . + 'Log in with ' . + 'this address or choose another one.'; + $this->assertSame( $expected, $error['message'] ); } } } @@ -1461,7 +1606,7 @@ public function test_json_create_user() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1478,7 +1623,7 @@ public function test_create_user_without_permission() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1498,7 +1643,7 @@ public function test_create_user_invalid_id() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1517,7 +1662,7 @@ public function test_create_user_invalid_email() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1537,7 +1682,7 @@ public function test_create_user_invalid_role() { ); $request = new WP_REST_Request( 'POST', '/wp/v2/users' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -1545,7 +1690,7 @@ public function test_create_user_invalid_role() { } public function test_update_item() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'user_email' => 'test@example.com', 'user_pass' => 'sjflsfls', @@ -1570,7 +1715,7 @@ public function test_update_item() { $_POST['locale'] = 'de_DE'; $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $_POST ); $response = rest_get_server()->dispatch( $request ); @@ -1611,13 +1756,13 @@ public function test_update_item_no_change() { } public function test_update_item_existing_email() { - $user1 = $this->factory->user->create( + $user1 = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', ) ); - $user2 = $this->factory->user->create( + $user2 = self::factory()->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com', @@ -1675,7 +1820,7 @@ public function test_update_item_existing_email_case_not_own() { } public function test_update_item_invalid_locale() { - $user1 = $this->factory->user->create( + $user1 = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', @@ -1694,7 +1839,7 @@ public function test_update_item_invalid_locale() { } public function test_update_item_en_US_locale() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', @@ -1718,7 +1863,7 @@ public function test_update_item_en_US_locale() { * @ticket 38632 */ public function test_update_item_empty_locale() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', @@ -1742,13 +1887,13 @@ public function test_update_item_empty_locale() { } public function test_update_item_username_attempt() { - $user1 = $this->factory->user->create( + $user1 = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', ) ); - $user2 = $this->factory->user->create( + $user2 = self::factory()->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com', @@ -1767,13 +1912,13 @@ public function test_update_item_username_attempt() { } public function test_update_item_existing_nicename() { - $user1 = $this->factory->user->create( + $user1 = self::factory()->user->create( array( 'user_login' => 'test_json_user', 'user_email' => 'testjson@example.com', ) ); - $user2 = $this->factory->user->create( + $user2 = self::factory()->user->create( array( 'user_login' => 'test_json_user2', 'user_email' => 'testjson2@example.com', @@ -1792,7 +1937,7 @@ public function test_update_item_existing_nicename() { } public function test_json_update_user() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'user_email' => 'testjson2@example.com', 'user_pass' => 'sjflsfl3sdjls', @@ -1817,7 +1962,7 @@ public function test_json_update_user() { $pw_before = $userdata->user_pass; $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); - $request->add_header( 'content-type', 'application/json' ); + $request->add_header( 'Content-Type', 'application/json' ); $request->set_body( wp_json_encode( $params ) ); $response = rest_get_server()->dispatch( $request ); @@ -1837,7 +1982,7 @@ public function test_json_update_user() { } public function test_update_user_role() { - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( self::$user ); @@ -1858,7 +2003,7 @@ public function test_update_user_role() { } public function test_update_user_multiple_roles() { - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( self::$user ); @@ -1904,7 +2049,7 @@ public function test_update_user_role_invalid_privilege_escalation() { * @group ms-excluded */ public function test_update_user_role_invalid_privilege_deescalation() { - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $user_id ); @@ -1933,7 +2078,7 @@ public function test_update_user_role_invalid_privilege_deescalation() { * @group ms-required */ public function test_update_user_role_privilege_deescalation_multisite() { - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $user_id ); $user = wp_get_current_user(); @@ -1947,7 +2092,7 @@ public function test_update_user_role_privilege_deescalation_multisite() { $this->assertSame( 'editor', $new_data['roles'][0] ); $this->assertNotEquals( 'administrator', $new_data['roles'][0] ); - $user_id = $this->factory->user->create( array( 'role' => 'administrator' ) ); + $user_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); wp_set_current_user( $user_id ); $user = wp_get_current_user(); @@ -1999,14 +2144,14 @@ public function test_update_user_without_permission() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', self::$user ) ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_edit', $response, 403 ); $request = new WP_REST_Request( 'PUT', '/wp/v2/users/me' ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -2026,7 +2171,7 @@ public function test_update_user_invalid_id() { ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', self::$editor ) ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( $params ); $response = rest_get_server()->dispatch( $request ); @@ -2037,7 +2182,7 @@ public function test_update_user_invalid_id() { * @ticket 40263 */ public function test_update_item_only_roles_as_editor() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2055,7 +2200,7 @@ public function test_update_item_only_roles_as_editor() { * @ticket 40263 */ public function test_update_item_only_roles_as_site_administrator() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2076,7 +2221,7 @@ public function test_update_item_only_roles_as_site_administrator() { * @ticket 40263 */ public function test_update_item_including_roles_and_other_params() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2332,7 +2477,7 @@ public function test_user_roundtrip_as_superadmin_html() { } public function test_delete_item() { - $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) ); + $user_id = self::factory()->user->create( array( 'display_name' => 'Deleted User' ) ); $this->allow_user_to_manage_multisite(); @@ -2357,7 +2502,7 @@ public function test_delete_item() { } public function test_delete_item_no_trash() { - $user_id = $this->factory->user->create( array( 'display_name' => 'Deleted User' ) ); + $user_id = self::factory()->user->create( array( 'display_name' => 'Deleted User' ) ); $this->allow_user_to_manage_multisite(); @@ -2387,7 +2532,7 @@ public function test_delete_item_no_trash() { } public function test_delete_current_item() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'administrator', 'display_name' => 'Deleted User', @@ -2416,7 +2561,7 @@ public function test_delete_current_item() { } public function test_delete_current_item_no_trash() { - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'administrator', 'display_name' => 'Deleted User', @@ -2449,7 +2594,7 @@ public function test_delete_current_item_no_trash() { } public function test_delete_user_without_permission() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); @@ -2487,15 +2632,15 @@ public function test_delete_user_reassign() { $this->allow_user_to_manage_multisite(); // Test with a new user, to avoid any complications. - $user_id = $this->factory->user->create(); - $reassign_id = $this->factory->user->create(); - $test_post = $this->factory->post->create( + $user_id = self::factory()->user->create(); + $reassign_id = self::factory()->user->create(); + $test_post = self::factory()->post->create( array( 'post_author' => $user_id, ) ); - // Sanity check to ensure the factory created the post correctly. + // Confidence check to ensure the factory created the post correctly. $post = get_post( $test_post ); $this->assertEquals( $user_id, $post->post_author ); @@ -2521,7 +2666,7 @@ public function test_delete_user_reassign() { } public function test_delete_user_invalid_reassign_id() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); @@ -2542,7 +2687,7 @@ public function test_delete_user_invalid_reassign_id() { } public function test_delete_user_invalid_reassign_passed_as_string() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); @@ -2557,13 +2702,13 @@ public function test_delete_user_invalid_reassign_passed_as_string() { } public function test_delete_user_reassign_passed_as_boolean_false_trashes_post() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); wp_set_current_user( self::$user ); - $test_post = $this->factory->post->create( + $test_post = self::factory()->post->create( array( 'post_author' => $user_id, ) @@ -2585,13 +2730,13 @@ public function test_delete_user_reassign_passed_as_boolean_false_trashes_post() } public function test_delete_user_reassign_passed_as_string_false_trashes_post() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); wp_set_current_user( self::$user ); - $test_post = $this->factory->post->create( + $test_post = self::factory()->post->create( array( 'post_author' => $user_id, ) @@ -2613,13 +2758,13 @@ public function test_delete_user_reassign_passed_as_string_false_trashes_post() } public function test_delete_user_reassign_passed_as_empty_string_trashes_post() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); wp_set_current_user( self::$user ); - $test_post = $this->factory->post->create( + $test_post = self::factory()->post->create( array( 'post_author' => $user_id, ) @@ -2641,13 +2786,13 @@ public function test_delete_user_reassign_passed_as_empty_string_trashes_post() } public function test_delete_user_reassign_passed_as_0_reassigns_author() { - $user_id = $this->factory->user->create(); + $user_id = self::factory()->user->create(); $this->allow_user_to_manage_multisite(); wp_set_current_user( self::$user ); - $test_post = $this->factory->post->create( + $test_post = self::factory()->post->create( array( 'post_author' => $user_id, ) @@ -2694,7 +2839,6 @@ public function test_get_item_schema() { $this->assertArrayHasKey( 'url', $properties ); $this->assertArrayHasKey( 'username', $properties ); $this->assertArrayHasKey( 'roles', $properties ); - } public function test_get_item_schema_show_avatar() { @@ -2816,13 +2960,24 @@ public function test_additional_field_update_errors() { $wp_rest_additional_fields = array(); } + public function additional_field_get_callback( $response_data, $field_name ) { + return get_user_meta( $response_data['id'], $field_name, true ); + } + + public function additional_field_update_callback( $value, $user, $field_name ) { + if ( 'returnError' === $value ) { + return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); + } + update_user_meta( $user->ID, $field_name, $value ); + } + /** * @ticket 39701 * @group ms-required */ public function test_get_item_from_different_site_as_site_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2842,7 +2997,7 @@ public function test_get_item_from_different_site_as_site_administrator() { */ public function test_get_item_from_different_site_as_network_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2862,7 +3017,7 @@ public function test_get_item_from_different_site_as_network_administrator() { */ public function test_update_item_from_different_site_as_site_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2872,7 +3027,7 @@ public function test_update_item_from_different_site_as_site_administrator() { wp_set_current_user( self::$user ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( array( 'first_name' => 'New Name' ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); @@ -2884,7 +3039,7 @@ public function test_update_item_from_different_site_as_site_administrator() { */ public function test_update_item_from_different_site_as_network_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2894,7 +3049,7 @@ public function test_update_item_from_different_site_as_network_administrator() wp_set_current_user( self::$superadmin ); $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/users/%d', $user_id ) ); - $request->add_header( 'content-type', 'application/x-www-form-urlencoded' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); $request->set_body_params( array( 'first_name' => 'New Name' ) ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_user_invalid_id', $response, 404 ); @@ -2906,7 +3061,7 @@ public function test_update_item_from_different_site_as_network_administrator() */ public function test_delete_item_from_different_site_as_site_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -2928,7 +3083,7 @@ public function test_delete_item_from_different_site_as_site_administrator() { */ public function test_delete_item_from_different_site_as_network_administrator() { switch_to_blog( self::$site ); - $user_id = $this->factory->user->create( + $user_id = self::factory()->user->create( array( 'role' => 'author', ) @@ -3063,15 +3218,127 @@ public function data_get_default_data() { ); } - public function additional_field_get_callback( $object ) { - return get_user_meta( $object['id'], 'my_custom_int', true ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + wp_set_current_user( self::$user ); + $request = new WP_REST_Request( $method, sprintf( '/wp/v2/users/%d', self::$user ) ); + + $hook_name = 'rest_prepare_user'; + + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); } - public function additional_field_update_callback( $value, $user ) { - if ( 'returnError' === $value ) { - return new WP_Error( 'rest_invalid_param', 'Testing an error.', array( 'status' => 400 ) ); + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method HTTP method to use. + */ + public function test_get_items_only_fetches_ids_for_head_requests( $method ) { + $is_head_request = 'HEAD' === $method; + $request = new WP_REST_Request( $method, '/wp/v2/users' ); + + $filter = new MockAction(); + + add_filter( 'pre_user_query', array( $filter, 'filter' ), 10, 2 ); + + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + if ( $is_head_request ) { + $this->assertSame( array(), $response->get_data() ); + } else { + $this->assertNotEmpty( $response->get_data() ); + } + + $args = $filter->get_args(); + $this->assertTrue( isset( $args[0][0] ), 'Query parameters were not captured.' ); + $this->assertInstanceOf( WP_User_Query::class, $args[0][0], 'Query parameters were not captured.' ); + + /** @var WP_User $query */ + $query = $args[0][0]; + + if ( $is_head_request ) { + $this->assertArrayHasKey( 'fields', $query->query_vars, 'The fields parameter is not set in the query vars.' ); + $this->assertSame( 'id', $query->query_vars['fields'], 'The query must fetch only user IDs.' ); + } else { + $this->assertTrue( + ! array_key_exists( 'fields', $query->query_vars ) || 'id' !== $query->query_vars['fields'], + 'The fields parameter should not be forced to "id" for non-HEAD requests.' + ); } - update_user_meta( $user->ID, 'my_custom_int', $value ); + + if ( ! $is_head_request ) { + return; + } + + global $wpdb; + $users_table = preg_quote( $wpdb->users, '/' ); + $pattern = '/^SELECT\s+SQL_CALC_FOUND_ROWS\s+' . $users_table . '\.ID\n\s+FROM\s+' . $users_table . '/is'; + + // Assert that the SQL query only fetches the id column. + $this->assertMatchesRegularExpression( $pattern, $query->request, 'The SQL query does not match the expected string.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + $user_id = self::factory()->user->create(); + wp_set_current_user( self::$user ); + + $request = new WP_REST_Request( 'HEAD', sprintf( $path, $user_id ) ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/users/%d' ), + 'get_items request' => array( '/wp/v2/users' ), + ); } protected function check_user_data( $user, $data, $context, $links ) { @@ -3094,9 +3361,7 @@ protected function check_user_data( $user, $data, $context, $links ) { $this->assertSame( $user->user_login, $data['username'] ); $this->assertSame( $user->roles, $data['roles'] ); $this->assertSame( get_user_locale( $user ), $data['locale'] ); - } - - if ( 'edit' !== $context ) { + } else { $this->assertArrayNotHasKey( 'roles', $data ); $this->assertArrayNotHasKey( 'capabilities', $data ); $this->assertArrayNotHasKey( 'registered_date', $data ); diff --git a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php index a7d1d47d61f9e..3003c2e9741de 100644 --- a/tests/phpunit/tests/rest-api/rest-widget-types-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widget-types-controller.php @@ -5,16 +5,12 @@ * @package WordPress * @subpackage REST_API * @since 5.8.0 - */ - -/** - * Tests for WP_REST_Widget_Types_Controller. * - * @since 5.8.0 + * @covers WP_REST_Widget_Types_Controller * * @see WP_TEST_REST_Controller_Testcase * @group restapi - * @covers WP_REST_Widget_Types_Controller + * @group widgets */ class WP_Test_REST_Widget_Types_Controller extends WP_Test_REST_Controller_Testcase { @@ -118,13 +114,24 @@ public function test_get_items() { $response = rest_get_server()->dispatch( $request ); $data = $response->get_data(); $this->assertGreaterThan( 1, count( $data ) ); - $endpoint = new WP_REST_Widget_Types_Controller; + $endpoint = new WP_REST_Widget_Types_Controller(); foreach ( $data as $item ) { $widget_type = $endpoint->get_widget( $item['name'] ); $this->check_widget_type_object( $widget_type, $item, $item['_links'] ); } } + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_widget_types_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/widget-types' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + /** * @ticket 53303 */ @@ -165,7 +172,7 @@ public function test_get_items_removes_duplicates() { $data = $response->get_data(); $text_widgets = array_filter( $data, - static function( $widget ) { + static function ( $widget ) { return 'text' === $widget['id']; } ); @@ -180,11 +187,92 @@ public function test_get_item() { wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_name ); $response = rest_get_server()->dispatch( $request ); - $endpoint = new WP_REST_Widget_Types_Controller; + $endpoint = new WP_REST_Widget_Types_Controller(); $widget_type = $endpoint->get_widget( $widget_name ); $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $widget_name = 'calendar'; + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/' . $widget_name ); + + $hook_name = 'rest_prepare_widget_type'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/widget-types/calendar' ), + 'get_items request' => array( '/wp/v2/widget-types' ), + ); + } + /** * @ticket 41683 */ @@ -193,23 +281,27 @@ public function test_get_widget_legacy() { wp_register_sidebar_widget( $widget_id, 'WP legacy widget', - static function() {} + static function () {} ); wp_set_current_user( self::$admin_id ); $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_id ); $response = rest_get_server()->dispatch( $request ); - $endpoint = new WP_REST_Widget_Types_Controller; + $endpoint = new WP_REST_Widget_Types_Controller(); $widget_type = $endpoint->get_widget( $widget_id ); $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_widget_invalid_name() { + public function test_get_widget_invalid_name( $method ) { $widget_type = 'fake'; wp_set_current_user( self::$admin_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/' . $widget_type ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/' . $widget_type ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_widget_type_invalid', $response, 404 ); @@ -224,7 +316,7 @@ public function test_get_widgets_decodes_html_entities() { wp_register_sidebar_widget( $widget_id, '‘Legacy ‑ Archive ‑ Widget’', - static function() {}, + static function () {}, array( 'description' => '“A great & interesting archive of your site’s posts!”', ) @@ -255,41 +347,57 @@ public function test_get_item_schema() { } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_wrong_permission() { + public function test_get_items_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_wrong_permission() { + public function test_get_item_wrong_permission( $method ) { wp_set_current_user( self::$subscriber_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/calendar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_items_no_permission() { + public function test_get_items_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widget-types/calendar' ); + $request = new WP_REST_Request( $method, '/wp/v2/widget-types/calendar' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } @@ -298,9 +406,9 @@ public function test_get_item_no_permission() { * @ticket 41683 */ public function test_prepare_item() { - $endpoint = new WP_REST_Widget_Types_Controller; + $endpoint = new WP_REST_Widget_Types_Controller(); $widget_type = $endpoint->get_widget( 'calendar' ); - $request = new WP_REST_Request; + $request = new WP_REST_Request(); $request->set_param( 'context', 'edit' ); $response = $endpoint->prepare_item_for_response( $widget_type, $request ); $this->check_widget_type_object( $widget_type, $response->get_data(), $response->get_links() ); @@ -366,7 +474,7 @@ public function test_encode_form_data_with_no_input() { array( 'encoded' => base64_encode( serialize( array() ) ), 'hash' => wp_hash( serialize( array() ) ), - 'raw' => new stdClass, + 'raw' => new stdClass(), ), $data['instance'] ); @@ -402,7 +510,7 @@ public function test_encode_form_data_with_number() { array( 'encoded' => base64_encode( serialize( array() ) ), 'hash' => wp_hash( serialize( array() ) ), - 'raw' => new stdClass, + 'raw' => new stdClass(), ), $data['instance'] ); @@ -531,17 +639,29 @@ public function test_encode_form_data_no_raw() { } /** - * The test_create_item() method does not exist for widget types. + * The create_item() method does not exist for widget types. + * + * @doesNotPerformAssertions */ - public function test_create_item() {} + public function test_create_item() { + // Controller does not implement create_item(). + } /** - * The test_update_item() method does not exist for widget types. + * The update_item() method does not exist for widget types. + * + * @doesNotPerformAssertions */ - public function test_update_item() {} + public function test_update_item() { + // Controller does not implement update_item(). + } /** - * The test_delete_item() method does not exist for widget types. + * The delete_item() method does not exist for widget types. + * + * @doesNotPerformAssertions */ - public function test_delete_item() {} + public function test_delete_item() { + // Controller does not implement delete_item(). + } } diff --git a/tests/phpunit/tests/rest-api/rest-widgets-controller.php b/tests/phpunit/tests/rest-api/rest-widgets-controller.php index 03d4ea0931542..27a58eb638f9c 100644 --- a/tests/phpunit/tests/rest-api/rest-widgets-controller.php +++ b/tests/phpunit/tests/rest-api/rest-widgets-controller.php @@ -1,20 +1,16 @@ dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } + /** + * Data provider intended to provide HTTP method names for testing GET and HEAD requests. + * + * @return array + */ + public static function data_readable_http_methods() { + return array( + 'GET request' => array( 'GET' ), + 'HEAD request' => array( 'HEAD' ), + ); + } + /** * @ticket 53915 */ @@ -335,11 +348,15 @@ public function test_get_items_without_show_in_rest_are_removed_from_the_list() } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_items_wrong_permission_author() { + public function test_get_items_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } @@ -380,21 +397,22 @@ public function test_get_items() { $request = new WP_REST_Request( 'GET', '/wp/v2/widgets' ); $response = rest_get_server()->dispatch( $request ); - $data = $response->get_data(); - $data = $this->remove_links( $data ); + remove_filter( 'pre_http_request', array( $this, 'mocked_rss_response' ) ); + $data = $response->get_data(); + $data = $this->remove_links( $data ); $this->assertSameSets( array( array( 'id' => 'block-1', 'id_base' => 'block', 'sidebar' => 'sidebar-1', - 'rendered' => '

      Block test

      ', + 'rendered' => '

      Block test

      ', ), array( 'id' => 'rss-1', 'id_base' => 'rss', 'sidebar' => 'sidebar-1', - 'rendered' => 'RSS RSS test', + 'rendered' => 'RSS RSS test', ), array( 'id' => 'testwidget', @@ -409,14 +427,62 @@ public function test_get_items() { $wp_widget_factory->widgets['WP_Widget_RSS']->widget_options['show_instance_in_rest'] = true; } + /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_items_with_head_request_should_not_prepare_widget_data( $method ) { + $block_content = '

      Block test

      '; + + $this->setup_widget( + 'rss', + 1, + array( + 'title' => 'RSS test', + 'url' => 'https://wordpress.org/news/feed', + ) + ); + $this->setup_widget( + 'block', + 1, + array( + 'content' => $block_content, + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'block-1', 'rss-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/widgets' ); + + $hook_name = 'rest_prepare_post'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + + add_filter( $hook_name, $callback ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + + $this->assertNotWPError( $response ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 0, $filter->get_call_count(), 'The "' . $hook_name . '" filter was called when it should not be for HEAD requests.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + public function mocked_rss_response() { $single_value_headers = array( - 'content-type' => 'application/rss+xml; charset=UTF-8', + 'Content-Type' => 'application/rss+xml; charset=UTF-8', 'link' => '; rel="https://api.w.org/"', ); return array( - 'headers' => new Requests_Utility_CaseInsensitiveDictionary( $single_value_headers ), + 'headers' => new WpOrg\Requests\Utility\CaseInsensitiveDictionary( $single_value_headers ), 'body' => file_get_contents( DIR_TESTDATA . '/feed/wordpress-org-news.xml' ), 'response' => array( 'code' => 200, @@ -531,9 +597,125 @@ public function test_get_item() { } /** + * @dataProvider data_readable_http_methods + * @ticket 56481 + * + * @param string $method The HTTP method to use. + */ + public function test_get_item_should_allow_adding_headers_via_filter( $method ) { + $this->setup_widget( + 'text', + 1, + array( + 'text' => 'Custom text test', + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'text-1' ) + ); + + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); + + $hook_name = 'rest_prepare_widget'; + $filter = new MockAction(); + $callback = array( $filter, 'filter' ); + add_filter( $hook_name, $callback ); + $header_filter = new class() { + public static function add_custom_header( $response ) { + $response->header( 'X-Test-Header', 'Test' ); + + return $response; + } + }; + add_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + $response = rest_get_server()->dispatch( $request ); + remove_filter( $hook_name, $callback ); + remove_filter( $hook_name, array( $header_filter, 'add_custom_header' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( 1, $filter->get_call_count(), 'The "' . $hook_name . '" filter was not called when it should be for GET/HEAD requests.' ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-Test-Header', $headers, 'The "X-Test-Header" header should be present in the response.' ); + $this->assertSame( 'Test', $headers['X-Test-Header'], 'The "X-Test-Header" header value should be equal to "Test".' ); + if ( 'HEAD' !== $method ) { + return null; + } + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + add_filter( 'pre_http_request', array( $this, 'mocked_rss_response' ) ); + global $wp_widget_factory; + + $wp_widget_factory->widgets['WP_Widget_RSS']->widget_options['show_instance_in_rest'] = false; + + $block_content = '

      Block test

      '; + + $this->setup_widget( + 'rss', + 1, + array( + 'title' => 'RSS test', + 'url' => 'https://wordpress.org/news/feed', + ) + ); + $this->setup_widget( + 'block', + 1, + array( + 'content' => $block_content, + ) + ); + $this->setup_sidebar( + 'sidebar-1', + array( + 'name' => 'Test sidebar', + ), + array( 'block-1', 'rss-1', 'testwidget' ) + ); + + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + remove_filter( 'pre_http_request', array( $this, 'mocked_rss_response' ) ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/widgets/block-1' ), + 'get_items request' => array( '/wp/v2/widgets' ), + ); + } + + /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_no_permission() { + public function test_get_item_no_permission( $method ) { wp_set_current_user( 0 ); $this->setup_widget( @@ -551,15 +733,19 @@ public function test_get_item_no_permission() { array( 'text-1' ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 401 ); } /** + * @dataProvider data_readable_http_methods * @ticket 41683 + * @ticket 56481 + * + * @param string $method The HTTP method to use. */ - public function test_get_item_wrong_permission_author() { + public function test_get_item_wrong_permission_author( $method ) { wp_set_current_user( self::$author_id ); $this->setup_widget( 'text', @@ -575,7 +761,7 @@ public function test_get_item_wrong_permission_author() { ) ); - $request = new WP_REST_Request( 'GET', '/wp/v2/widgets/text-1' ); + $request = new WP_REST_Request( $method, '/wp/v2/widgets/text-1' ); $response = rest_get_server()->dispatch( $request ); $this->assertErrorResponse( 'rest_cannot_manage_widgets', $response, 403 ); } @@ -1508,9 +1694,12 @@ public function test_delete_item_multiple() { } /** - * The test_prepare_item() method does not exist for sidebar. + * The prepare_item() method does not exist for sidebar. + * + * @doesNotPerformAssertions */ public function test_prepare_item() { + // Controller does not implement prepare_item(). } /** @@ -1523,6 +1712,8 @@ public function test_get_item_schema() { $data = $response->get_data(); $properties = $data['schema']['properties']; + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + $this->assertCount( 7, $properties ); $this->assertArrayHasKey( 'id', $properties ); $this->assertArrayHasKey( 'id_base', $properties ); @@ -1549,7 +1740,7 @@ protected function remove_links( $data ) { if ( is_array( $item ) && isset( $item['_links'] ) ) { unset( $data[ $count ]['_links'] ); } - $count ++; + ++$count; } return $data; diff --git a/tests/phpunit/tests/rest-api/wpIsRestEndpoint.php b/tests/phpunit/tests/rest-api/wpIsRestEndpoint.php new file mode 100644 index 0000000000000..2cc9c0cad4d64 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpIsRestEndpoint.php @@ -0,0 +1,66 @@ +assertFalse( wp_is_rest_endpoint() ); + } + + /** + * Tests that `wp_is_rest_endpoint()` relies on whether the global REST server is dispatching. + * + * @ticket 42061 + */ + public function test_wp_is_rest_endpoint_via_global() { + global $wp_rest_server; + + $wp_rest_server = new Spy_REST_Server(); + do_action( 'rest_api_init', $wp_rest_server ); + + // The presence of a REST server itself won't set this to true. + $this->assertFalse( wp_is_rest_endpoint() ); + + // Set up filter to record value during dispatching. + $result_within_request = null; + add_filter( + 'rest_pre_dispatch', + function ( $result ) use ( &$result_within_request ) { + $result_within_request = wp_is_rest_endpoint(); + return $result; + } + ); + + /* + * Dispatch a request (doesn't matter that it's invalid). + * This already is completed after this method call. + */ + $wp_rest_server->dispatch( new WP_REST_Request() ); + + // Within that request, the function should have returned true. + $this->assertTrue( $result_within_request ); + + // After the dispatching, the function should return false again. + $this->assertFalse( wp_is_rest_endpoint() ); + } + + /** + * Tests that `wp_is_rest_endpoint()` returns a result enforced via filter. + * + * @ticket 42061 + */ + public function test_wp_is_rest_endpoint_via_filter() { + add_filter( 'wp_is_rest_endpoint', '__return_true' ); + $this->assertTrue( wp_is_rest_endpoint() ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php new file mode 100644 index 0000000000000..43525263ac5ba --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1CategoriesController.php @@ -0,0 +1,529 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + + self::$subscriber_user_id = self::factory()->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + /** + * Set up before each test. + */ + public function set_up(): void { + parent::set_up(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + $this->server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + $this->register_test_ability_categories(); + + wp_set_current_user( self::$admin_user_id ); + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + + // Clean up test ability categories. + foreach ( wp_get_ability_categories() as $ability_category ) { + if ( ! str_starts_with( $ability_category->get_slug(), 'test-' ) ) { + continue; + } + + wp_unregister_ability_category( $ability_category->get_slug() ); + } + + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Register test ability categories for testing. + */ + public function register_test_ability_categories(): void { + // Simulates the init hook to allow test ability categories registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + + wp_register_ability_category( + 'test-data-retrieval', + array( + 'label' => 'Data Retrieval', + 'description' => 'Abilities that retrieve and return data from the WordPress site.', + ) + ); + + wp_register_ability_category( + 'test-data-modification', + array( + 'label' => 'Data Modification', + 'description' => 'Abilities that modify data on the WordPress site.', + ) + ); + + wp_register_ability_category( + 'test-communication', + array( + 'label' => 'Communication', + 'description' => 'Abilities that send messages or notifications.', + 'meta' => array( + 'priority' => 'high', + ), + ) + ); + + // Register multiple ability categories for pagination testing + for ( $i = 1; $i <= 60; $i++ ) { + wp_register_ability_category( + "test-category-{$i}", + array( + 'label' => "Test Category {$i}", + 'description' => "Test category number {$i}", + ) + ); + } + + array_pop( $wp_current_filter ); + } + + /** + * Test listing all ability categories. + * + * @ticket 64098 + */ + public function test_get_items(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + + $this->assertCount( 50, $data, 'First page should return exactly 50 items (default per_page)' ); + + $category_slugs = wp_list_pluck( $data, 'slug' ); + $this->assertContains( 'test-data-retrieval', $category_slugs ); + $this->assertContains( 'test-data-modification', $category_slugs ); + $this->assertContains( 'test-communication', $category_slugs ); + } + + /** + * Test getting a specific ability category. + * + * @ticket 64098 + */ + public function test_get_item(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'test-data-retrieval', $data['slug'] ); + $this->assertSame( 'Data Retrieval', $data['label'] ); + $this->assertSame( 'Abilities that retrieve and return data from the WordPress site.', $data['description'] ); + $this->assertArrayHasKey( 'meta', $data ); + } + + /** + * Test getting an ability category with meta. + * + * @ticket 64098 + */ + public function test_get_item_with_meta(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-communication' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'test-communication', $data['slug'] ); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertIsArray( $data['meta'] ); + $this->assertSame( 'high', $data['meta']['priority'] ); + } + + /** + * Test getting a specific ability category with only selected fields. + * + * @ticket 64098 + */ + public function test_get_item_with_selected_fields(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $request->set_param( '_fields', 'slug,label' ); + $response = $this->server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response should only contain the requested fields.' ); + $this->assertSame( 'test-data-retrieval', $data['slug'] ); + $this->assertSame( 'Data Retrieval', $data['label'] ); + } + + /** + * Test getting a non-existent ability category returns 404. + * + * @ticket 64098 + * + * @expectedIncorrectUsage WP_Ability_Categories_Registry::get_registered + */ + public function test_get_item_not_found(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/non-existent' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'rest_ability_category_not_found', $data['code'] ); + } + + /** + * Test permission check for listing ability categories. + * + * @ticket 64098 + */ + public function test_get_items_permission_denied(): void { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test permission check for single ability category. + * + * @ticket 64098 + */ + public function test_get_item_permission_denied(): void { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test pagination headers. + * + * @ticket 64098 + */ + public function test_pagination_headers(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $request->set_param( 'per_page', 10 ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + + $total_categories = count( wp_get_ability_categories() ); + $this->assertEquals( $total_categories, (int) $headers['X-WP-Total'] ); + $this->assertEquals( ceil( $total_categories / 10 ), (int) $headers['X-WP-TotalPages'] ); + } + + /** + * Test HEAD method returns empty body with proper headers. + * + * @ticket 64098 + */ + public function test_head_request(): void { + $request = new WP_REST_Request( 'HEAD', '/wp-abilities/v1/categories' ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertEmpty( $data ); + + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + } + + /** + * Test pagination links. + * + * @ticket 64098 + */ + public function test_pagination_links(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $request->set_param( 'per_page', 10 ); + $request->set_param( 'page', 1 ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + $this->assertStringContainsString( 'rel="next"', $link_header ); + $this->assertStringNotContainsString( 'rel="prev"', $link_header ); + + $request->set_param( 'page', 3 ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + $this->assertStringContainsString( 'rel="next"', $link_header ); + $this->assertStringContainsString( 'rel="prev"', $link_header ); + + $total_categories = count( wp_get_ability_categories() ); + $last_page = ceil( $total_categories / 10 ); + $request->set_param( 'page', $last_page ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + $this->assertStringNotContainsString( 'rel="next"', $link_header ); + $this->assertStringContainsString( 'rel="prev"', $link_header ); + } + + /** + * Test collection parameters. + * + * @ticket 64098 + */ + public function test_collection_params(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $request->set_param( 'per_page', 5 ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertCount( 5, $data ); + $request->set_param( 'page', 2 ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 5, $data ); + + $page1_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $page1_request->set_param( 'per_page', 5 ); + $page1_request->set_param( 'page', 1 ); + $page1_response = $this->server->dispatch( $page1_request ); + $page1_slugs = wp_list_pluck( $page1_response->get_data(), 'slug' ); + $page2_slugs = wp_list_pluck( $data, 'slug' ); + + $this->assertNotEquals( $page1_slugs, $page2_slugs ); + } + + /** + * Test response links for individual ability categories. + * + * @ticket 64098 + */ + public function test_ability_category_response_links(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $response = $this->server->dispatch( $request ); + + $links = $response->get_links(); + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'collection', $links ); + $this->assertArrayHasKey( 'abilities', $links ); + + $self_link = $links['self'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/categories/test-data-retrieval', $self_link ); + + $collection_link = $links['collection'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/categories', $collection_link ); + + $abilities_link = $links['abilities'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/abilities?category=test-data-retrieval', $abilities_link ); + } + + /** + * Test context parameter. + * + * @ticket 64098 + */ + public function test_context_parameter(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $request->set_param( 'context', 'view' ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'description', $data ); + + $request->set_param( 'context', 'embed' ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'slug', $data ); + $this->assertArrayHasKey( 'label', $data ); + } + + /** + * Test schema retrieval. + * + * @ticket 64098 + */ + public function test_get_schema(): void { + $request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/categories' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'schema', $data ); + $schema = $data['schema']; + + $this->assertSame( 'ability-category', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); + $this->assertArrayHasKey( 'properties', $schema ); + + $properties = $schema['properties']; + + $this->assertCount( 4, $properties, 'Schema should have exactly 4 properties.' ); + + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); + + $slug_property = $properties['slug']; + $this->assertSame( 'string', $slug_property['type'] ); + $this->assertTrue( $slug_property['readonly'] ); + } + + /** + * Test ability category slug with valid format. + * + * @ticket 64098 + */ + public function test_ability_category_slug_with_valid_format(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/test-data-retrieval' ); + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Data provider for invalid ability category slugs. + * + * @return array + */ + public function data_invalid_ability_category_slugs_provider(): array { + return array( + 'Uppercase' => array( 'Data-Retrieval' ), + '@ symbol' => array( 'data@retrieval' ), + 'space' => array( 'data retrieval' ), + 'dot' => array( 'data.retrieval' ), + 'underscore' => array( 'data_retrieval' ), + 'URL encoded space' => array( 'data%20retrieval' ), + ); + } + + /** + * Test ability category slugs with invalid format. + * + * @ticket 64098 + * + * @dataProvider data_invalid_ability_category_slugs_provider + * + * @param string $slug Invalid ability category slug to test. + */ + public function test_ability_category_slug_with_invalid_format( string $slug ): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories/' . $slug ); + $response = $this->server->dispatch( $request ); + + $this->assertContains( $response->get_status(), array( 400, 404 ) ); + } + + /** + * Data provider for invalid pagination parameters. + * + * @return array}> + */ + public function data_invalid_pagination_params_provider(): array { + return array( + 'Zero page' => array( array( 'page' => 0 ) ), + 'Negative page' => array( array( 'page' => -1 ) ), + 'Non-numeric page' => array( array( 'page' => 'abc' ) ), + 'Zero per page' => array( array( 'per_page' => 0 ) ), + 'Negative per page' => array( array( 'per_page' => -10 ) ), + 'Exceeds maximum' => array( array( 'per_page' => 1000 ) ), + 'Non-numeric per page' => array( array( 'per_page' => 'all' ) ), + ); + } + + /** + * Test pagination parameters with invalid values. + * + * @ticket 64098 + * + * @dataProvider data_invalid_pagination_params_provider + * + * @param array $params Invalid pagination parameters. + */ + public function test_invalid_pagination_parameters( array $params ): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/categories' ); + $request->set_query_params( $params ); + + $response = $this->server->dispatch( $request ); + + $this->assertContains( $response->get_status(), array( 200, 400 ) ); + + if ( $response->get_status() !== 200 ) { + return; + } + + $data = $response->get_data(); + $this->assertIsArray( $data ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php new file mode 100644 index 0000000000000..d73a2c64177fc --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1ListController.php @@ -0,0 +1,1019 @@ +user->create( + array( + 'role' => 'subscriber', + ) + ); + + self::register_test_categories(); + } + + /** + * Tear down after class. + */ + public static function tear_down_after_class(): void { + // Clean up registered test ability categories. + foreach ( array( 'math', 'system', 'general' ) as $slug ) { + wp_unregister_ability_category( $slug ); + } + + parent::tear_down_after_class(); + } + + /** + * Set up before each test. + */ + public function set_up(): void { + parent::set_up(); + + // Set up REST server + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + $this->server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + $this->register_test_abilities(); + + // Set default user for tests + wp_set_current_user( self::$user_id ); + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + // Clean up test abilities. + foreach ( wp_get_abilities() as $ability ) { + if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) { + continue; + } + + wp_unregister_ability( $ability->get_name() ); + } + + // Reset REST server + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Register test categories for testing. + */ + public static function register_test_categories(): void { + // Simulates the init hook to allow test ability categories registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + + wp_register_ability_category( + 'math', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations and calculations.', + ) + ); + + wp_register_ability_category( + 'system', + array( + 'label' => 'System', + 'description' => 'System information and operations.', + ) + ); + + wp_register_ability_category( + 'general', + array( + 'label' => 'General', + 'description' => 'General purpose abilities.', + ) + ); + + array_pop( $wp_current_filter ); + } + + /** + * Helper to register a test ability. + * + * @param string $name Ability name. + * @param array $args Ability arguments. + */ + private function register_test_ability( string $name, array $args ): void { + // Simulates the init hook to allow test abilities registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_init'; + + wp_register_ability( $name, $args ); + + array_pop( $wp_current_filter ); + } + + /** + * Register test abilities for testing. + */ + private function register_test_abilities(): void { + // Register a regular ability. + $this->register_test_ability( + 'test/calculator', + array( + 'label' => 'Calculator', + 'description' => 'Performs basic calculations', + 'category' => 'math', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'operation' => array( + 'type' => 'string', + 'enum' => array( 'add', 'subtract', 'multiply', 'divide' ), + ), + 'a' => array( 'type' => 'number' ), + 'b' => array( 'type' => 'number' ), + ), + ), + 'output_schema' => array( + 'type' => 'number', + ), + 'execute_callback' => static function ( array $input ) { + switch ( $input['operation'] ) { + case 'add': + return $input['a'] + $input['b']; + case 'subtract': + return $input['a'] - $input['b']; + case 'multiply': + return $input['a'] * $input['b']; + case 'divide': + return 0 !== $input['b'] ? $input['a'] / $input['b'] : null; + default: + return null; + } + }, + 'permission_callback' => static function () { + return current_user_can( 'read' ); + }, + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Register a read-only ability. + $this->register_test_ability( + 'test/system-info', + array( + 'label' => 'System Info', + 'description' => 'Returns system information', + 'category' => 'system', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'detail_level' => array( + 'type' => 'string', + 'enum' => array( 'basic', 'full' ), + 'default' => 'basic', + ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'php_version' => array( 'type' => 'string' ), + 'wp_version' => array( 'type' => 'string' ), + ), + ), + 'execute_callback' => static function ( array $input ) { + $info = array( + 'php_version' => phpversion(), + 'wp_version' => get_bloginfo( 'version' ), + ); + if ( 'full' === ( $input['detail_level'] ?? 'basic' ) ) { + $info['memory_limit'] = ini_get( 'memory_limit' ); + } + return $info; + }, + 'permission_callback' => static function () { + return current_user_can( 'read' ); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'category' => 'system', + 'show_in_rest' => true, + ), + ) + ); + + // Ability that does not show in REST. + $this->register_test_ability( + 'test/not-show-in-rest', + array( + 'label' => 'Hidden from REST', + 'description' => 'It does not show in REST.', + 'category' => 'general', + 'execute_callback' => static function (): int { + return 0; + }, + 'permission_callback' => '__return_true', + ) + ); + + // Register multiple abilities for pagination testing + for ( $i = 1; $i <= 60; $i++ ) { + $this->register_test_ability( + "test/ability-{$i}", + array( + 'label' => "Test Ability {$i}", + 'description' => "Test ability number {$i}", + 'category' => 'general', + 'execute_callback' => static function () use ( $i ) { + return "Result from ability {$i}"; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + } + } + + /** + * Test listing all abilities. + * + * @ticket 64098 + */ + public function test_get_items(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertNotEmpty( $data ); + + $this->assertCount( 50, $data, 'First page should return exactly 50 items (default per_page)' ); + + $ability_names = wp_list_pluck( $data, 'name' ); + $this->assertContains( 'test/calculator', $ability_names ); + $this->assertContains( 'test/system-info', $ability_names ); + $this->assertNotContains( 'test/not-show-in-rest', $ability_names ); + } + + /** + * Test getting a specific ability. + * + * @ticket 64098 + */ + public function test_get_item(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/calculator' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 7, $data, 'Response should contain all fields.' ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); + $this->assertSame( 'Performs basic calculations', $data['description'] ); + $this->assertSame( 'math', $data['category'] ); + $this->assertArrayHasKey( 'input_schema', $data ); + $this->assertArrayHasKey( 'output_schema', $data ); + $this->assertArrayHasKey( 'meta', $data ); + $this->assertTrue( $data['meta']['show_in_rest'] ); + } + + /** + * Test getting a specific ability with only selected fields. + * + * @ticket 64098 + */ + public function test_get_item_with_selected_fields(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/calculator' ); + $request->set_param( '_fields', 'name,label' ); + $response = $this->server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 2, $data, 'Response should only contain the requested fields.' ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); + } + + /** + * Test getting a specific ability with embed context. + * + * @ticket 64098 + */ + public function test_get_item_with_embed_context(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/calculator' ); + $request->set_param( 'context', 'embed' ); + $response = $this->server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $this->server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertCount( 3, $data, 'Response should only contain the fields for embed context.' ); + $this->assertSame( 'test/calculator', $data['name'] ); + $this->assertSame( 'Calculator', $data['label'] ); + $this->assertSame( 'math', $data['category'] ); + } + + /** + * Test getting a non-existent ability returns 404. + * + * @ticket 64098 + * + * @expectedIncorrectUsage WP_Abilities_Registry::get_registered + */ + public function test_get_item_not_found(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/non/existent' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); + } + + /** + * Test getting an ability that does not show in REST returns 404. + * + * @ticket 64098 + */ + public function test_get_item_not_show_in_rest(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/not-show-in-rest' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); + } + + /** + * Test permission check for listing abilities. + * + * @ticket 64098 + */ + public function test_get_items_permission_denied(): void { + // Test with non-logged-in user + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 401, $response->get_status() ); + } + + /** + * Test pagination headers. + * + * @ticket 64098 + */ + public function test_pagination_headers(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_param( 'per_page', 10 ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + + $total_abilities = count( wp_get_abilities() ) - 1; // Exclude the one that doesn't show in REST. + $this->assertEquals( $total_abilities, (int) $headers['X-WP-Total'] ); + $this->assertEquals( ceil( $total_abilities / 10 ), (int) $headers['X-WP-TotalPages'] ); + } + + /** + * Test HEAD method returns empty body with proper headers. + * + * @ticket 64098 + */ + public function test_head_request(): void { + $request = new WP_REST_Request( 'HEAD', '/wp-abilities/v1/abilities' ); + $response = $this->server->dispatch( $request ); + + // Verify empty response body + $data = $response->get_data(); + $this->assertEmpty( $data ); + + // Verify pagination headers are present + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + } + + /** + * Test pagination links. + * + * @ticket 64098 + */ + public function test_pagination_links(): void { + // Test first page (should have 'next' link header but no 'prev') + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_param( 'per_page', 10 ); + $request->set_param( 'page', 1 ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + // Parse Link header for rel="next" and rel="prev" + $this->assertStringContainsString( 'rel="next"', $link_header ); + $this->assertStringNotContainsString( 'rel="prev"', $link_header ); + + // Test middle page (should have both 'next' and 'prev' link headers) + $request->set_param( 'page', 3 ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + $this->assertStringContainsString( 'rel="next"', $link_header ); + $this->assertStringContainsString( 'rel="prev"', $link_header ); + + // Test last page (should have 'prev' link header but no 'next') + $total_abilities = count( wp_get_abilities() ); + $last_page = ceil( $total_abilities / 10 ); + $request->set_param( 'page', $last_page ); + $response = $this->server->dispatch( $request ); + + $headers = $response->get_headers(); + $link_header = $headers['Link'] ?? ''; + + $this->assertStringNotContainsString( 'rel="next"', $link_header ); + $this->assertStringContainsString( 'rel="prev"', $link_header ); + } + + /** + * Test collection parameters. + * + * @ticket 64098 + */ + public function test_collection_params(): void { + // Test per_page parameter + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_param( 'per_page', 5 ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertCount( 5, $data ); + + // Test page parameter + $request->set_param( 'page', 2 ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 5, $data ); + + // Verify we got different abilities on page 2 + $page1_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $page1_request->set_param( 'per_page', 5 ); + $page1_request->set_param( 'page', 1 ); + $page1_response = $this->server->dispatch( $page1_request ); + $page1_names = wp_list_pluck( $page1_response->get_data(), 'name' ); + $page2_names = wp_list_pluck( $data, 'name' ); + + $this->assertNotEquals( $page1_names, $page2_names ); + } + + /** + * Test response links for individual abilities. + * + * @ticket 64098 + */ + public function test_ability_response_links(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/calculator' ); + $response = $this->server->dispatch( $request ); + + $links = $response->get_links(); + $this->assertArrayHasKey( 'self', $links ); + $this->assertArrayHasKey( 'collection', $links ); + $this->assertArrayHasKey( 'wp:action-run', $links ); + + // Verify link URLs + $self_link = $links['self'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/abilities/test/calculator', $self_link ); + + $collection_link = $links['collection'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/abilities', $collection_link ); + + $run_link = $links['wp:action-run'][0]['href']; + $this->assertStringContainsString( '/wp-abilities/v1/abilities/test/calculator/run', $run_link ); + } + + /** + * Test context parameter. + * + * @ticket 64098 + */ + public function test_context_parameter(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/calculator' ); + $request->set_param( 'context', 'view' ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'description', $data ); + + $request->set_param( 'context', 'embed' ); + $response = $this->server->dispatch( $request ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'name', $data ); + $this->assertArrayHasKey( 'label', $data ); + } + + /** + * Test schema retrieval. + * + * @ticket 64098 + */ + public function test_get_schema(): void { + $request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/abilities' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'schema', $data ); + $schema = $data['schema']; + + $this->assertSame( 'ability', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); + $this->assertArrayHasKey( 'properties', $schema ); + + $properties = $schema['properties']; + + // Assert the count of properties to catch when new keys are added + $this->assertCount( 7, $properties, 'Schema should have exactly 7 properties. If this fails, update this test to include the new property.' ); + + // Check all expected properties exist + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'label', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'input_schema', $properties ); + $this->assertArrayHasKey( 'output_schema', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); + $this->assertArrayHasKey( 'category', $properties ); + } + + /** + * Test ability name with valid special characters. + * + * @ticket 64098 + */ + public function test_ability_name_with_valid_special_characters(): void { + // Register ability with hyphen (valid). + $this->register_test_ability( + 'test-hyphen/ability', + array( + 'label' => 'Test Hyphen Ability', + 'description' => 'Test ability with hyphen', + 'category' => 'general', + 'execute_callback' => static function ( $input ) { + return array( 'success' => true ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Test valid special characters (hyphen, forward slash) + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test-hyphen/ability' ); + $response = $this->server->dispatch( $request ); + + wp_unregister_ability( 'test-hyphen/ability' ); + + $this->assertEquals( 200, $response->get_status() ); + } + + /** + * Data provider for invalid ability names. + * + * @return array + */ + public function data_invalid_ability_names_provider(): array { + return array( + '@ symbol' => array( 'test@ability' ), + 'space' => array( 'test ability' ), + 'dot' => array( 'test.ability' ), + 'hash' => array( 'test#ability' ), + 'URL encoded space' => array( 'test%20ability' ), + 'angle brackets' => array( 'test' ), + 'pipe' => array( 'test|ability' ), + 'backslash' => array( 'test\\ability' ), + ); + } + + /** + * Test ability names with invalid special characters. + * + * @ticket 64098 + * + * @dataProvider data_invalid_ability_names_provider + * + * @param string $name Invalid ability name to test. + */ + public function test_ability_name_with_invalid_special_characters( string $name ): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/' . $name ); + $response = $this->server->dispatch( $request ); + // Should return 404 as the regex pattern won't match + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Test extremely long ability names. + * + * @ticket 64098 + * + * @expectedIncorrectUsage WP_Abilities_Registry::get_registered + */ + public function test_extremely_long_ability_names(): void { + // Create a very long but valid ability name + $long_name = 'test/' . str_repeat( 'a', 1000 ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/' . $long_name ); + $response = $this->server->dispatch( $request ); + + // Should return 404 as ability doesn't exist + $this->assertEquals( 404, $response->get_status() ); + } + + /** + * Data provider for invalid pagination parameters. + * + * @return array}> + */ + public function data_invalid_pagination_params_provider(): array { + return array( + 'Zero page' => array( array( 'page' => 0 ) ), + 'Negative page' => array( array( 'page' => -1 ) ), + 'Non-numeric page' => array( array( 'page' => 'abc' ) ), + 'Zero per page' => array( array( 'per_page' => 0 ) ), + 'Negative per page' => array( array( 'per_page' => -10 ) ), + 'Exceeds maximum' => array( array( 'per_page' => 1000 ) ), + 'Non-numeric per page' => array( array( 'per_page' => 'all' ) ), + ); + } + + /** + * Test pagination parameters with invalid values. + * + * @ticket 64098 + * + * @dataProvider data_invalid_pagination_params_provider + * + * @param array $params Invalid pagination parameters. + */ + public function test_invalid_pagination_parameters( array $params ): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_query_params( $params ); + + $response = $this->server->dispatch( $request ); + + // Should either use defaults or return error + $this->assertContains( $response->get_status(), array( 200, 400 ) ); + + if ( $response->get_status() !== 200 ) { + return; + } + + // Check that reasonable defaults were used + $data = $response->get_data(); + $this->assertIsArray( $data ); + } + + /** + * Test filtering abilities by category. + * + * @ticket 64098 + */ + public function test_filter_by_category(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_param( 'category', 'math' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertIsArray( $data ); + + // Should only have math category abilities + foreach ( $data as $ability ) { + $this->assertSame( 'math', $ability['category'], 'All abilities should be in math category' ); + } + + // Should at least contain the calculator + $ability_names = wp_list_pluck( $data, 'name' ); + $this->assertContains( 'test/calculator', $ability_names ); + $this->assertNotContains( 'test/system-info', $ability_names, 'System info should not be in math category' ); + } + + /** + * Test filtering by non-existent category returns empty results. + * + * @ticket 64098 + */ + public function test_filter_by_nonexistent_category(): void { + // Ensure category doesn't exist - test should fail if it does. + $this->assertFalse( + wp_has_ability_category( 'nonexistent' ), + 'The nonexistent category should not be registered - test isolation may be broken' + ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities' ); + $request->set_param( 'category', 'nonexistent' ); + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertIsArray( $data ); + $this->assertEmpty( $data, 'Should return empty array for non-existent category' ); + } + + /** + * Test that WordPress-internal schema keywords are stripped from ability schemas in REST response. + * + * @ticket 65035 + */ + public function test_internal_schema_keywords_stripped_from_response(): void { + $this->register_test_ability( + 'test/with-internal-keywords', + array( + 'label' => 'Test Internal Keywords', + 'description' => 'Tests stripping of internal schema keywords', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'content' => array( + 'type' => 'string', + 'description' => 'The content value.', + 'sanitize_callback' => 'sanitize_text_field', + 'validate_callback' => 'is_string', + 'arg_options' => array( 'sanitize_callback' => 'wp_kses_post' ), + ), + ), + ), + 'output_schema' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + 'execute_callback' => static function ( $input ) { + return $input['content']; + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'show_in_rest' => true ), + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/with-internal-keywords' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertArrayHasKey( 'input_schema', $data ); + $this->assertArrayHasKey( 'properties', $data['input_schema'] ); + $this->assertArrayHasKey( 'content', $data['input_schema']['properties'] ); + $this->assertArrayHasKey( 'output_schema', $data ); + + // Verify internal keywords are stripped from input_schema properties. + $content_schema = $data['input_schema']['properties']['content']; + $this->assertArrayNotHasKey( 'sanitize_callback', $content_schema ); + $this->assertArrayNotHasKey( 'validate_callback', $content_schema ); + $this->assertArrayNotHasKey( 'arg_options', $content_schema ); + + // Verify valid JSON Schema keywords are preserved. + $this->assertSame( 'string', $content_schema['type'] ); + $this->assertSame( 'The content value.', $content_schema['description'] ); + + // Verify internal keywords are stripped from output_schema. + $this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema'] ); + $this->assertSame( 'string', $data['output_schema']['type'] ); + } + + /** + * Test that internal schema keywords are stripped from nested sub-schema locations. + * + * @ticket 64098 + */ + public function test_internal_schema_keywords_stripped_from_nested_sub_schemas(): void { + $this->register_test_ability( + 'test/nested-internal-keywords', + array( + 'label' => 'Test Nested Keywords', + 'description' => 'Tests stripping from all sub-schema locations', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + 'anyOf' => array( + array( + 'type' => 'object', + 'sanitize_callback' => 'sanitize_text_field', + 'properties' => array( + 'value' => array( + 'type' => 'string', + 'validate_callback' => 'is_string', + ), + ), + ), + array( + 'type' => 'number', + 'arg_options' => array( 'sanitize_callback' => 'absint' ), + ), + ), + 'oneOf' => array( + array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'allOf' => array( + array( + 'type' => 'object', + 'validate_callback' => 'rest_validate_request_arg', + ), + ), + 'not' => array( + 'type' => 'null', + 'arg_options' => array( 'sanitize_callback' => 'absint' ), + ), + 'patternProperties' => array( + '^S_' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'definitions' => array( + 'address' => array( + 'type' => 'object', + 'validate_callback' => 'rest_validate_request_arg', + 'properties' => array( + 'street' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + ), + 'dependencies' => array( + 'bar' => array( + 'type' => 'object', + 'validate_callback' => 'rest_validate_request_arg', + 'properties' => array( + 'baz' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + ), + 'qux' => array( 'bar' ), + ), + 'additionalProperties' => array( + 'type' => 'string', + 'sanitize_callback' => 'sanitize_text_field', + ), + ), + 'output_schema' => array( + 'type' => 'array', + 'items' => array( + array( + 'type' => 'string', + 'validate_callback' => 'is_string', + ), + array( + 'type' => 'number', + 'arg_options' => array( 'sanitize_callback' => 'absint' ), + ), + ), + 'additionalItems' => array( + 'type' => 'boolean', + 'sanitize_callback' => 'rest_sanitize_boolean', + ), + ), + 'execute_callback' => static function ( $input ) { + return array(); + }, + 'permission_callback' => '__return_true', + 'meta' => array( 'show_in_rest' => true ), + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/nested-internal-keywords' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + + // Verify internal keywords are stripped from anyOf sub-schemas. + $this->assertArrayHasKey( 'anyOf', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['anyOf'][0] ); + $this->assertSame( 'object', $data['input_schema']['anyOf'][0]['type'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['anyOf'][0]['properties']['value'] ); + $this->assertSame( 'string', $data['input_schema']['anyOf'][0]['properties']['value']['type'] ); + $this->assertArrayNotHasKey( 'arg_options', $data['input_schema']['anyOf'][1] ); + $this->assertSame( 'number', $data['input_schema']['anyOf'][1]['type'] ); + + // Verify internal keywords are stripped from oneOf sub-schemas. + $this->assertArrayHasKey( 'oneOf', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['oneOf'][0] ); + $this->assertSame( 'string', $data['input_schema']['oneOf'][0]['type'] ); + + // Verify internal keywords are stripped from allOf sub-schemas. + $this->assertArrayHasKey( 'allOf', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['allOf'][0] ); + $this->assertSame( 'object', $data['input_schema']['allOf'][0]['type'] ); + + // Verify internal keywords are stripped from not sub-schema. + $this->assertArrayHasKey( 'not', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'arg_options', $data['input_schema']['not'] ); + $this->assertSame( 'null', $data['input_schema']['not']['type'] ); + + // Verify internal keywords are stripped from patternProperties sub-schemas. + $this->assertArrayHasKey( 'patternProperties', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['patternProperties']['^S_'] ); + $this->assertSame( 'string', $data['input_schema']['patternProperties']['^S_']['type'] ); + + // Verify internal keywords are stripped from dependencies schema values. + $this->assertArrayHasKey( 'dependencies', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['dependencies']['bar'] ); + $this->assertSame( 'object', $data['input_schema']['dependencies']['bar']['type'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['dependencies']['bar']['properties']['baz'] ); + $this->assertSame( 'string', $data['input_schema']['dependencies']['bar']['properties']['baz']['type'] ); + // Property dependencies (numeric arrays) should pass through unchanged. + $this->assertSame( array( 'bar' ), $data['input_schema']['dependencies']['qux'] ); + + // Verify internal keywords are stripped from definitions sub-schemas. + $this->assertArrayHasKey( 'definitions', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['input_schema']['definitions']['address'] ); + $this->assertSame( 'object', $data['input_schema']['definitions']['address']['type'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['definitions']['address']['properties']['street'] ); + $this->assertSame( 'string', $data['input_schema']['definitions']['address']['properties']['street']['type'] ); + + // Verify internal keywords are stripped from additionalProperties sub-schema. + $this->assertArrayHasKey( 'additionalProperties', $data['input_schema'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['input_schema']['additionalProperties'] ); + $this->assertSame( 'string', $data['input_schema']['additionalProperties']['type'] ); + + // Verify internal keywords are stripped from tuple-style items sub-schemas. + $this->assertArrayHasKey( 'items', $data['output_schema'] ); + $this->assertCount( 2, $data['output_schema']['items'] ); + $this->assertArrayNotHasKey( 'validate_callback', $data['output_schema']['items'][0] ); + $this->assertSame( 'string', $data['output_schema']['items'][0]['type'] ); + $this->assertArrayNotHasKey( 'arg_options', $data['output_schema']['items'][1] ); + $this->assertSame( 'number', $data['output_schema']['items'][1]['type'] ); + + // Verify internal keywords are stripped from additionalItems sub-schema. + $this->assertArrayHasKey( 'additionalItems', $data['output_schema'] ); + $this->assertArrayNotHasKey( 'sanitize_callback', $data['output_schema']['additionalItems'] ); + $this->assertSame( 'boolean', $data['output_schema']['additionalItems']['type'] ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php new file mode 100644 index 0000000000000..c8a1c802091c5 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestAbilitiesV1RunController.php @@ -0,0 +1,1241 @@ +user->create( + array( + 'role' => 'editor', + ) + ); + + self::$no_permission_user_id = self::factory()->user->create( + array( + 'role' => 'subscriber', + ) + ); + + self::register_test_categories(); + } + + /** + * Tear down after class. + */ + public static function tear_down_after_class(): void { + // Clean up registered test ability categories. + foreach ( array( 'math', 'system', 'general' ) as $slug ) { + wp_unregister_ability_category( $slug ); + } + + parent::tear_down_after_class(); + } + + /** + * Set up before each test. + */ + public function set_up(): void { + parent::set_up(); + + global $wp_rest_server; + $wp_rest_server = new WP_REST_Server(); + $this->server = $wp_rest_server; + + do_action( 'rest_api_init' ); + + $this->register_test_abilities(); + + // Set default user for tests + wp_set_current_user( self::$user_id ); + } + + /** + * Tear down after each test. + */ + public function tear_down(): void { + // Clean up test abilities. + foreach ( wp_get_abilities() as $ability ) { + if ( ! str_starts_with( $ability->get_name(), 'test/' ) ) { + continue; + } + + wp_unregister_ability( $ability->get_name() ); + } + + global $wp_rest_server; + $wp_rest_server = null; + + parent::tear_down(); + } + + /** + * Register test categories for testing. + */ + public static function register_test_categories(): void { + // Simulates the init hook to allow test ability category registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_categories_init'; + + wp_register_ability_category( + 'math', + array( + 'label' => 'Math', + 'description' => 'Mathematical operations and calculations.', + ) + ); + + wp_register_ability_category( + 'system', + array( + 'label' => 'System', + 'description' => 'System information and operations.', + ) + ); + + wp_register_ability_category( + 'general', + array( + 'label' => 'General', + 'description' => 'General purpose abilities.', + ) + ); + + array_pop( $wp_current_filter ); + } + + /** + * Helper to register a test ability. + * + * @param string $name Ability name. + * @param array $args Ability arguments. + */ + private function register_test_ability( string $name, array $args ): void { + // Simulates the init hook to allow test abilities registration. + global $wp_current_filter; + $wp_current_filter[] = 'wp_abilities_api_init'; + + wp_register_ability( $name, $args ); + + array_pop( $wp_current_filter ); + } + + /** + * Register test abilities for testing. + */ + private function register_test_abilities(): void { + // Regular ability (POST only). + $this->register_test_ability( + 'test/calculator', + array( + 'label' => 'Calculator', + 'description' => 'Performs calculations', + 'category' => 'math', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'a' => array( + 'type' => 'number', + 'description' => 'First number', + ), + 'b' => array( + 'type' => 'number', + 'description' => 'Second number', + ), + ), + 'required' => array( 'a', 'b' ), + 'additionalProperties' => false, + ), + 'output_schema' => array( + 'type' => 'number', + ), + 'execute_callback' => static function ( array $input ) { + return $input['a'] + $input['b']; + }, + 'permission_callback' => static function () { + return current_user_can( 'edit_posts' ); + }, + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Read-only ability (GET method). + $this->register_test_ability( + 'test/user-info', + array( + 'label' => 'User Info', + 'description' => 'Gets user information', + 'category' => 'system', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'user_id' => array( + 'type' => 'integer', + 'default' => 0, + ), + ), + ), + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'id' => array( 'type' => 'integer' ), + 'login' => array( 'type' => 'string' ), + ), + ), + 'execute_callback' => static function ( array $input ) { + $user_id = $input['user_id'] ?? get_current_user_id(); + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + return new WP_Error( 'user_not_found', 'User not found' ); + } + return array( + 'id' => $user->ID, + 'login' => $user->user_login, + ); + }, + 'permission_callback' => static function () { + return is_user_logged_in(); + }, + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + // Destructive ability (DELETE method). + $this->register_test_ability( + 'test/delete-user', + array( + 'label' => 'Delete User', + 'description' => 'Deletes a user', + 'category' => 'system', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'user_id' => array( + 'type' => 'integer', + 'default' => 0, + ), + ), + ), + 'output_schema' => array( + 'type' => 'string', + 'required' => true, + ), + 'execute_callback' => static function ( array $input ) { + $user_id = $input['user_id'] ?? get_current_user_id(); + $user = get_user_by( 'id', $user_id ); + if ( ! $user ) { + return new WP_Error( 'user_not_found', 'User not found' ); + } + return 'User successfully deleted!'; + }, + 'permission_callback' => static function () { + return is_user_logged_in(); + }, + 'meta' => array( + 'annotations' => array( + 'destructive' => true, + 'idempotent' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + // Ability with contextual permissions + $this->register_test_ability( + 'test/restricted', + array( + 'label' => 'Restricted Action', + 'description' => 'Requires specific input for permission', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'secret' => array( 'type' => 'string' ), + 'data' => array( 'type' => 'string' ), + ), + 'required' => array( 'secret', 'data' ), + ), + 'output_schema' => array( + 'type' => 'string', + ), + 'execute_callback' => static function ( array $input ) { + return 'Success: ' . $input['data']; + }, + 'permission_callback' => static function ( array $input ) { + // Only allow if secret matches + return isset( $input['secret'] ) && 'valid_secret' === $input['secret']; + }, + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that does not show in REST. + $this->register_test_ability( + 'test/not-show-in-rest', + array( + 'label' => 'Hidden from REST', + 'description' => 'It does not show in REST.', + 'category' => 'general', + 'execute_callback' => static function (): int { + return 0; + }, + 'permission_callback' => '__return_true', + ) + ); + + // Ability that returns null + $this->register_test_ability( + 'test/null-return', + array( + 'label' => 'Null Return', + 'description' => 'Returns null', + 'category' => 'general', + 'execute_callback' => static function () { + return null; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability that returns WP_Error + $this->register_test_ability( + 'test/error-return', + array( + 'label' => 'Error Return', + 'description' => 'Returns error', + 'category' => 'general', + 'execute_callback' => static function () { + return new WP_Error( 'test_error', 'This is a test error' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Ability with invalid output + $this->register_test_ability( + 'test/invalid-output', + array( + 'label' => 'Invalid Output', + 'description' => 'Returns invalid output', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'number', + ), + 'execute_callback' => static function () { + return 'not a number'; // Invalid - schema expects number + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Read-only ability for query params testing. + $this->register_test_ability( + 'test/query-params', + array( + 'label' => 'Query Params Test', + 'description' => 'Tests query parameter handling', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'param1' => array( 'type' => 'string' ), + 'param2' => array( 'type' => 'integer' ), + ), + ), + 'execute_callback' => static function ( $input ) { + return $input; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + } + + /** + * Test executing a regular ability with POST. + * + * @ticket 64098 + */ + public function test_execute_regular_ability_post(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'input' => array( + 'a' => 5, + 'b' => 3, + ), + ) + ) + ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertEquals( 8, $response->get_data() ); + } + + /** + * Test executing a read-only ability with GET. + * + * @ticket 64098 + */ + public function test_execute_readonly_ability_get(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/user-info/run' ); + $request->set_query_params( + array( + 'input' => array( + 'user_id' => self::$user_id, + ), + ) + ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( self::$user_id, $data['id'] ); + } + + /** + * Test executing a destructive ability with GET. + * + * @ticket 64098 + */ + public function test_execute_destructive_ability_delete(): void { + $request = new WP_REST_Request( 'DELETE', '/wp-abilities/v1/abilities/test/delete-user/run' ); + $request->set_query_params( + array( + 'input' => array( + 'user_id' => self::$user_id, + ), + ) + ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 'User successfully deleted!', $response->get_data() ); + } + + /** + * Test HTTP method validation for regular abilities. + * + * @ticket 64098 + */ + public function test_regular_ability_requires_post(): void { + $this->register_test_ability( + 'test/open-tool', + array( + 'label' => 'Open Tool', + 'description' => 'Tool with no permission requirements', + 'category' => 'general', + 'execute_callback' => static function () { + return 'success'; + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/open-tool/run' ); + $response = $this->server->dispatch( $request ); + + $this->assertSame( 405, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_invalid_method', $data['code'] ); + $this->assertSame( 'Abilities that perform updates require POST method.', $data['message'] ); + } + + /** + * Test HTTP method validation for read-only abilities. + * + * @ticket 64098 + */ + public function test_readonly_ability_requires_get(): void { + // Try POST on a read-only ability (should fail). + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/user-info/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( array( 'user_id' => 1 ) ) ); + + $response = $this->server->dispatch( $request ); + + $this->assertSame( 405, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_invalid_method', $data['code'] ); + $this->assertSame( 'Read-only abilities require GET method.', $data['message'] ); + } + + /** + * Test HTTP method validation for destructive abilities. + * + * @ticket 64098 + */ + public function test_destructive_ability_requires_delete(): void { + // Try POST on a destructive ability (should fail). + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/delete-user/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( array( 'user_id' => 1 ) ) ); + + $response = $this->server->dispatch( $request ); + + $this->assertSame( 405, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_invalid_method', $data['code'] ); + $this->assertSame( 'Abilities that perform destructive actions require DELETE method.', $data['message'] ); + } + + /** + * Test output validation against schema. + * Note: When output validation fails in WP_Ability::execute(), it returns null, + * which causes the REST controller to return 'ability_invalid_output'. + * + * @ticket 64098 + */ + public function test_output_validation(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/invalid-output/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + $this->assertSame( 500, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'ability_invalid_output', $data['code'] ); + $this->assertSame( + 'Ability "test/invalid-output" has invalid output. Reason: output is not of type number.', + $data['message'] + ); + } + + /** + * Test permission check for execution. + * + * @ticket 64098 + */ + public function test_execution_permission_denied(): void { + wp_set_current_user( self::$no_permission_user_id ); + + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'input' => array( + 'a' => 5, + 'b' => 3, + ), + ) + ) + ); + + $response = $this->server->dispatch( $request ); + + $this->assertSame( 403, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_cannot_execute', $data['code'] ); + $this->assertSame( 'Sorry, you are not allowed to execute this ability.', $data['message'] ); + } + + /** + * Test contextual permission check. + * + * @ticket 64098 + */ + public function test_contextual_permission_check(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/restricted/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'input' => array( + 'secret' => 'wrong_secret', + 'data' => 'test data', + ), + ) + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 403, $response->get_status() ); + + $request->set_body( + wp_json_encode( + array( + 'input' => array( + 'secret' => 'valid_secret', + 'data' => 'test data', + ), + ) + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + $this->assertSame( 'Success: test data', $response->get_data() ); + } + + /** + * Test handling an ability that does not show in REST. + * + * @ticket 64098 + */ + public function test_do_not_show_in_rest(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/not-show-in-rest/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); + $this->assertSame( 'Ability not found.', $data['message'] ); + } + + /** + * Test handling of null is a valid return value. + * + * @ticket 64098 + */ + public function test_null_return_handling(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/null-return/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertNull( $data ); + } + + /** + * Test handling of WP_Error return from ability. + * + * @ticket 64098 + */ + public function test_wp_error_return_handling(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/error-return/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 500, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'test_error', $data['code'] ); + $this->assertSame( 'This is a test error', $data['message'] ); + } + + /** + * Test non-existent ability returns 404. + * + * @ticket 64098 + * + * @expectedIncorrectUsage WP_Abilities_Registry::get_registered + */ + public function test_execute_non_existent_ability(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/non/existent/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 404, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_not_found', $data['code'] ); + } + + /** + * Test schema retrieval for run endpoint. + * + * @ticket 64098 + */ + public function test_run_endpoint_schema(): void { + $request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/abilities/test/calculator/run' ); + $response = $this->server->dispatch( $request ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'schema', $data ); + $schema = $data['schema']; + + $this->assertSame( 'ability-execution', $schema['title'] ); + $this->assertSame( 'object', $schema['type'] ); + $this->assertArrayHasKey( 'properties', $schema ); + $this->assertArrayHasKey( 'result', $schema['properties'] ); + } + + /** + * Test that invalid JSON in POST body is handled correctly. + * + * @ticket 64098 + */ + public function test_invalid_json_in_post_body(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + // Set raw body with invalid JSON + $request->set_body( '{"input": {invalid json}' ); + + $response = $this->server->dispatch( $request ); + + // When JSON is invalid, WordPress returns 400 Bad Request + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test GET request with complex nested input array. + * + * @ticket 64098 + */ + public function test_get_request_with_nested_input_array(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/query-params/run' ); + $request->set_query_params( + array( + 'input' => array( + 'level1' => array( + 'level2' => array( + 'value' => 'nested', + ), + ), + 'array' => array( 1, 2, 3 ), + ), + ) + ); + + $response = $this->server->dispatch( $request ); + $this->assertEquals( 200, $response->get_status() ); + + $data = $response->get_data(); + $this->assertSame( 'nested', $data['level1']['level2']['value'] ); + $this->assertEquals( array( 1, 2, 3 ), $data['array'] ); + } + + /** + * Test GET request with non-array input parameter. + * + * @ticket 64098 + */ + public function test_get_request_with_non_array_input(): void { + $request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/query-params/run' ); + $request->set_query_params( + array( + 'input' => 'not-an-array', // String instead of array + ) + ); + + $response = $this->server->dispatch( $request ); + // When input is not an array, WordPress returns 400 Bad Request + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test POST request with non-array input in JSON body. + * + * @ticket 64098 + */ + public function test_post_request_with_non_array_input(): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( + wp_json_encode( + array( + 'input' => 'string-value', // String instead of array + ) + ) + ); + + $response = $this->server->dispatch( $request ); + // When input is not an array, WordPress returns 400 Bad Request + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test ability with invalid output that fails validation. + * + * @ticket 64098 + */ + public function test_output_validation_failure_returns_error(): void { + // Register ability with strict output schema. + $this->register_test_ability( + 'test/strict-output', + array( + 'label' => 'Strict Output', + 'description' => 'Ability with strict output schema', + 'category' => 'general', + 'output_schema' => array( + 'type' => 'object', + 'properties' => array( + 'status' => array( + 'type' => 'string', + 'enum' => array( 'success', 'failure' ), + ), + ), + 'required' => array( 'status' ), + ), + 'execute_callback' => static function () { + // Return invalid output that doesn't match schema + return array( 'wrong_field' => 'value' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/strict-output/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + + $response = $this->server->dispatch( $request ); + + // Should return error when output validation fails. + $this->assertSame( 500, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'ability_invalid_output', $data['code'] ); + $this->assertSame( + 'Ability "test/strict-output" has invalid output. Reason: status is a required property of output.', + $data['message'] + ); + } + + /** + * Test ability with invalid input that fails validation. + * + * @ticket 64098 + */ + public function test_input_validation_failure_returns_error(): void { + // Register ability with strict input schema. + $this->register_test_ability( + 'test/strict-input', + array( + 'label' => 'Strict Input', + 'description' => 'Ability with strict input schema', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + 'properties' => array( + 'required_field' => array( + 'type' => 'string', + ), + ), + 'required' => array( 'required_field' ), + ), + 'execute_callback' => static function () { + return array( 'status' => 'success' ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/strict-input/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + // Missing required field + $request->set_body( wp_json_encode( array( 'input' => array( 'other_field' => 'value' ) ) ) ); + + $response = $this->server->dispatch( $request ); + + // Should return error when input validation fails. + $this->assertSame( 400, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'ability_invalid_input', $data['code'] ); + $this->assertSame( + 'Ability "test/strict-input" has invalid input. Reason: required_field is a required property of input.', + $data['message'] + ); + } + + /** + * Test ability without annotations defaults to POST method. + * + * @ticket 64098 + */ + public function test_ability_without_annotations_defaults_to_post_method(): void { + // Register ability without annotations. + $this->register_test_ability( + 'test/no-annotations', + array( + 'label' => 'No Annotations', + 'description' => 'Ability without annotations.', + 'category' => 'general', + 'execute_callback' => static function () { + return array( 'executed' => true ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Should require POST (default behavior). + $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/no-annotations/run' ); + $get_response = $this->server->dispatch( $get_request ); + $this->assertEquals( 405, $get_response->get_status() ); + + // Should work with POST. + $post_request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/no-annotations/run' ); + $post_request->set_header( 'Content-Type', 'application/json' ); + + $post_response = $this->server->dispatch( $post_request ); + $this->assertEquals( 200, $post_response->get_status() ); + } + + /** + * Test edge case with empty input for GET method. + * + * @ticket 64098 + */ + public function test_empty_input_handling_get_method(): void { + $this->register_test_ability( + 'test/read-only-empty', + array( + 'label' => 'Read-only Empty', + 'description' => 'Read-only with empty input.', + 'category' => 'general', + 'execute_callback' => static function () { + return array( 'input_was_empty' => 0 === func_num_args() ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + // Tests GET with no input parameter. + $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/read-only-empty/run' ); + $get_response = $this->server->dispatch( $get_request ); + $this->assertEquals( 200, $get_response->get_status() ); + $this->assertTrue( $get_response->get_data()['input_was_empty'] ); + } + + /** + * Test edge case with empty input for GET method, and normalized input using schema. + * + * @ticket 64098 + */ + public function test_empty_input_handling_get_method_with_normalized_input(): void { + $this->register_test_ability( + 'test/read-only-empty-array', + array( + 'label' => 'Read-only Empty Array', + 'description' => 'Read-only with inferred empty array input from schema.', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'array', + 'default' => array(), + ), + 'execute_callback' => static function ( $input ) { + return is_array( $input ) && empty( $input ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'annotations' => array( + 'readonly' => true, + ), + 'show_in_rest' => true, + ), + ) + ); + + // Tests GET with no input parameter. + $get_request = new WP_REST_Request( 'GET', '/wp-abilities/v1/abilities/test/read-only-empty-array/run' ); + $get_response = $this->server->dispatch( $get_request ); + $this->assertEquals( 200, $get_response->get_status() ); + $this->assertTrue( $get_response->get_data() ); + } + + /** + * Test edge case with empty input for POST method. + * + * @ticket 64098 + */ + public function test_empty_input_handling_post_method(): void { + $this->register_test_ability( + 'test/regular-empty', + array( + 'label' => 'Regular Empty', + 'description' => 'Regular with empty input.', + 'category' => 'general', + 'execute_callback' => static function () { + return array( 'input_was_empty' => 0 === func_num_args() ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + // Tests POST with no body. + $post_request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/regular-empty/run' ); + $post_request->set_header( 'Content-Type', 'application/json' ); + $post_request->set_body( '{}' ); // Empty JSON object + + $post_response = $this->server->dispatch( $post_request ); + $this->assertEquals( 200, $post_response->get_status() ); + $this->assertTrue( $post_response->get_data()['input_was_empty'] ); + } + + /** + * Data provider for malformed JSON tests. + * + * @return array + */ + public function data_malformed_json_provider(): array { + return array( + 'Missing value' => array( '{"input": }' ), + 'Trailing comma in array' => array( '{"input": [1, 2, }' ), + 'Missing quotes on key' => array( '{input: {}}' ), + 'JavaScript undefined' => array( '{"input": undefined}' ), + 'JavaScript NaN' => array( '{"input": NaN}' ), + 'Missing quotes nested keys' => array( '{"input": {a: 1, b: 2}}' ), + 'Single quotes' => array( '\'{"input": {}}\'' ), + 'Unclosed object' => array( '{"input": {"key": "value"' ), + ); + } + + /** + * Test malformed JSON in POST body. + * + * @ticket 64098 + * + * @dataProvider data_malformed_json_provider + * + * @param string $json Malformed JSON to test. + */ + public function test_malformed_json_post_body( string $json ): void { + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/calculator/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( $json ); + + $response = $this->server->dispatch( $request ); + + // Malformed JSON should result in 400 Bad Request + $this->assertEquals( 400, $response->get_status() ); + } + + /** + * Test input with various PHP types as strings. + * + * @ticket 64098 + */ + public function test_php_type_strings_in_input(): void { + // Register ability that accepts any input + $this->register_test_ability( + 'test/echo', + array( + 'label' => 'Echo', + 'description' => 'Echoes input', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + ), + 'execute_callback' => static function ( $input ) { + return array( 'echo' => $input ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $inputs = array( + 'null' => null, + 'true' => true, + 'false' => false, + 'int' => 123, + 'float' => 123.456, + 'string' => 'test', + 'empty' => '', + 'zero' => 0, + 'negative' => -1, + ); + + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/echo/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( array( 'input' => $inputs ) ) ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertEquals( $inputs, $data['echo'] ); + } + + /** + * Test input with mixed encoding. + * + * @ticket 64098 + */ + public function test_mixed_encoding_in_input(): void { + // Register ability that accepts any input + $this->register_test_ability( + 'test/echo-encoding', + array( + 'label' => 'Echo Encoding', + 'description' => 'Echoes input with encoding', + 'category' => 'general', + 'input_schema' => array( + 'type' => 'object', + ), + 'execute_callback' => static function ( $input ) { + return array( 'echo' => $input ); + }, + 'permission_callback' => '__return_true', + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $input = array( + 'utf8' => 'Hello 世界', + 'emoji' => '🎉🎊🎈', + 'html' => '', + 'encoded' => '<test>', + 'newlines' => "line1\nline2\rline3\r\nline4", + 'tabs' => "col1\tcol2\tcol3", + 'quotes' => "It's \"quoted\"", + ); + + $request = new WP_REST_Request( 'POST', '/wp-abilities/v1/abilities/test/echo-encoding/run' ); + $request->set_header( 'Content-Type', 'application/json' ); + $request->set_body( wp_json_encode( array( 'input' => $input ) ) ); + + $response = $this->server->dispatch( $request ); + + $this->assertEquals( 200, $response->get_status() ); + $data = $response->get_data(); + + // Input should be preserved exactly + $this->assertEquals( $input['utf8'], $data['echo']['utf8'] ); + $this->assertEquals( $input['emoji'], $data['echo']['emoji'] ); + $this->assertEquals( $input['html'], $data['echo']['html'] ); + } + + /** + * Data provider for invalid HTTP methods. + * + * @return array + */ + public function data_invalid_http_methods_provider(): array { + return array( + 'PATCH' => array( 'PATCH' ), + 'PUT' => array( 'PUT' ), + 'DELETE' => array( 'DELETE' ), + 'HEAD' => array( 'HEAD' ), + ); + } + + /** + * Test request with invalid HTTP methods. + * + * @ticket 64098 + * + * @dataProvider data_invalid_http_methods_provider + * + * @param string $method HTTP method to test. + */ + public function test_invalid_http_methods( string $method ): void { + // Register an ability with no permission requirements for this test + $this->register_test_ability( + 'test/method-test', + array( + 'label' => 'Method Test', + 'description' => 'Test ability for HTTP method validation', + 'category' => 'general', + 'execute_callback' => static function () { + return array( 'success' => true ); + }, + 'permission_callback' => '__return_true', // No permission requirements + 'meta' => array( + 'show_in_rest' => true, + ), + ) + ); + + $request = new WP_REST_Request( $method, '/wp-abilities/v1/abilities/test/method-test/run' ); + $response = $this->server->dispatch( $request ); + + // Regular abilities should only accept POST, so these should return 405. + $this->assertSame( 405, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'rest_ability_invalid_method', $data['code'] ); + $this->assertSame( 'Abilities that perform updates require POST method.', $data['message'] ); + } + + /** + * Test OPTIONS method handling. + * + * @ticket 64098 + */ + public function test_options_method_handling(): void { + $request = new WP_REST_Request( 'OPTIONS', '/wp-abilities/v1/abilities/test/calculator/run' ); + $response = $this->server->dispatch( $request ); + // OPTIONS requests return 200 with allowed methods + $this->assertEquals( 200, $response->get_status() ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php b/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php new file mode 100644 index 0000000000000..408021b039d31 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestBlockPatternCategoriesController.php @@ -0,0 +1,236 @@ +user->create( array( 'role' => 'administrator' ) ); + + // Setup an empty testing instance of `WP_Block_Pattern_Categories_Registry` and save the original. + self::$orig_registry = WP_Block_Pattern_Categories_Registry::get_instance(); + self::$registry_instance_property = new ReflectionProperty( 'WP_Block_Pattern_Categories_Registry', 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + self::$registry_instance_property->setAccessible( true ); + } + $test_registry = new WP_Block_Pattern_Categories_Registry(); + self::$registry_instance_property->setValue( null, $test_registry ); + + // Register some categories in the test registry. + $test_registry->register( + 'test', + array( + 'label' => 'Test', + 'description' => 'Test description', + ) + ); + $test_registry->register( + 'query', + array( + 'label' => 'Query', + 'description' => 'Query', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + + // Restore the original registry instance. + self::$registry_instance_property->setValue( null, self::$orig_registry ); + + if ( PHP_VERSION_ID < 80100 ) { + self::$registry_instance_property->setAccessible( false ); + } + self::$registry_instance_property = null; + self::$orig_registry = null; + } + + public function set_up() { + parent::set_up(); + + switch_theme( 'emptytheme' ); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( static::REQUEST_ROUTE, $routes ); + } + + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + + $expected_names = array( 'test', 'query' ); + $expected_fields = array( 'name', 'label', 'description' ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $request['_fields'] = 'name,label,description'; + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertCount( count( $expected_names ), $data ); + foreach ( $data as $idx => $item ) { + $this->assertSame( $expected_names[ $idx ], $item['name'] ); + $this->assertSame( $expected_fields, array_keys( $item ) ); + } + } + + /** + * @ticket 56481 + */ + public function test_get_items_with_head_request_should_not_prepare_block_pattern_categories_data() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', static::REQUEST_ROUTE ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', static::REQUEST_ROUTE ); + $request->set_param( '_fields', 'name' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Verify capability check for unauthorized request (not logged in). + */ + public function test_get_items_unauthorized() { + // Ensure current user is logged out. + wp_logout(); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_do_request( $request ); + + $this->assertWPError( $response->as_error() ); + $this->assertSame( 401, $response->get_status() ); + } + + /** + * Verify capability check for forbidden request (insufficient capability). + */ + public function test_get_items_forbidden() { + // Set current user without `edit_posts` capability. + wp_set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_do_request( $request ); + + $this->assertWPError( $response->as_error() ); + $this->assertSame( 403, $response->get_status() ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() { + // Controller does not implement get_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestBlockPatternsController.php b/tests/phpunit/tests/rest-api/wpRestBlockPatternsController.php new file mode 100644 index 0000000000000..938405479faee --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestBlockPatternsController.php @@ -0,0 +1,287 @@ +user->create( array( 'role' => 'administrator' ) ); + + // Setup an empty testing instance of `WP_Block_Patterns_Registry` and save the original. + self::$orig_registry = WP_Block_Patterns_Registry::get_instance(); + self::$registry_instance_property = new ReflectionProperty( 'WP_Block_Patterns_Registry', 'instance' ); + if ( PHP_VERSION_ID < 80100 ) { + self::$registry_instance_property->setAccessible( true ); + } + $test_registry = new WP_Block_Pattern_Categories_Registry(); + self::$registry_instance_property->setValue( null, $test_registry ); + + // Register some patterns in the test registry. + $test_registry->register( + 'test/one', + array( + 'title' => 'Pattern One', + 'content' => '

      One

      ', + 'viewportWidth' => 1440, + 'categories' => array( 'test' ), + 'templateTypes' => array( 'page' ), + 'source' => 'theme', + ) + ); + + $test_registry->register( + 'test/two', + array( + 'title' => 'Pattern Two', + 'content' => '

      Two

      ', + 'categories' => array( 'test' ), + 'templateTypes' => array( 'single' ), + 'source' => 'core', + ) + ); + + $test_registry->register( + 'test/three', + array( + 'title' => 'Pattern Three', + 'content' => '

      Three

      ', + 'categories' => array( 'test', 'buttons', 'query' ), + 'source' => 'pattern-directory/featured', + ) + ); + } + + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + + // Restore the original registry instance. + self::$registry_instance_property->setValue( null, self::$orig_registry ); + if ( PHP_VERSION_ID < 80100 ) { + self::$registry_instance_property->setAccessible( false ); + } + self::$registry_instance_property = null; + self::$orig_registry = null; + } + + public function set_up() { + parent::set_up(); + + switch_theme( 'emptytheme' ); + } + + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( static::REQUEST_ROUTE, $routes ); + } + + /** + * @group external-http + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $request['_fields'] = 'name,content,source,template_types'; + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertIsArray( $data, 'WP_REST_Block_Patterns_Controller::get_items() should return an array' ); + $this->assertGreaterThanOrEqual( 2, count( $data ), 'WP_REST_Block_Patterns_Controller::get_items() should return at least 2 items' ); + $this->assertSame( + array( + 'name' => 'test/one', + 'content' => '

      One

      ', + 'template_types' => array( 'page' ), + 'source' => 'theme', + ), + $data[0], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/one' + ); + $this->assertSame( + array( + 'name' => 'test/two', + 'content' => '

      Two

      ', + 'template_types' => array( 'single' ), + 'source' => 'core', + ), + $data[1], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/two' + ); + } + + /** + * Verify capability check for unauthorized request (not logged in). + */ + public function test_get_items_unauthorized() { + // Ensure current user is logged out. + wp_logout(); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_do_request( $request ); + + $this->assertWPError( $response->as_error() ); + $this->assertSame( 401, $response->get_status() ); + } + + /** + * Verify capability check for forbidden request (insufficient capability). + */ + public function test_get_items_forbidden() { + // Set current user without `edit_posts` capability. + wp_set_current_user( self::factory()->user->create( array( 'role' => 'subscriber' ) ) ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_do_request( $request ); + + $this->assertWPError( $response->as_error() ); + $this->assertSame( 403, $response->get_status() ); + } + + /** + * Tests the proper migration of old core pattern categories to new ones. + * + * @since 6.2.0 + * + * @ticket 57532 + * @group external-http + * + * @covers WP_REST_Block_Patterns_Controller::get_items + */ + public function test_get_items_migrate_pattern_categories() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $request['_fields'] = 'name,categories'; + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertIsArray( $data, 'WP_REST_Block_Patterns_Controller::get_items() should return an array' ); + $this->assertGreaterThanOrEqual( 3, count( $data ), 'WP_REST_Block_Patterns_Controller::get_items() should return at least 3 items' ); + $this->assertSame( + array( + 'name' => 'test/one', + 'categories' => array( 'test' ), + ), + $data[0], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/one' + ); + $this->assertSame( + array( + 'name' => 'test/two', + 'categories' => array( 'test' ), + ), + $data[1], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/two' + ); + $this->assertSame( + array( + 'name' => 'test/three', + 'categories' => array( 'test', 'call-to-action', 'posts' ), + ), + $data[2], + 'WP_REST_Block_Patterns_Controller::get_items() should return test/three' + ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() { + // Controller does not implement get_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestEditSiteExportController.php b/tests/phpunit/tests/rest-api/wpRestEditSiteExportController.php new file mode 100644 index 0000000000000..5ed45fe2ca54a --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestEditSiteExportController.php @@ -0,0 +1,150 @@ +user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + /** + * Delete test data after our tests run. + * + * @since 5.9.0 + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$subscriber_id ); + } + + /** + * @covers WP_REST_Edit_Site_Export_Controller::register_routes + * @ticket 54448 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( static::REQUEST_ROUTE, $routes ); + $this->assertCount( 1, $routes[ static::REQUEST_ROUTE ] ); + } + + /** + * @covers WP_REST_Edit_Site_Export_Controller::permissions_check + * + * @ticket 54448 + */ + public function test_export_for_no_user_permissions() { + wp_set_current_user( 0 ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_export_templates', $response, 401 ); + } + + /** + * @covers WP_REST_Edit_Site_Export_Controller::permissions_check + * + * @ticket 54448 + */ + public function test_export_for_user_with_insufficient_permissions() { + wp_set_current_user( self::$subscriber_id ); + + $request = new WP_REST_Request( 'GET', static::REQUEST_ROUTE ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_export_templates', $response, 403 ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_context_param() { + // Controller does not use get_context_param(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item() { + // Controller does not implement get_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_items() { + // Controller does not implement get_items(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @doesNotPerformAssertions + */ + public function test_get_item_schema() { + // Controller does not implement get_item_schema(). + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php new file mode 100644 index 0000000000000..a5b76af3438d2 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestMenuItemsController.php @@ -0,0 +1,1080 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + /** + * + */ + public static function wpTearDownAfterClass() { + self::delete_user( self::$admin_id ); + self::delete_user( self::$subscriber_id ); + } + + /** + * + */ + public function set_up() { + parent::set_up(); + + $this->tag_id = self::factory()->tag->create(); + + $this->menu_id = wp_create_nav_menu( rand_str() ); + + $this->menu_item_id = wp_update_nav_menu_item( + $this->menu_id, + 0, + array( + 'menu-item-type' => 'taxonomy', + 'menu-item-object' => 'post_tag', + 'menu-item-object-id' => $this->tag_id, + 'menu-item-status' => 'publish', + ) + ); + } + + /** + * @ticket 40878 + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + + $this->assertArrayHasKey( '/wp/v2/menu-items', $routes ); + $this->assertCount( 2, $routes['/wp/v2/menu-items'] ); + $this->assertArrayHasKey( '/wp/v2/menu-items/(?P[\d]+)', $routes ); + $this->assertCount( 3, $routes['/wp/v2/menu-items/(?P[\d]+)'] ); + } + + /** + * @ticket 40878 + * @covers ::get_context_param + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items/' . $this->menu_item_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + } + + /** + * @ticket 40878 + * @covers ::get_collection_params + */ + public function test_registered_query_params() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['endpoints'][0]['args']; + $this->assertArrayHasKey( 'before', $properties ); + $this->assertArrayHasKey( 'context', $properties ); + $this->assertArrayHasKey( 'exclude', $properties ); + $this->assertArrayHasKey( 'include', $properties ); + $this->assertArrayHasKey( 'menu_order', $properties ); + $this->assertArrayHasKey( 'menus', $properties ); + $this->assertArrayHasKey( 'menus_exclude', $properties ); + $this->assertArrayHasKey( 'offset', $properties ); + $this->assertArrayHasKey( 'order', $properties ); + $this->assertArrayHasKey( 'orderby', $properties ); + $this->assertArrayHasKey( 'page', $properties ); + $this->assertArrayHasKey( 'per_page', $properties ); + $this->assertArrayHasKey( 'search', $properties ); + $this->assertArrayHasKey( 'search_columns', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + } + + /** + * @ticket 40878 + */ + public function test_registered_get_item_params() { + $request = new WP_REST_Request( 'OPTIONS', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $keys = array_keys( $data['endpoints'][0]['args'] ); + $this->assertEqualSets( array( 'context', 'id' ), $keys ); + } + + /** + * @ticket 40878 + * @covers ::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_items_response( $response ); + } + + /** + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_item_response( $response, 'view' ); + } + + /** + * @ticket 54304 + * @covers ::get_items + */ + public function test_get_items_filter() { + add_filter( 'rest_menu_read_access', '__return_true' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_items_response( $response ); + } + + /** + * @ticket 54304 + * @covers ::get_item + */ + public function test_get_item_filter() { + add_filter( 'rest_menu_read_access', '__return_true' ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_item_response( $response, 'view' ); + } + + /** + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item_edit() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_item_response( $response, 'edit' ); + } + + /** + * @ticket 40878 + * @covers ::get_item + * @covers ::prepare_links + */ + public function test_get_item_term_links() { + wp_set_current_user( self::$admin_id ); + + $menu_item_id = wp_update_nav_menu_item( + $this->menu_id, + 0, + array( + 'menu-item-type' => 'taxonomy', + 'menu-item-object' => 'post_tag', + 'menu-item-object-id' => $this->tag_id, + 'menu-item-status' => 'publish', + 'menu-item-title' => 'Food', + ) + ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $menu_item_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_item_response( $response, 'edit' ); + } + + /** + * @ticket 40878 + * @covers ::get_item + * @covers ::prepare_links + */ + public function test_get_item_term_posts() { + wp_set_current_user( self::$admin_id ); + + $post_id = self::factory()->post->create(); + + $menu_item_id = wp_update_nav_menu_item( + $this->menu_id, + 0, + array( + 'menu-item-type' => 'post_type', + 'menu-item-object' => 'post', + 'menu-item-object-id' => $post_id, + 'menu-item-status' => 'publish', + 'menu-item-title' => 'Food', + ) + ); + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menu-items/%d', $menu_item_id ) ); + $request->set_param( 'context', 'edit' ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_get_menu_item_response( $response, 'edit' ); + } + + /** + * Test that title.raw contains the verbatim title and that title.rendered + * has been passed through the_title which escapes & characters. + * + * @see https://github.com/WordPress/gutenberg/pull/24673 + * + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item_escapes_title() { + wp_set_current_user( self::$admin_id ); + + $menu_item_id = wp_update_nav_menu_item( + $this->menu_id, + 0, + array( + 'menu-item-type' => 'taxonomy', + 'menu-item-object' => 'post_tag', + 'menu-item-object-id' => $this->tag_id, + 'menu-item-status' => 'publish', + 'menu-item-title' => 'Foo & bar', + ) + ); + + $request = new WP_REST_Request( + 'GET', + "/wp/v2/menu-items/$menu_item_id" + ); + $request->set_query_params( + array( + 'context' => 'edit', + ) + ); + + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + $title = $data['title']; + + if ( ! is_multisite() ) { + // Check that title.raw is the unescaped title and that + // title.rendered has been run through the_title. + $this->assertSame( 'Foo & bar', $title['rendered'] ); + $this->assertSame( 'Foo & bar', $title['raw'] ); + } else { + // In a multisite, administrators do not have unfiltered_html and + // post_title is ran through wp_kses before being saved in the + // database. Running the title through the_title does nothing in + // this case. + $this->assertSame( 'Foo & bar', $title['rendered'] ); + $this->assertSame( 'Foo & bar', $title['raw'] ); + } + + wp_delete_post( $menu_item_id ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data(); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->check_create_menu_item_response( $response ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_invalid() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menus' => array( 123, 456 ), + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_term() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'taxonomy', + 'title' => 'Tags', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_term_invalid_id', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_change_position() { + wp_set_current_user( self::$admin_id ); + $new_menu_id = wp_create_nav_menu( rand_str() ); + $expected = array(); + $actual = array(); + for ( $i = 1; $i < 5; $i++ ) { + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menu_order' => $i, + 'menus' => $new_menu_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->check_create_menu_item_response( $response ); + $data = $response->get_data(); + + $expected[] = $i; + $actual[] = $data['menu_order']; + } + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_menu_order_must_be_set() { + wp_set_current_user( self::$admin_id ); + $new_menu_id = wp_create_nav_menu( rand_str() ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menu_order' => 0, + 'menus' => $new_menu_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menu_order' => 1, + 'menus' => $new_menu_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_position_2() { + wp_set_current_user( self::$admin_id ); + $new_menu_id = wp_create_nav_menu( rand_str() ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menu_order' => 'ddddd', + 'menus' => $new_menu_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_position_3() { + wp_set_current_user( self::$admin_id ); + $new_menu_id = wp_create_nav_menu( rand_str() ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menu_order' => -9, + 'menus' => $new_menu_id, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_parent() { + wp_set_current_user( self::$admin_id ); + wp_create_nav_menu( rand_str() ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'parent' => -9, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_menu() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'menus' => -9, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'invalid_menu_id', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_post() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'post_type', + 'title' => 'Post', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_post_type() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'post_type_archive', + 'menu-item-object' => 'invalid_post_type', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_type', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_custom_link() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'custom', + 'title' => '', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_title_required', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_missing_custom_link_url() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'custom', + 'url' => '', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_url_required', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_invalid_custom_link_url() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'type' => 'custom', + 'url' => '"^<>{}`', + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertArrayHasKey( 'url', $response->get_data()['data']['details'] ); + $this->assertSame( 'rest_invalid_url', $response->get_data()['data']['details']['url']['code'] ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'xfn' => array( 'test1', 'test2', 'test3' ), + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->check_update_menu_item_response( $response ); + $new_data = $response->get_data(); + $this->assertSame( $this->menu_item_id, $new_data['id'] ); + $this->assertSame( $params['title'], $new_data['title']['raw'] ); + $this->assertSame( $params['description'], $new_data['description'] ); + $this->assertSame( $params['type_label'], $new_data['type_label'] ); + $this->assertSame( $params['xfn'], $new_data['xfn'] ); + $post = get_post( $this->menu_item_id ); + $menu_item = wp_setup_nav_menu_item( $post ); + $this->assertSame( $params['title'], $menu_item->title ); + $this->assertSame( $params['description'], $menu_item->description ); + $this->assertSame( $params['xfn'], explode( ' ', $menu_item->xfn ) ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_clean_xfn() { + wp_set_current_user( self::$admin_id ); + + $bad_data = array( 'test1":|":', 'test2+|+', 'test3±', 'test4😀' ); + $good_data = array( 'test1', 'test2', 'test3', 'test4' ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data( + array( + 'xfn' => $bad_data, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->check_update_menu_item_response( $response ); + $new_data = $response->get_data(); + $this->assertSame( $this->menu_item_id, $new_data['id'] ); + $this->assertSame( $params['title'], $new_data['title']['raw'] ); + $this->assertSame( $params['description'], $new_data['description'] ); + $this->assertSame( $params['type_label'], $new_data['type_label'] ); + $this->assertSame( $good_data, $new_data['xfn'] ); + $post = get_post( $this->menu_item_id ); + $menu_item = wp_setup_nav_menu_item( $post ); + $this->assertSame( $params['title'], $menu_item->title ); + $this->assertSame( $params['description'], $menu_item->description ); + $this->assertSame( $good_data, explode( ' ', $menu_item->xfn ) ); + } + + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_invalid() { + wp_set_current_user( self::$admin_id ); + $post_id = self::factory()->post->create(); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $post_id ) ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $params = $this->set_menu_item_data(); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @ticket 40878 + * @covers ::delete_item + */ + public function test_delete_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->assertNull( get_post( $this->menu_item_id ) ); + } + + /** + * @ticket 40878 + * @covers ::delete_item + */ + public function test_delete_item_no_force() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->set_param( 'force', false ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 501, $response->get_status() ); + $this->assertNotNull( get_post( $this->menu_item_id ) ); + } + + /** + * @ticket 40878 + * @covers ::delete_item + */ + public function test_delete_item_invalid() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/menu-items/9999' ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_id', $response, 404 ); + } + + /** + * @ticket 40878 + * @covers ::prepare_item_for_response + */ + public function test_prepare_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $this->check_get_menu_item_response( $response ); + } + + /** + * @ticket 40878 + * @covers ::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 18, $properties ); + $this->assertArrayHasKey( 'type_label', $properties ); + $this->assertArrayHasKey( 'attr_title', $properties ); + $this->assertArrayHasKey( 'classes', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'url', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); + $this->assertArrayHasKey( 'menu_order', $properties ); + $this->assertArrayHasKey( 'object', $properties ); + $this->assertArrayHasKey( 'object_id', $properties ); + $this->assertArrayHasKey( 'target', $properties ); + $this->assertArrayHasKey( 'parent', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'xfn', $properties ); + $this->assertArrayHasKey( 'invalid', $properties ); + } + + /** + * @ticket 40878 + * @covers ::get_items_permissions_check + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @ticket 40878 + * @covers ::get_item_permissions_check + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @ticket 40878 + * @covers ::get_items_permissions_check + */ + public function test_get_items_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @ticket 40878 + * @covers ::get_item_permissions_check + */ + public function test_get_item_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-items/' . $this->menu_item_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @param WP_REST_Response $response Response Class. + * @param string $context Defaults to View. + */ + protected function check_get_menu_items_response( $response, $context = 'view' ) { + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + $this->assertSame( 200, $response->get_status() ); + + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'X-WP-Total', $headers ); + $this->assertArrayHasKey( 'X-WP-TotalPages', $headers ); + + $all_data = $response->get_data(); + foreach ( $all_data as $data ) { + $post = get_post( $data['id'] ); + // Base fields for every post. + $menu_item = wp_setup_nav_menu_item( $post ); + /** + * As the links for the post are "response_links" format in the data array we have to pull them out and parse them. + */ + $links = $data['_links']; + foreach ( $links as &$links_array ) { + foreach ( $links_array as &$link ) { + $attributes = array_diff_key( + $link, + array( + 'href' => 1, + 'name' => 1, + ) + ); + $link = array_diff_key( $link, $attributes ); + $link['attributes'] = $attributes; + } + } + + $this->check_menu_item_data( $menu_item, $data, $context, $links ); + } + } + + /** + * @param WP_Post $post WP_Post object. + * @param array $data Data compare. + * @param string $context Context of REST Request. + * @param array $links Array links. + */ + protected function check_menu_item_data( $post, $data, $context, $links ) { + $post_type_obj = get_post_type_object( self::POST_TYPE ); + + // Standard fields. + $this->assertSame( $post->ID, $data['id'] ); + $this->assertSame( wpautop( $post->post_content ), $data['description'] ); + + // Check filtered values. + if ( post_type_supports( self::POST_TYPE, 'title' ) ) { + add_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + add_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); + $this->assertSame( $post->title, $data['title']['rendered'] ); + remove_filter( 'protected_title_format', array( $this, 'protected_title_format' ) ); + remove_filter( 'private_title_format', array( $this, 'protected_title_format' ) ); + if ( 'edit' === $context ) { + $this->assertSame( $post->title, $data['title']['raw'] ); + } else { + $this->assertFalse( isset( $data['title']['raw'] ) ); + } + } else { + $this->assertFalse( isset( $data['title'] ) ); + } + + // post_parent. + $this->assertArrayHasKey( 'parent', $data ); + if ( $post->post_parent ) { + if ( is_int( $data['parent'] ) ) { + $this->assertSame( $post->post_parent, $data['parent'] ); + } else { + $this->assertSame( $post->post_parent, $data['parent']['id'] ); + $menu_item = wp_setup_nav_menu_item( get_post( $data['parent']['id'] ) ); + $this->check_get_menu_item_response( $data['parent'], $menu_item, 'view-parent' ); + } + } else { + $this->assertEmpty( $data['parent'] ); + } + + $this->assertFalse( $data['invalid'] ); + + // page attributes. + $this->assertSame( $post->menu_order, $data['menu_order'] ); + + $taxonomies = wp_list_filter( get_object_taxonomies( self::POST_TYPE, 'objects' ), array( 'show_in_rest' => true ) ); + foreach ( $taxonomies as $taxonomy ) { + $this->assertTrue( isset( $data[ $taxonomy->rest_base ] ) ); + $terms = wp_get_object_terms( $post->ID, $taxonomy->name, array( 'fields' => 'ids' ) ); + sort( $terms ); + if ( 'nav_menu' === $taxonomy->name ) { + $term_id = $terms ? array_shift( $terms ) : 0; + $this->assertSame( $term_id, $data[ $taxonomy->rest_base ] ); + } else { + sort( $data[ $taxonomy->rest_base ] ); + $this->assertSame( $terms, $data[ $taxonomy->rest_base ] ); + } + } + + // test links. + if ( $links ) { + $links = test_rest_expand_compact_links( $links ); + $this->assertSame( $links['self'][0]['href'], rest_url( 'wp/v2/' . $post_type_obj->rest_base . '/' . $data['id'] ) ); + $this->assertSame( $links['collection'][0]['href'], rest_url( 'wp/v2/' . $post_type_obj->rest_base ) ); + $this->assertSame( $links['about'][0]['href'], rest_url( 'wp/v2/types/' . self::POST_TYPE ) ); + + $num = 0; + foreach ( $taxonomies as $taxonomy ) { + $this->assertSame( $taxonomy->name, $links['https://api.w.org/term'][ $num ]['attributes']['taxonomy'] ); + $this->assertSame( add_query_arg( 'post', $data['id'], rest_url( 'wp/v2/' . $taxonomy->rest_base ) ), $links['https://api.w.org/term'][ $num ]['href'] ); + ++$num; + } + + if ( 'post_type' === $data['type'] ) { + $this->assertArrayHasKey( 'https://api.w.org/menu-item-object', $links ); + $this->assertArrayHasKey( $data['type'], $links['https://api.w.org/menu-item-object'][0]['attributes'] ); + $this->assertSame( $links['https://api.w.org/menu-item-object'][0]['href'], rest_url( rest_get_route_for_post( $data['object_id'] ) ) ); + } + + if ( 'taxonomy' === $data['type'] ) { + $this->assertArrayHasKey( 'https://api.w.org/menu-item-object', $links ); + $this->assertArrayHasKey( $data['type'], $links['https://api.w.org/menu-item-object'][0]['attributes'] ); + $this->assertSame( $links['https://api.w.org/menu-item-object'][0]['href'], rest_url( rest_get_route_for_term( $data['object_id'] ) ) ); + } + } + } + + /** + * @param WP_REST_Response $response Response Class. + * @param string $context Defaults to View. + */ + protected function check_get_menu_item_response( $response, $context = 'view' ) { + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $post = get_post( $data['id'] ); + $menu_item = wp_setup_nav_menu_item( $post ); + $this->check_menu_item_data( $menu_item, $data, $context, $response->get_links() ); + } + + /** + * @param WP_REST_Response $response Response Class. + */ + protected function check_create_menu_item_response( $response ) { + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 201, $response->get_status() ); + $headers = $response->get_headers(); + $this->assertArrayHasKey( 'Location', $headers ); + + $data = $response->get_data(); + $post = get_post( $data['id'] ); + $menu_item = wp_setup_nav_menu_item( $post ); + $this->check_menu_item_data( $menu_item, $data, 'edit', $response->get_links() ); + } + + /** + * @param WP_REST_Response $response Response Class. + */ + protected function check_update_menu_item_response( $response ) { + $this->assertNotWPError( $response ); + $response = rest_ensure_response( $response ); + + $this->assertSame( 200, $response->get_status() ); + $headers = $response->get_headers(); + $this->assertArrayNotHasKey( 'Location', $headers ); + + $data = $response->get_data(); + $post = get_post( $data['id'] ); + $menu_item = wp_setup_nav_menu_item( $post ); + $this->check_menu_item_data( $menu_item, $data, 'edit', $response->get_links() ); + } + + /** + * @param array $args Override params. + * + * @return mixed + */ + protected function set_menu_item_data( $args = array() ) { + $defaults = array( + 'object_id' => 0, + 'parent' => 0, + 'menu_order' => 1, + 'menus' => $this->menu_id, + 'type' => 'custom', + 'title' => 'Custom Link Title', + 'url' => '#', + 'description' => '', + 'attr-title' => '', + 'target' => '', + 'type_label' => 'Custom Link', + 'classes' => '', + 'xfn' => '', + 'status' => 'draft', + ); + + return wp_parse_args( $args, $defaults ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_properly_handles_slashed_data() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menu-items' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $parameters = $this->set_menu_item_data( + array( + 'title' => 'Some \\\'title', + ) + ); + $request->set_body_params( $parameters ); + $response = rest_get_server()->dispatch( $request ); + $this->assertNotWPError( $response->as_error() ); + $data = $response->get_data(); + $post = get_post( $data['id'] ); + $this->assertSame( $parameters['title'], $post->post_title ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_properly_handles_slashed_data() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'PUT', sprintf( '/wp/v2/menu-items/%d', $this->menu_item_id ) ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + $title = 'Some \\\'title'; + $params = $this->set_menu_item_data( + array( + 'title' => $title, + ) + ); + $request->set_body_params( $params ); + $response = rest_get_server()->dispatch( $request ); + $new_data = $response->get_data(); + $this->assertSame( $params['title'], $new_data['title']['raw'] ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php b/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php new file mode 100644 index 0000000000000..1a0ffc4bb536b --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestMenuLocationsController.php @@ -0,0 +1,258 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + } + + /** + * Set up. + */ + public function set_up() { + parent::set_up(); + + // Unregister all nav menu locations. + foreach ( array_keys( get_registered_nav_menus() ) as $location ) { + unregister_nav_menu( $location ); + } + } + + /** + * Register nav menu locations. + * + * @param array $locations Location slugs. + */ + public function register_nav_menu_locations( $locations ) { + foreach ( $locations as $location ) { + register_nav_menu( $location, ucfirst( $location ) ); + } + } + + /** + * @ticket 40878 + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/menu-locations', $routes ); + $this->assertCount( 1, $routes['/wp/v2/menu-locations'] ); + $this->assertArrayHasKey( '/wp/v2/menu-locations/(?P[\w-]+)', $routes ); + $this->assertCount( 1, $routes['/wp/v2/menu-locations/(?P[\w-]+)'] ); + } + + /** + * @ticket 40878 + * @covers ::get_context_param + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $menu = 'primary'; + $this->register_nav_menu_locations( array( $menu ) ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations/' . $menu ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @ticket 40878 + * @covers ::get_items + */ + public function test_get_items() { + $menus = array( 'primary', 'secondary' ); + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = array_values( $data ); + $this->assertCount( 2, $data ); + $names = wp_list_pluck( $data, 'name' ); + $descriptions = wp_list_pluck( $data, 'description' ); + $this->assertSame( $menus, $names ); + $menu_descriptions = array_map( 'ucfirst', $names ); + $this->assertSame( $menu_descriptions, $descriptions ); + } + + /** + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item() { + $menu = 'primary'; + $this->register_nav_menu_locations( array( $menu ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( $menu, $data['name'] ); + } + + /** + * @ticket 54304 + * @covers ::get_items + */ + public function test_get_items_filter() { + $menus = array( 'primary', 'secondary' ); + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + add_filter( 'rest_menu_read_access', '__return_true' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $data = array_values( $data ); + $this->assertCount( 2, $data, 'Number of menu location are not 2' ); + + $names = wp_list_pluck( $data, 'name' ); + $descriptions = wp_list_pluck( $data, 'description' ); + $this->assertSame( $menus, $names ); + $menu_descriptions = array_map( 'ucfirst', $names ); + + $this->assertSame( $menu_descriptions, $descriptions, 'Menu descriptions do not match' ); + } + + /** + * @ticket 54304 + * @covers ::get_item + */ + public function test_get_item_filter() { + $menu = 'primary'; + $this->register_nav_menu_locations( array( $menu ) ); + + add_filter( 'rest_menu_read_access', '__return_true' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( $menu, $data['name'] ); + } + + /** + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item_invalid() { + $menu = 'primary'; + $this->register_nav_menu_locations( array( $menu ) ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/invalid' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_menu_location_invalid', $response, 404 ); + } + + /** + * The create_item() method does not exist for menu locations. + * + * @doesNotPerformAssertions + */ + public function test_create_item() { + // Controller does not implement create_item(). + } + + /** + * The update_item() method does not exist for menu locations. + * + * @doesNotPerformAssertions + */ + public function test_update_item() { + // Controller does not implement update_item(). + } + + /** + * The delete_item() method does not exist for menu locations. + * + * @doesNotPerformAssertions + */ + public function test_delete_item() { + // Controller does not implement delete_item(). + } + + /** + * The prepare_item() method does not exist for menu locations. + * + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + /** + * @ticket 40878 + * @covers ::get_item_schema + */ + public function test_get_item_schema() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menu-locations' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 3, $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'menu', $properties ); + } + + + /** + * @ticket 40878 + * @covers ::get_items + * @covers ::get_items_permissions_check + */ + public function test_get_items_menu_location_context_without_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_view', $response, rest_authorization_required_code() ); + } + + /** + * @ticket 40878 + * @covers ::get_item + * @covers ::get_item_permissions_check + */ + public function test_get_item_menu_location_context_without_permission() { + $menu = 'primary'; + $this->register_nav_menu_locations( array( $menu ) ); + + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menu-locations/' . $menu ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'rest_cannot_view', $response, rest_authorization_required_code() ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestMenusController.php b/tests/phpunit/tests/rest-api/wpRestMenusController.php new file mode 100644 index 0000000000000..864b09417d2cb --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestMenusController.php @@ -0,0 +1,680 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + } + + /** + * + */ + public function set_up() { + parent::set_up(); + // Unregister all nav menu locations. + foreach ( array_keys( get_registered_nav_menus() ) as $location ) { + unregister_nav_menu( $location ); + } + + $orig_args = array( + 'name' => 'Original Name', + 'description' => 'Original Description', + 'slug' => 'original-slug', + 'taxonomy' => 'nav_menu', + ); + + $this->menu_id = self::factory()->term->create( $orig_args ); + + register_meta( + 'term', + 'test_single_menu', + array( + 'object_subtype' => self::TAXONOMY, + 'show_in_rest' => true, + 'single' => true, + 'type' => 'string', + ) + ); + } + + /** + * Register nav menu locations. + * + * @param array $locations Location slugs. + */ + public function register_nav_menu_locations( $locations ) { + foreach ( $locations as $location ) { + register_nav_menu( $location, ucfirst( $location ) ); + } + } + + /** + * @ticket 40878 + * @covers ::register_routes + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( '/wp/v2/menus', $routes ); + $this->assertArrayHasKey( '/wp/v2/menus/(?P[\d]+)', $routes ); + } + + /** + * @ticket 40878 + * @covers ::get_context_param + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + // Single. + $tag1 = self::factory()->tag->create( array( 'name' => 'Season 5' ) ); + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus/' . $tag1 ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSameSets( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + $this->assertSame( array( 'v1' => true ), $data['endpoints'][0]['allow_batch'] ); + } + + /** + * @ticket 40878 + * @covers ::get_collection_params + */ + public function test_registered_query_params() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $keys = array_keys( $data['endpoints'][0]['args'] ); + sort( $keys ); + $this->assertSame( + array( + 'context', + 'exclude', + 'hide_empty', + 'include', + 'offset', + 'order', + 'orderby', + 'page', + 'per_page', + 'post', + 'search', + 'slug', + ), + $keys + ); + } + + /** + * @ticket 40878 + * @covers ::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + wp_update_nav_menu_object( + 0, + array( + 'description' => 'Test get', + 'menu-name' => 'test Name get', + ) + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus' ); + $request->set_param( 'per_page', self::$per_page ); + $response = rest_get_server()->dispatch( $request ); + $this->check_get_taxonomy_terms_response( $response ); + } + + /** + * @ticket 40878 + * @covers ::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $nav_menu_id = wp_update_nav_menu_object( + 0, + array( + 'description' => 'Test menu', + 'menu-name' => 'test Name', + ) + ); + + $this->register_nav_menu_locations( array( 'primary' ) ); + set_theme_mod( 'nav_menu_locations', array( 'primary' => $nav_menu_id ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $nav_menu_id ); + $response = rest_get_server()->dispatch( $request ); + $this->check_get_taxonomy_term_response( $response, $nav_menu_id ); + } + + + /** + * @ticket 54304 + * @covers ::get_items + */ + public function test_get_items_filter() { + add_filter( 'rest_menu_read_access', '__return_true' ); + wp_update_nav_menu_object( + 0, + array( + 'description' => 'Test get', + 'menu-name' => 'test Name get', + ) + ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus' ); + $request->set_param( 'per_page', self::$per_page ); + $response = rest_get_server()->dispatch( $request ); + $this->check_get_taxonomy_terms_response( $response ); + } + + /** + * @ticket 54304 + * @covers ::get_item + */ + public function test_get_item_filter() { + add_filter( 'rest_menu_read_access', '__return_true' ); + $nav_menu_id = wp_update_nav_menu_object( + 0, + array( + 'description' => 'Test menu', + 'menu-name' => 'test Name', + ) + ); + + $this->register_nav_menu_locations( array( 'primary' ) ); + set_theme_mod( 'nav_menu_locations', array( 'primary' => $nav_menu_id ) ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $nav_menu_id ); + $response = rest_get_server()->dispatch( $request ); + $this->check_get_taxonomy_term_response( $response, $nav_menu_id ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome menus' ); + $request->set_param( 'description', 'This menu is so awesome.' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + $headers = $response->get_headers(); + $data = $response->get_data(); + $this->assertStringContainsString( '/wp/v2/menus/' . $data['id'], $headers['Location'] ); + $this->assertSame( 'My Awesome menus', $data['name'] ); + $this->assertSame( 'This menu is so awesome.', $data['description'] ); + $this->assertSame( 'my-awesome-menus', $data['slug'] ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_same_name() { + wp_set_current_user( self::$admin_id ); + + wp_update_nav_menu_object( + 0, + array( + 'description' => 'This menu is so Original', + 'menu-name' => 'Original', + ) + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'Original' ); + $request->set_param( 'description', 'This menu is so Original' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertErrorResponse( 'menu_exists', $response, 400 ); + } + + /** + * @ticket 40878 + * @covers ::update_item + * @covers ::handle_auto_add + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'auto_add', true ); + $request->set_param( + 'meta', + array( + 'test_single_menu' => 'just meta', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertSame( 'New Name', $data['name'] ); + $this->assertSame( 'New Description', $data['description'] ); + $this->assertSame( true, $data['auto_add'] ); + $this->assertSame( 'new-name', $data['slug'] ); + $this->assertSame( 'just meta', $data['meta']['test_single_menu'] ); + $this->assertFalse( isset( $data['meta']['test_cat_meta'] ) ); + } + + /** + * @ticket 40878 + * @covers ::delete_item + */ + public function test_delete_item() { + wp_set_current_user( self::$admin_id ); + + $nav_menu_id = wp_update_nav_menu_object( + 0, + array( + 'description' => 'Deleted Menu', + 'menu-name' => 'Deleted Menu', + ) + ); + + $term = get_term_by( 'id', $nav_menu_id, self::TAXONOMY ); + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/menus/' . $term->term_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertTrue( $data['deleted'] ); + $this->assertSame( 'Deleted Menu', $data['previous']['name'] ); + } + + /** + * @ticket 40878 + * @covers ::prepare_item_for_response + * @covers ::get_item + */ + public function test_prepare_item() { + $nav_menu_id = wp_update_nav_menu_object( + 0, + array( + 'description' => 'Foo Menu', + 'menu-name' => 'Foo Menu', + ) + ); + + $term = get_term_by( 'id', $nav_menu_id, self::TAXONOMY ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $term->term_id ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->check_taxonomy_term( $term, $data, $response->get_links() ); + } + + /** + * @ticket 40878 + * @covers ::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/menus' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 7, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'meta', $properties ); + $this->assertArrayHasKey( 'name', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'locations', $properties ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_with_location_permission_correct() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome Term' ); + $request->set_param( 'slug', 'so-awesome' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 201, $response->get_status() ); + $data = $response->get_data(); + $term_id = $data['id']; + $locations = get_nav_menu_locations(); + $this->assertSame( $locations['primary'], $term_id ); + } + + /** + * @ticket 40878 + * @covers ::create_item + */ + public function test_create_item_with_invalid_location() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus' ); + $request->set_param( 'name', 'My Awesome Term' ); + $request->set_param( 'slug', 'so-awesome' ); + $request->set_param( 'locations', 'bar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 400, $response->get_status() ); + $this->assertErrorResponse( 'rest_invalid_param', $response, 400 ); + $this->assertArrayHasKey( 'locations', $response->get_data()['data']['details'] ); + $this->assertSame( 'rest_invalid_menu_location', $response->get_data()['data']['details']['locations']['code'] ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_with_no_location() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'bar' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 400, $response->get_status() ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_with_location_permission_correct() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $locations = get_nav_menu_locations(); + $this->assertSame( $locations['primary'], $this->menu_id ); + } + + /** + * @ticket 40878 + * @covers ::update_item + */ + public function test_update_item_with_location_permission_incorrect() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_param( 'name', 'New Name' ); + $request->set_param( 'description', 'New Description' ); + $request->set_param( 'slug', 'new-slug' ); + $request->set_param( 'locations', 'primary' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( rest_authorization_required_code(), $response->get_status() ); + } + + /** + * @ticket 40878 + * @covers ::prepare_links + */ + public function test_get_item_links() { + wp_set_current_user( self::$admin_id ); + + $nav_menu_id = wp_update_nav_menu_object( + 0, + array( + 'description' => 'Foo Menu', + 'menu-name' => 'Foo Menu', + ) + ); + + register_nav_menu( 'foo', 'Bar' ); + + set_theme_mod( 'nav_menu_locations', array( 'foo' => $nav_menu_id ) ); + + $request = new WP_REST_Request( 'GET', sprintf( '/wp/v2/menus/%d', $nav_menu_id ) ); + $response = rest_get_server()->dispatch( $request ); + + $links = $response->get_links(); + $this->assertArrayHasKey( 'https://api.w.org/menu-location', $links ); + + $location_url = rest_url( '/wp/v2/menu-locations/foo' ); + $this->assertSame( $location_url, $links['https://api.w.org/menu-location'][0]['href'] ); + } + + /** + * @ticket 40878 + * @covers ::update_item + * @covers ::handle_locations + */ + public function test_change_menu_location() { + $this->register_nav_menu_locations( array( 'primary', 'secondary' ) ); + $secondary_id = self::factory()->term->create( + array( + 'name' => 'Secondary Name', + 'description' => 'Secondary Description', + 'slug' => 'secondary-slug', + 'taxonomy' => 'nav_menu', + ) + ); + + $locations = get_nav_menu_locations(); + $locations['primary'] = $this->menu_id; + $locations['secondary'] = $secondary_id; + set_theme_mod( 'nav_menu_locations', $locations ); + + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/menus/' . $this->menu_id ); + $request->set_body_params( + array( + 'locations' => array( 'secondary' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status() ); + + $locations = get_nav_menu_locations(); + $this->assertArrayNotHasKey( 'primary', $locations ); + $this->assertArrayHasKey( 'secondary', $locations ); + $this->assertSame( $this->menu_id, $locations['secondary'] ); + } + + /** + * @ticket 40878 + * @covers ::get_items + * @covers ::get_items_permissions_check + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @ticket 40878 + * @covers ::get_items + * @covers ::get_items_permissions_check + */ + public function test_get_item_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $this->menu_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 401 ); + } + + /** + * @ticket 40878 + * @covers ::get_items + * @covers ::get_items_permissions_check + */ + public function test_get_items_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @ticket 40878 + * @covers ::get_item + * @covers ::get_item_permissions_check + */ + public function test_get_item_wrong_permission() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/menus/' . $this->menu_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_view', $response, 403 ); + } + + /** + * @ticket 40878 + */ + public function test_it_allows_batch_requests_when_updating_menus() { + $rest_server = rest_get_server(); + // This call is needed to initialize route_options. + $rest_server->get_routes(); + $route_options = $rest_server->get_route_options( '/wp/v2/menus/(?P[\d]+)' ); + + $this->assertArrayHasKey( 'allow_batch', $route_options ); + $this->assertSame( array( 'v1' => true ), $route_options['allow_batch'] ); + } + + /** + * @param WP_REST_Response $response Response Class. + */ + protected function check_get_taxonomy_terms_response( $response ) { + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $args = array( + 'hide_empty' => false, + ); + $tags = get_terms( self::TAXONOMY, $args ); + $this->assertCount( count( $tags ), $data ); + $this->assertSame( $tags[0]->term_id, $data[0]['id'] ); + $this->assertSame( $tags[0]->name, $data[0]['name'] ); + $this->assertSame( $tags[0]->slug, $data[0]['slug'] ); + $this->assertSame( $tags[0]->description, $data[0]['description'] ); + } + + /** + * @param WP_REST_Response $response Response Class. + * @param int $id Term ID. + */ + protected function check_get_taxonomy_term_response( $response, $id ) { + $this->assertSame( 200, $response->get_status() ); + + $data = $response->get_data(); + $menu = get_term( $id, self::TAXONOMY ); + $this->check_taxonomy_term( $menu, $data, $response->get_links() ); + } + + /** + * @param WP_Term $term WP_Term object. + * @param array $data Data from REST API. + * @param array $links Array of links. + */ + protected function check_taxonomy_term( $term, $data, $links ) { + $this->assertSame( $term->term_id, $data['id'] ); + $this->assertSame( $term->name, $data['name'] ); + $this->assertSame( $term->slug, $data['slug'] ); + $this->assertSame( $term->description, $data['description'] ); + $this->assertFalse( isset( $data['parent'] ) ); + + $locations = get_nav_menu_locations(); + if ( ! empty( $locations ) ) { + $menu_locations = array(); + foreach ( $locations as $location => $menu_id ) { + if ( $menu_id === $term->term_id ) { + $menu_locations[] = $location; + } + } + + $this->assertSame( $menu_locations, $data['locations'] ); + } + + $relations = array( + 'self', + 'collection', + 'about', + 'https://api.w.org/post_type', + ); + + if ( ! empty( $data['parent'] ) ) { + $relations[] = 'up'; + } + + if ( ! empty( $data['locations'] ) ) { + $relations[] = 'https://api.w.org/menu-location'; + } + + $this->assertSameSets( $relations, array_keys( $links ) ); + $this->assertStringContainsString( 'wp/v2/taxonomies/' . $term->taxonomy, $links['about'][0]['href'] ); + $this->assertSame( add_query_arg( 'menus', $term->term_id, rest_url( 'wp/v2/menu-items' ) ), $links['https://api.w.org/post_type'][0]['href'] ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php new file mode 100644 index 0000000000000..d3cbf91260488 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestTemplateAutosavesController.php @@ -0,0 +1,852 @@ +user->create( + array( + 'role' => 'contributor', + ) + ); + + self::$admin_id = $factory->user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( self::$admin_id ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_POST_TYPE, + 'post_name' => self::TEMPLATE_NAME, + 'post_title' => 'My template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_PART_POST_TYPE, + 'post_name' => self::TEMPLATE_PART_NAME, + 'post_title' => 'My template part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + } + + /** + * @covers WP_REST_Template_Autosaves_Controller::register_routes + * @ticket 56922 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + $routes, + 'Template autosaves route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + $routes, + 'Single template autosave based on the given ID route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves', + $routes, + 'Template part autosaves route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/autosaves/(?P[\d]+)', + $routes, + 'Single template part autosave based on the given ID route does not exist.' + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_context_param() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_context_param_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_context_param_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_context_param + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_context_param_with_data_provider( $rest_base, $template_id ) { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + // Collection. + $this->assertCount( + 2, + $data['endpoints'], + 'Failed to assert that the collection autosave endpoints count is 2.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the GET collection endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + "Failed to assert that the enum values for the GET collection endpoint are 'view', 'embed', and 'edit'." + ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves/1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( + 1, + $data['endpoints'], + 'Failed to assert that the single autosave endpoints count is 1.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the single autosave endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + "Failed to assert that the enum values for the single autosave endpoint are 'view', 'embed', and 'edit'." + ); + } + + /** + * Data provider for test_context_param_with_data_provider. + * + * @return array + */ + public function data_context_param_with_data_provider() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_items() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_get_items_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_items_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_items + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + // Cannot access this property in the data provider because it is not initialized at the time of execution. + $parent_post = self::$$parent_post_property_name; + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => $parent_post->ID, + 'post_type' => $parent_post->post_type, + ) + ); + + $request = new WP_REST_Request( + 'GET', + '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' + ); + $response = rest_get_server()->dispatch( $request ); + $autosaves = $response->get_data(); + $this->assertSame( WP_Http::OK, $response->get_status(), 'Response is expected to have a status code of 200.' ); + + $this->assertCount( + 1, + $autosaves, + 'Failed asserting that the response data contains exactly 1 item.' + ); + + $this->assertSame( + $autosave_post_id, + $autosaves[0]['wp_id'], + 'Failed asserting that the ID of the autosave matches the expected autosave post ID.' + ); + $this->assertSame( + $parent_post->ID, + $autosaves[0]['parent'], + 'Failed asserting that the parent ID of the autosave matches the template post ID.' + ); + $this->assertSame( + 'Autosave content.', + $autosaves[0]['content']['raw'], + 'Failed asserting that the content of the autosave is "Autosave content.".' + ); + } + + /** + * @ticket 56481 + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves' + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider for test_get_items_with_data_provider. + * + * @return array + */ + public function data_get_items_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @dataProvider data_get_items_for_templates_based_on_theme_files_should_return_bad_response_status + * @ticket 61970 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_for_templates_based_on_theme_files_should_return_bad_response_status( $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( + 'rest_invalid_template', + $response, + WP_Http::BAD_REQUEST, + sprintf( 'Response is expected to have a status code of %d.', WP_Http::BAD_REQUEST ) + ); + } + + /** + * Data provider for test_get_items_for_templates_based_on_theme_files_should_return_bad_response_status. + * + * @return array + */ + public function data_get_items_for_templates_based_on_theme_files_should_return_bad_response_status() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//page-home' ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//small-header' ), + ); + } + + /** + * @dataProvider data_get_item_for_templates_based_on_theme_files_should_return_bad_response_status + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_for_templates_based_on_theme_files_should_return_bad_response_status( $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves/1' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( + 'rest_invalid_template', + $response, + WP_Http::BAD_REQUEST, + sprintf( 'Response is expected to have a status code of %d.', WP_Http::BAD_REQUEST ) + ); + } + + /** + * Data provider for test_get_item_for_templates_based_on_theme_files_should_return_bad_response_status. + * + * @return array + */ + public function data_get_item_for_templates_based_on_theme_files_should_return_bad_response_status() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//page-home' ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//small-header' ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_get_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_item_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => $parent_post->ID, + 'post_type' => $parent_post->post_type, + ) + ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves/' . $autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( WP_Http::OK, $response->get_status(), 'Response is expected to have a status code of 200.' ); + $autosave = $response->get_data(); + + $this->assertIsArray( $autosave, 'Failed asserting that the autosave is an array.' ); + $this->assertSame( + $autosave_post_id, + $autosave['wp_id'], + "Failed asserting that the autosave id is the same as $autosave_post_id." + ); + $this->assertSame( + $parent_post->ID, + $autosave['parent'], + sprintf( + 'Failed asserting that the parent id of the autosave is the same as %s.', + $parent_post->ID + ) + ); + } + + /** + * @ticket 56481 + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => self::$template_post->ID, + 'post_type' => self::PARENT_POST_TYPE, + ) + ); + + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/autosaves/' . $autosave_post_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider for test_get_item_with_data_provider. + * + * @return array + */ + public function data_get_item_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @dataProvider data_get_item_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_head_request_with_specified_fields_returns_success_response( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => $parent_post->ID, + 'post_type' => $parent_post->post_type, + ) + ); + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves/' . $autosave_post_id + ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * @dataProvider data_get_items_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_items + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_head_request_with_specified_fields_returns_success_response( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + // Cannot access this property in the data provider because it is not initialized at the time of execution. + $parent_post = self::$$parent_post_property_name; + wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => $parent_post->ID, + 'post_type' => $parent_post->post_type, + ) + ); + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' + ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_prepare_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_prepare_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_prepare_item_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::prepare_item_for_response + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_prepare_item_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + $parent_post = self::$$parent_post_property_name; + $autosave_post_id = wp_create_post_autosave( + array( + 'post_content' => 'Autosave content.', + 'post_ID' => $parent_post->ID, + 'post_type' => $parent_post->post_type, + ) + ); + $autosave_db_post = get_post( $autosave_post_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves/' . $autosave_db_post->ID ); + $controller = new WP_REST_Template_Autosaves_Controller( $parent_post->post_type ); + $response = $controller->prepare_item_for_response( $autosave_db_post, $request ); + $this->assertInstanceOf( + WP_REST_Response::class, + $response, + 'Failed asserting that the response object is an instance of WP_REST_Response.' + ); + + $autosave = $response->get_data(); + $this->assertIsArray( $autosave, 'Failed asserting that the autosave is an array.' ); + $this->assertSame( + $autosave_db_post->ID, + $autosave['wp_id'], + "Failed asserting that the autosave id is the same as $autosave_db_post->ID." + ); + $this->assertSame( + $parent_post->ID, + $autosave['parent'], + sprintf( + 'Failed asserting that the parent id of the autosave is the same as %s.', + $parent_post->ID + ) + ); + + $links = $response->get_links(); + $this->assertIsArray( $links, 'Failed asserting that the links are an array.' ); + + $this->assertStringEndsWith( + $template_id . '/autosaves/' . $autosave_db_post->ID, + $links['self'][0]['href'], + "Failed asserting that the self link ends with $template_id . '/autosaves/' . $autosave_db_post->ID." + ); + + $this->assertStringEndsWith( + $template_id, + $links['parent'][0]['href'], + "Failed asserting that the parent link ends with %$template_id." + ); + } + + /** + * Data provider for test_prepare_item_with_data_provider. + * + * @return array + */ + public function data_prepare_item_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_item_schema() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_prepare_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_item_schema_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::get_item_schema + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + * @param int $properties_count Number of properties to check for in the schema. + * @param array $additional_properties Additional properties to check for in the schema. + */ + public function test_get_item_schema_with_data_provider( $rest_base, $template_id, $properties_count, $additional_properties = array() ) { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $properties = $data['schema']['properties']; + + $this->assertCount( $properties_count, $properties ); + $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); + $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); + $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); + $this->assertArrayHasKey( 'source', $properties, 'Source key should exist in properties.' ); + $this->assertArrayHasKey( 'origin', $properties, 'Origin key should exist in properties.' ); + $this->assertArrayHasKey( 'content', $properties, 'Content key should exist in properties.' ); + $this->assertArrayHasKey( 'title', $properties, 'Title key should exist in properties.' ); + $this->assertArrayHasKey( 'description', $properties, 'description key should exist in properties.' ); + $this->assertArrayHasKey( 'status', $properties, 'status key should exist in properties.' ); + $this->assertArrayHasKey( 'wp_id', $properties, 'wp_id key should exist in properties.' ); + $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); + $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); + $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); + $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); + $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); + foreach ( $additional_properties as $additional_property ) { + $this->assertArrayHasKey( $additional_property, $properties, $additional_property . ' key should exist in properties.' ); + } + } + + /** + * Data provider for test_get_item_schema_with_data_provider. + * + * @return array + */ + public function data_get_item_schema_with_data_provider() { + return array( + 'templates' => array( + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + 19, + array( 'is_custom', 'plugin' ), + ), + 'template parts' => array( + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, + 18, + array( 'area' ), + ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_create_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_create_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_create_item_with_data_provider + * @covers WP_REST_Template_Autosaves_Controller::create_item + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_create_item_with_data_provider( $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + $request->add_header( 'Content-Type', 'application/x-www-form-urlencoded' ); + + $request_parameters = array( + 'title' => 'Post Title', + 'content' => 'Post content', + 'excerpt' => 'Post excerpt', + 'name' => 'test', + 'id' => $template_id, + ); + + $request->set_body_params( $request_parameters ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, 'The response from this request should not return a WP_Error object.' ); + $response = rest_ensure_response( $response ); + $data = $response->get_data(); + + $this->assertArrayHasKey( 'content', $data, 'Response should contain a key called content.' ); + $this->assertSame( $request_parameters['content'], $data['content']['raw'], 'Response data should match for field content.' ); + + $this->assertArrayHasKey( 'title', $data, 'Response should contain a key called title.' ); + $this->assertSame( $request_parameters['title'], $data['title']['raw'], 'Response data should match for field title.' ); + } + + /** + * Data provider for test_create_item_with_data_provider. + * + * @return array + */ + public function data_create_item_with_data_provider() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template part' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @dataProvider data_create_item_incorrect_permission + * @covers WP_REST_Template_Autosaves_Controller::create_item_permissions_check + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_create_item_incorrect_permission( $rest_base, $template_id ) { + wp_set_current_user( self::$contributor_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, WP_Http::FORBIDDEN ); + } + + /** + * Data provider for test_create_item_incorrect_permission. + * + * @return array + */ + public function data_create_item_incorrect_permission() { + return array( + 'template' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template part' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @dataProvider data_create_item_no_permission + * @covers WP_REST_Template_Autosaves_Controller::create_item_permissions_check + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_create_item_no_permission( $rest_base, $template_id ) { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'POST', '/wp/v2/' . $rest_base . '/' . $template_id . '/autosaves' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * Data provider for test_create_item_no_permission. + * + * @return array + */ + public function data_create_item_no_permission() { + return array( + 'template' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template part' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_update_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to update template autosaves.", + WP_REST_Template_Autosaves_Controller::class + ) + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_delete_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to delete template autosaves.", + WP_REST_Template_Autosaves_Controller::class + ) + ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php new file mode 100644 index 0000000000000..e8a18b275e7cd --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestTemplateRevisionsController.php @@ -0,0 +1,1223 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + wp_set_current_user( self::$admin_id ); + + self::$contributor_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + + // Set up template post. + self::$template_post = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_POST_TYPE, + 'post_name' => self::TEMPLATE_NAME, + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + wp_set_post_terms( self::$template_post->ID, self::TEST_THEME, 'wp_theme' ); + + // Update post to create new revisions. + foreach ( range( 2, 5 ) as $revision_index ) { + self::$template_revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_post->ID, + 'post_content' => 'Content revision #' . $revision_index, + ) + ); + } + + // Create a new template post to test the get_item method. + self::$template_post_2 = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_POST_TYPE, + 'post_name' => self::TEMPLATE_NAME_2, + 'post_title' => 'My Template 2', + 'post_content' => 'Content 2', + 'post_excerpt' => 'Description of my template 2', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + ), + ) + ); + wp_set_post_terms( self::$template_post_2->ID, self::TEST_THEME, 'wp_theme' ); + + // Set up template part post. + self::$template_part_post = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_PART_POST_TYPE, + 'post_name' => self::TEMPLATE_PART_NAME, + 'post_title' => 'My template part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + wp_set_post_terms( self::$template_part_post->ID, self::TEST_THEME, 'wp_theme' ); + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + + // Update post to create new revisions. + foreach ( range( 2, 5 ) as $revision_index ) { + self::$template_part_revisions[] = _wp_put_post_revision( + array( + 'ID' => self::$template_part_post->ID, + 'post_content' => 'Content revision #' . $revision_index, + ) + ); + } + + // Set up template part post. + self::$template_part_post_2 = $factory->post->create_and_get( + array( + 'post_type' => self::TEMPLATE_PART_POST_TYPE, + 'post_name' => self::TEMPLATE_PART_NAME_2, + 'post_title' => 'My template part 2', + 'post_content' => 'Content 2', + 'post_excerpt' => 'Description of my template part 2', + 'tax_input' => array( + 'wp_theme' => array( + self::TEST_THEME, + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ) + ); + wp_set_post_terms( self::$template_part_post_2->ID, self::TEST_THEME, 'wp_theme' ); + wp_set_post_terms( self::$template_part_post_2->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + } + + /** + * Remove revisions when tests are complete. + */ + public static function wpTearDownAfterClass() { + // Also deletes revisions. + foreach ( self::$template_revisions as $template_revision ) { + wp_delete_post( $template_revision, true ); + } + + foreach ( self::$template_part_revisions as $template_part_revision ) { + wp_delete_post( $template_part_revision, true ); + } + } + + /** + * @covers WP_REST_Template_Revisions_Controller::register_routes + * @ticket 56922 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + $routes, + 'Template revisions route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + $routes, + 'Single template revision based on the given ID route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions', + $routes, + 'Template part revisions route does not exist.' + ); + $this->assertArrayHasKey( + '/wp/v2/template-parts/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)/revisions/(?P[\d]+)', + $routes, + 'Single template part revision based on the given ID route does not exist.' + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_context_param() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_context_param_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_context_param_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_context_param + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_context_param_with_data_provider( $rest_base, $template_id ) { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the collection endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + 'Failed to assert correct enum values for the collection endpoint.' + ); + + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/1' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertCount( + 2, + $data['endpoints'], + 'Failed to assert that the single revision endpoint count is 2.' + ); + $this->assertSame( + 'view', + $data['endpoints'][0]['args']['context']['default'], + 'Failed to assert that the default context for the single revision endpoint is "view".' + ); + $this->assertSame( + array( 'view', 'embed', 'edit' ), + $data['endpoints'][0]['args']['context']['enum'], + 'Failed to assert correct enum values for the single revision endpoint.' + ); + } + + /** + * Data provider for test_context_param. + * + * @return array + */ + public function data_context_param_with_data_provider() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_items() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_get_items_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_items_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_items + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + $parent_post = self::$$parent_post_property_name; + + $request = new WP_REST_Request( + 'GET', + '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' + ); + $response = rest_get_server()->dispatch( $request ); + $revisions = $response->get_data(); + $this->assertSame( WP_Http::OK, $response->get_status(), 'Response is expected to have a status code of 200.' ); + + $this->assertCount( + 4, + $revisions, + 'Failed asserting that the response data contains exactly 4 items.' + ); + + $this->assertSame( + $parent_post->ID, + $revisions[0]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #5', + $revisions[0]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #5".' + ); + + $this->assertSame( + $parent_post->ID, + $revisions[1]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #4', + $revisions[1]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #4".' + ); + + $this->assertSame( + $parent_post->ID, + $revisions[2]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #3', + $revisions[2]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #3".' + ); + + $this->assertSame( + $parent_post->ID, + $revisions[3]['parent'], + 'Failed asserting that the parent ID of the revision matches the template post ID.' + ); + $this->assertSame( + 'Content revision #2', + $revisions[3]['content']['raw'], + 'Failed asserting that the content of the revision is "Content revision #2".' + ); + } + + /** + * Data provider for test_get_items_with_data_provider. + * + * @return array + */ + public function data_get_items_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + /** + * @ticket 56481 + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request + * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check + * @ticket 56922 + * @ticket 56481 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. + */ + public function test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request( $rest_base, $template_id, $method ) { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * Data provider for test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request. + * + * @return array + */ + public function data_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request() { + return array( + 'templates, GET request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'GET' ), + 'templates, HEAD request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'HEAD' ), + 'template parts, GET request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'GET' ), + 'template parts, HEAD request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'HEAD' ), + ); + } + + /** + * @dataProvider data_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions + * @covers WP_REST_Template_Revisions_Controller::get_items_permissions_check + * @ticket 56922 + * @ticket 56481 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. + */ + public function test_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions( $rest_base, string $template_id, $method ) { + wp_set_current_user( self::$contributor_id ); + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_read', $response, WP_Http::FORBIDDEN ); + } + + /** + * Data provider for test_get_items_endpoint_should_return_unauthorized_https_status_code_for_unauthorized_request. + * + * @return array + */ + public function data_get_items_endpoint_should_return_forbidden_https_status_code_for_users_with_insufficient_permissions() { + return array( + 'templates, GET request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'GET' ), + 'templates, HEAD request' => array( 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME, 'HEAD' ), + 'template parts, GET request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'GET' ), + 'template parts, HEAD request' => array( 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, 'HEAD' ), + ); + } + + /** + * @dataProvider data_get_items_for_templates_based_on_theme_files_should_return_bad_response_status + * @ticket 61970 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_for_templates_based_on_theme_files_should_return_bad_response_status( $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( + 'rest_invalid_template', + $response, + WP_Http::BAD_REQUEST, + sprintf( 'Response is expected to have a status code of %d.', WP_Http::BAD_REQUEST ) + ); + } + + /** + * Data provider for test_get_items_for_templates_based_on_theme_files_should_return_bad_response_status. + * + * @return array + */ + public function data_get_items_for_templates_based_on_theme_files_should_return_bad_response_status() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//page-home' ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//small-header' ), + ); + } + + /** + * @dataProvider data_get_item_for_templates_based_on_theme_files_should_return_bad_response_status + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_for_templates_based_on_theme_files_should_return_bad_response_status( $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/1' ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( + 'rest_invalid_template', + $response, + WP_Http::BAD_REQUEST, + sprintf( 'Response is expected to have a status code of %d.', WP_Http::BAD_REQUEST ) + ); + } + + /** + * Data provider for test_get_item_for_templates_based_on_theme_files_should_return_bad_response_status. + * + * @return array + */ + public function data_get_item_for_templates_based_on_theme_files_should_return_bad_response_status() { + return array( + 'templates' => array( 'templates', self::TEST_THEME . '//page-home' ), + 'template parts' => array( 'template-parts', self::TEST_THEME . '//small-header' ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_get_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_item_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + + $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $revision = $response->get_data(); + + $this->assertIsArray( $revision, 'Failed asserting that the revision is an array.' ); + $this->assertSame( + $revision_id, + $revision['wp_id'], + "Failed asserting that the revision id is the same as $revision_id" + ); + $this->assertSame( + $parent_post->ID, + $revision['parent'], + sprintf( + 'Failed asserting that the parent id of the revision is the same as %s.', + self::$template_post->ID + ) + ); + } + + /** + * @ticket 56481 + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $revisions = wp_get_post_revisions( self::$template_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * Data provider for test_get_item_with_data_provider. + * + * @return array + */ + public function data_get_item_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @dataProvider data_get_item_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_item_head_request_with_specified_fields_returns_success_response( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + + $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id + ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * @dataProvider data_get_items_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_items + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_get_items_head_request_with_specified_fields_returns_success_response( $parent_post_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + $parent_post = self::$$parent_post_property_name; + + $request = new WP_REST_Request( + 'HEAD', + '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' + ); + + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * @dataProvider data_get_item_not_found + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + * @ticket 56481 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $method HTTP method to use. + */ + public function test_get_item_not_found( $parent_post_property_name, $rest_base, $method ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + + $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/invalid//parent/revisions/' . $revision_id ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, WP_Http::NOT_FOUND ); + } + + /** + * Data provider for test_get_item_not_found. + * + * @return array + */ + public function data_get_item_not_found() { + return array( + 'templates, GET request' => array( 'template_post', 'templates', 'GET' ), + 'templates, HEAD request' => array( 'template_post', 'templates', 'HEAD' ), + 'template parts, GET request' => array( 'template_part_post', 'template-parts', 'GET' ), + 'template parts, HEAD request' => array( 'template_part_post', 'template-parts', 'HEAD' ), + ); + } + + /** + * @dataProvider data_get_item_invalid_parent_id + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 59875 + * @ticket 56481 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $actual_parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + * @param string $method HTTP method to use. + */ + public function test_get_item_invalid_parent_id( $parent_post_property_name, $actual_parent_post_property_name, $rest_base, $template_id, $method ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + $actual_parent_post = self::$$actual_parent_post_property_name; + $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + + $request = new WP_REST_Request( $method, '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_revision_parent_id_mismatch', $response, 404 ); + + $expected_message = 'The revision does not belong to the specified parent with id of "' . $actual_parent_post->ID . '"'; + $this->assertSame( $expected_message, $response->as_error()->get_error_messages()[0], 'The message must contain the correct parent ID.' ); + } + + /** + * Data provider for test_get_item_invalid_parent_id. + * + * @return array + */ + public function data_get_item_invalid_parent_id() { + return array( + 'templates, GET request' => array( + 'template_post', + 'template_post_2', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME_2, + 'GET', + ), + 'templates, HEAD request' => array( + 'template_post', + 'template_post_2', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME_2, + 'HEAD', + ), + 'template parts, GET request' => array( + 'template_part_post', + 'template_part_post_2', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME_2, + 'GET', + ), + 'template parts, HEAD request' => array( + 'template_part_post', + 'template_part_post_2', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME_2, + 'HEAD', + ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_prepare_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_prepare_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_prepare_item_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::prepare_item_for_response + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_prepare_item_with_data_provider( $parent_post_property_name, $rest_base, $template_id ) { + $parent_post = self::$$parent_post_property_name; + $revisions = wp_get_post_revisions( $parent_post, array( 'fields' => 'ids' ) ); + $revision_id = array_shift( $revisions ); + $post = get_post( $revision_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $controller = new WP_REST_Template_Revisions_Controller( $parent_post->post_type ); + $response = $controller->prepare_item_for_response( $post, $request ); + $this->assertInstanceOf( + WP_REST_Response::class, + $response, + 'Failed asserting that the response object is an instance of WP_REST_Response.' + ); + + $revision = $response->get_data(); + $this->assertIsArray( $revision, 'Failed asserting that the revision is an array.' ); + $this->assertSame( + $revision_id, + $revision['wp_id'], + "Failed asserting that the revision id is the same as $revision_id." + ); + $this->assertSame( + $parent_post->ID, + $revision['parent'], + sprintf( + 'Failed asserting that the parent id of the revision is the same as %s.', + $parent_post->ID + ) + ); + + $links = $response->get_links(); + $this->assertIsArray( $links, 'Failed asserting that the links are an array.' ); + + $this->assertStringEndsWith( + $template_id . '/revisions/' . $revision_id, + $links['self'][0]['href'], + sprintf( + 'Failed asserting that the self link ends with %s.', + $template_id . '/revisions/' . $revision_id + ) + ); + + $this->assertStringEndsWith( + $template_id, + $links['parent'][0]['href'], + sprintf( + 'Failed asserting that the parent link ends with %s.', + $template_id + ) + ); + } + + /** + * Data provider for test_prepare_item_with_data_provider. + * + * @return array + */ + public function data_prepare_item_with_data_provider() { + return array( + 'templates' => array( 'template_post', 'templates', self::TEST_THEME . '//' . self::TEMPLATE_NAME ), + 'template parts' => array( 'template_part_post', 'template-parts', self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_get_item_schema() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_prepare_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_get_item_schema_with_data_provider + * @covers WP_REST_Template_Revisions_Controller::get_item_schema + * @ticket 56922 + * + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + * @param int $properties_count Number of properties to check for in the schema. + * @param array $additional_properties Additional properties to check for in the schema. + */ + public function test_get_item_schema_with_data_provider( $rest_base, $template_id, $properties_count, $additional_properties = array() ) { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + + $this->assertCount( $properties_count, $properties ); + $this->assertArrayHasKey( 'id', $properties, 'ID key should exist in properties.' ); + $this->assertArrayHasKey( 'slug', $properties, 'Slug key should exist in properties.' ); + $this->assertArrayHasKey( 'theme', $properties, 'Theme key should exist in properties.' ); + $this->assertArrayHasKey( 'source', $properties, 'Source key should exist in properties.' ); + $this->assertArrayHasKey( 'origin', $properties, 'Origin key should exist in properties.' ); + $this->assertArrayHasKey( 'content', $properties, 'Content key should exist in properties.' ); + $this->assertArrayHasKey( 'title', $properties, 'Title key should exist in properties.' ); + $this->assertArrayHasKey( 'description', $properties, 'description key should exist in properties.' ); + $this->assertArrayHasKey( 'status', $properties, 'status key should exist in properties.' ); + $this->assertArrayHasKey( 'wp_id', $properties, 'wp_id key should exist in properties.' ); + $this->assertArrayHasKey( 'has_theme_file', $properties, 'has_theme_file key should exist in properties.' ); + $this->assertArrayHasKey( 'author', $properties, 'author key should exist in properties.' ); + $this->assertArrayHasKey( 'modified', $properties, 'modified key should exist in properties.' ); + $this->assertArrayHasKey( 'parent', $properties, 'Parent key should exist in properties.' ); + $this->assertArrayHasKey( 'author_text', $properties, 'author_text key should exist in properties.' ); + $this->assertArrayHasKey( 'original_source', $properties, 'original_source key should exist in properties.' ); + + foreach ( $additional_properties as $additional_property ) { + $this->assertArrayHasKey( $additional_property, $properties, $additional_property . ' key should exist in properties.' ); + } + } + + /** + * Data provider for data_get_item_schema_with_data_provider. + * + * @return array + */ + public function data_get_item_schema_with_data_provider() { + return array( + 'templates' => array( + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + 19, + array( 'is_custom', 'plugin' ), + ), + 'template parts' => array( + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, + 18, + array( 'area' ), + ), + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_create_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to create template revisions.", + WP_REST_Template_Revisions_Controller::class + ) + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_update_item() { + $this->markTestSkipped( + sprintf( + "The '%s' controller doesn't currently support the ability to update template revisions.", + WP_REST_Template_Revisions_Controller::class + ) + ); + } + + /** + * @coversNothing + * @ticket 56922 + */ + public function test_delete_item() { + // A proper data provider cannot be used because this method's signature must match the parent method. + // Therefore, actual tests are performed in the test_delete_item_with_data_provider method. + $this->assertTrue( true ); + } + + /** + * @dataProvider data_delete_item_with_data_provider + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $revisions_property_name A class property name that contains the revisions array. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_delete_item_with_data_provider( $parent_post_property_name, $revisions_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + $revisions = self::$$revisions_property_name; + + $revision_id = _wp_put_post_revision( $parent_post ); + $revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertSame( 200, $response->get_status(), 'Failed asserting that the response status is 200.' ); + $this->assertNull( get_post( $revision_id ), 'Failed asserting that the post with the given revision ID is deleted.' ); + } + + /** + * Data provider for test_delete_item_with_data_provider. + * + * @return array + */ + public function data_delete_item_with_data_provider() { + return array( + 'templates' => array( + 'template_post', + 'template_revisions', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + ), + 'template parts' => array( + 'template_part_post', + 'template_part_revisions', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, + ), + ); + } + + /** + * @dataProvider data_delete_item_incorrect_permission + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $revisions_property_name A class property name that contains the revisions array. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_delete_item_incorrect_permission( $parent_post_property_name, $revisions_property_name, $rest_base, $template_id ) { + wp_set_current_user( self::$contributor_id ); + $parent_post = self::$$parent_post_property_name; + $revisions = self::$$revisions_property_name; + + $revision_id = _wp_put_post_revision( $parent_post ); + $revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, WP_Http::FORBIDDEN ); + } + + /** + * Data provider for test_delete_item_with_data_provider. + * + * @return array + */ + public function data_delete_item_incorrect_permission() { + return array( + 'templates' => array( + 'template_post', + 'template_revisions', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + ), + 'template parts' => array( + 'template_part_post', + 'template_part_revisions', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, + ), + ); + } + + /** + * @dataProvider data_delete_item_no_permission + * @covers WP_REST_Templates_Controller::delete_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $revisions_property_name A class property name that contains the revisions array. + * @param string $rest_base Base part of the REST API endpoint to test. + * @param string $template_id Template ID to use in the test. + */ + public function test_delete_item_no_permission( $parent_post_property_name, $revisions_property_name, $rest_base, $template_id ) { + wp_set_current_user( 0 ); + + $parent_post = self::$$parent_post_property_name; + $revisions = self::$$revisions_property_name; + + $revision_id = _wp_put_post_revision( $parent_post ); + $revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/' . $rest_base . '/' . $template_id . '/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_delete', $response, WP_Http::UNAUTHORIZED ); + } + + /** + * Data provider for test_delete_item_no_permission. + * + * @return array + */ + public function data_delete_item_no_permission() { + return array( + 'templates' => array( + 'template_post', + 'template_revisions', + 'templates', + self::TEST_THEME . '//' . self::TEMPLATE_NAME, + ), + 'template parts' => array( + 'template_part_post', + 'template_part_revisions', + 'template-parts', + self::TEST_THEME . '//' . self::TEMPLATE_PART_NAME, + ), + ); + } + + /** + * @dataProvider data_delete_item_not_found + * @covers WP_REST_Template_Revisions_Controller::get_item + * @ticket 56922 + * + * @param string $parent_post_property_name A class property name that contains the parent post object. + * @param string $revisions_property_name A class property name that contains the revisions array. + * @param string $rest_base Base part of the REST API endpoint to test. + */ + public function test_delete_item_not_found( $parent_post_property_name, $revisions_property_name, $rest_base ) { + wp_set_current_user( self::$admin_id ); + + $parent_post = self::$$parent_post_property_name; + $revisions = self::$$revisions_property_name; + + $revision_id = _wp_put_post_revision( $parent_post ); + $revisions[] = $revision_id; + + $request = new WP_REST_Request( 'DELETE', '/wp/v2/' . $rest_base . '/invalid//parent/revisions/' . $revision_id ); + $request->set_param( 'force', true ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_post_invalid_parent', $response, WP_Http::NOT_FOUND ); + } + + /** + * Data provider for test_delete_item_not_found. + * + * @return array + */ + public function data_delete_item_not_found() { + return array( + 'templates' => array( + 'template_post', + 'template_revisions', + 'templates', + ), + 'template parts' => array( + 'template_part_post', + 'template_part_revisions', + 'template-parts', + ), + ); + } + + /** + * Tests for the pagination. + * + * @ticket 62292 + * + * @covers WP_REST_Template_Revisions_Controller::get_items + */ + public function test_get_template_revisions_pagination() { + wp_set_current_user( self::$admin_id ); + + // Test offset. + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $request->set_param( 'offset', 1 ); + $request->set_param( 'per_page', 1 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 1, $data ); + $this->assertSame( 4, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 4, $response->get_headers()['X-WP-TotalPages'] ); + + // Test paged. + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $request->set_param( 'page', 2 ); + $request->set_param( 'per_page', 2 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertCount( 2, $data ); + $this->assertSame( 4, $response->get_headers()['X-WP-Total'] ); + $this->assertSame( 2, $response->get_headers()['X-WP-TotalPages'] ); + + // Test out of bounds. + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/' . self::TEST_THEME . '/' . self::TEMPLATE_NAME . '/revisions' ); + $request->set_param( 'page', 4 ); + $request->set_param( 'per_page', 6 ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_revision_invalid_page_number', $response, 400 ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestTemplatesController.php b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php new file mode 100644 index 0000000000000..0bbd6b151c6c0 --- /dev/null +++ b/tests/phpunit/tests/rest-api/wpRestTemplatesController.php @@ -0,0 +1,1219 @@ +user->create( + array( + 'role' => 'administrator', + ) + ); + self::$editor_id = $factory->user->create( + array( + 'role' => 'editor', + ) + ); + self::$subscriber_id = $factory->user->create( + array( + 'role' => 'subscriber', + ) + ); + + // Set up template post. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'my_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template.', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + ), + ); + self::$template_post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( self::$template_post->ID, get_stylesheet(), 'wp_theme' ); + + // Set up template part post. + $args = array( + 'post_type' => 'wp_template_part', + 'post_name' => 'my_template_part', + 'post_title' => 'My Template Part', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template part.', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + 'wp_template_part_area' => array( + WP_TEMPLATE_PART_AREA_HEADER, + ), + ), + ); + self::$template_part_post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( self::$template_part_post->ID, get_stylesheet(), 'wp_theme' ); + wp_set_post_terms( self::$template_part_post->ID, WP_TEMPLATE_PART_AREA_HEADER, 'wp_template_part_area' ); + } + + public static function wpTearDownAfterClass() { + wp_delete_post( self::$template_post->ID ); + } + + /** + * Tear down after each test. + * + * @since 6.5.0 + */ + public function tear_down() { + if ( has_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ) ) { + remove_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ); + } + if ( WP_Block_Type_Registry::get_instance()->is_registered( 'tests/block' ) ) { + unregister_block_type( 'tests/hooked-block' ); + } + + parent::tear_down(); + } + + /** + * @covers WP_REST_Templates_Controller::register_routes + * @ticket 54596 + * @ticket 56467 + */ + public function test_register_routes() { + $routes = rest_get_server()->get_routes(); + $this->assertArrayHasKey( + '/wp/v2/templates', + $routes, + 'Templates route does not exist' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/(?P([^\/:<>\*\?"\|]+(?:\/[^\/:<>\*\?"\|]+)?)[\/\w%-]+)', + $routes, + 'Single template based on the given ID route does not exist' + ); + $this->assertArrayHasKey( + '/wp/v2/templates/lookup', + $routes, + 'Get template fallback content route does not exist' + ); + } + + /** + * @covers WP_REST_Templates_Controller::get_context_param + */ + public function test_context_param() { + // Collection. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + // Single. + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'view', $data['endpoints'][0]['args']['context']['default'] ); + $this->assertSame( array( 'view', 'embed', 'edit' ), $data['endpoints'][0]['args']['context']['enum'] ); + } + + /** + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( + array( + 'id' => 'default//my_template', + 'theme' => 'default', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$template_post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', + ), + $this->find_and_normalize_template_by_id( $data, 'default//my_template' ) + ); + } + + /** + * @ticket 56481 + * + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @dataProvider data_head_request_with_specified_fields_returns_success_response + * @ticket 56481 + * + * @param string $path The path to test. + */ + public function test_head_request_with_specified_fields_returns_success_response( $path ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', $path ); + $request->set_param( '_fields', 'id' ); + $server = rest_get_server(); + $response = $server->dispatch( $request ); + add_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10, 3 ); + $response = apply_filters( 'rest_post_dispatch', $response, $server, $request ); + remove_filter( 'rest_post_dispatch', 'rest_filter_response_fields', 10 ); + + $this->assertSame( 200, $response->get_status(), 'The response status should be 200.' ); + } + + /** + * Data provider intended to provide paths for testing HEAD requests. + * + * @return array + */ + public static function data_head_request_with_specified_fields_returns_success_response() { + return array( + 'get_item request' => array( '/wp/v2/templates/default//my_template' ), + 'get_items request' => array( '/wp/v2/templates' ), + ); + } + + /** + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items_editor() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + + $this->assertSame( + array( + 'id' => 'default//my_template', + 'theme' => 'default', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$template_post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', + ), + $this->find_and_normalize_template_by_id( $data, 'default//my_template' ) + ); + } + + /** + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items_no_permission_subscriber() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, 403 ); + } + + /** + * @covers WP_REST_Templates_Controller::get_items + */ + public function test_get_items_no_permission() { + wp_set_current_user( 0 ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, 401 ); + } + + /** + * @covers WP_REST_Templates_Controller::get_item + */ + public function test_get_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + + $this->assertSame( + array( + 'id' => 'default//my_template', + 'theme' => 'default', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$template_post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', + ), + $data + ); + } + + /** + * @ticket 56481 + * + * @covers WP_REST_Templates_Controller::get_item + * @covers WP_REST_Templates_Controller::prepare_item_for_response + */ + public function test_get_item_should_return_no_response_body_for_head_requests() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'HEAD', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status(), 'Response status is 200.' ); + $this->assertSame( array(), $response->get_data(), 'The server should not generate a body in response to a HEAD request.' ); + } + + /** + * @covers WP_REST_Templates_Controller::get_item + */ + public function test_get_item_editor() { + wp_set_current_user( self::$editor_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + + $this->assertSame( + array( + 'id' => 'default//my_template', + 'theme' => 'default', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$template_post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', + ), + $data + ); + } + + /** + * @covers WP_REST_Templates_Controller::get_item + */ + public function test_get_item_subscriber() { + wp_set_current_user( self::$subscriber_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_cannot_manage_templates', $response, 403 ); + } + + /** + * @ticket 54507 + * @dataProvider data_get_item_works_with_a_single_slash + */ + public function test_get_item_works_with_a_single_slash( $endpoint_url ) { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', $endpoint_url ); + $response = rest_get_server()->dispatch( $request ); + + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + + $this->assertSame( + array( + 'id' => 'default//my_template', + 'theme' => 'default', + 'slug' => 'my_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Description of my template.', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'wp_id' => self::$template_post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => 0, + 'modified' => mysql_to_rfc3339( self::$template_post->post_modified ), + 'author_text' => 'Test Blog', + 'original_source' => 'site', + ), + $data + ); + } + + public function data_get_item_works_with_a_single_slash() { + return array( + array( '/wp/v2/templates/default/my_template' ), + array( '/wp/v2/templates/default//my_template' ), + ); + } + + /** + * @dataProvider data_get_item_with_valid_theme_dirname + * @covers WP_REST_Templates_Controller::get_item + * @ticket 54596 + * + * @param string $theme_dir Theme directory to test. + * @param string $template Template to test. + * @param array $args Arguments to create the 'wp_template" post. + */ + public function test_get_item_with_valid_theme_dirname( $theme_dir, $template, array $args ) { + wp_set_current_user( self::$admin_id ); + switch_theme( $theme_dir ); + + // Set up template post. + $args['post_type'] = 'wp_template'; + $args['tax_input'] = array( + 'wp_theme' => array( + get_stylesheet(), + ), + ); + $post = self::factory()->post->create_and_get( $args ); + wp_set_post_terms( $post->ID, get_stylesheet(), 'wp_theme' ); + + $request = new WP_REST_Request( 'GET', "/wp/v2/templates/{$theme_dir}//{$template}" ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + unset( $data['content'] ); + unset( $data['_links'] ); + $author_name = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + + $this->assertSameSetsWithIndex( + array( + 'id' => "{$theme_dir}//{$template}", + 'theme' => $theme_dir, + 'slug' => $template, + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => $args['post_excerpt'], + 'title' => array( + 'raw' => $args['post_title'], + 'rendered' => $args['post_title'], + ), + 'status' => 'publish', + 'wp_id' => $post->ID, + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + 'modified' => mysql_to_rfc3339( $post->post_modified ), + 'author_text' => $author_name, + 'original_source' => 'user', + ), + $data + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_item_with_valid_theme_dirname() { + $theme_root_dir = DIR_TESTDATA . '/themedir1/'; + return array( + 'template parts: parent theme' => array( + 'theme_dir' => 'themedir1/block-theme', + 'template' => 'small-header', + 'args' => array( + 'post_name' => 'small-header', + 'post_title' => 'Small Header Template', + 'post_content' => file_get_contents( $theme_root_dir . '/block-theme/parts/small-header.html' ), + 'post_excerpt' => 'Description of small header template.', + ), + ), + 'template: parent theme' => array( + 'theme_dir' => 'themedir1/block-theme', + 'template' => 'page-home', + 'args' => array( + 'post_name' => 'page-home', + 'post_title' => 'Home Page Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme/templates/page-home.html' ), + 'post_excerpt' => 'Description of page home template.', + ), + ), + 'template parts: parent theme with non latin characters' => array( + 'theme_dir' => 'themedir1/block-theme-non-latin', + 'template' => 'small-header-%cf%84%ce%b5%cf%83%cf%84', + 'args' => array( + 'post_name' => 'small-header-τεστ', + 'post_title' => 'Small Header τεστ Template', + 'post_content' => file_get_contents( $theme_root_dir . '/block-theme-non-latin/parts/small-header-test.html' ), + 'post_excerpt' => 'Description of small header τεστ template.', + ), + ), + 'template: parent theme with non latin name' => array( + 'theme_dir' => 'themedir1/block-theme-non-latin', + 'template' => 'page-%cf%84%ce%b5%cf%83%cf%84', + 'args' => array( + 'post_name' => 'page-τεστ', + 'post_title' => 'τεστ Page Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme-non-latin/templates/page-test.html' ), + 'post_excerpt' => 'Description of page τεστ template.', + ), + ), + 'template parts: parent theme with chinese characters' => array( + 'theme_dir' => 'themedir1/block-theme-non-latin', + 'template' => 'small-header-%e6%b5%8b%e8%af%95', + 'args' => array( + 'post_name' => 'small-header-测试', + 'post_title' => 'Small Header 测试 Template', + 'post_content' => file_get_contents( $theme_root_dir . '/block-theme-non-latin/parts/small-header-test.html' ), + 'post_excerpt' => 'Description of small header 测试 template.', + ), + ), + 'template: parent theme with non latin name using chinese characters' => array( + 'theme_dir' => 'themedir1/block-theme-non-latin', + 'template' => 'page-%e6%b5%8b%e8%af%95', + 'args' => array( + 'post_name' => 'page-测试', + 'post_title' => '测试 Page Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme-non-latin/templates/page-test.html' ), + 'post_excerpt' => 'Description of page 测试 template.', + ), + ), + 'template: parent theme deprecated path' => array( + 'theme_dir' => 'themedir1/block-theme-deprecated-path', + 'template' => 'page-home', + 'args' => array( + 'post_name' => 'page-home', + 'post_title' => 'Home Page Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme-deprecated-path/block-templates/page-home.html' ), + 'post_excerpt' => 'Description of page home template.', + ), + ), + 'template: child theme' => array( + 'theme_dir' => 'themedir1/block-theme-child', + 'template' => 'page-1', + 'args' => array( + 'post_name' => 'page-1', + 'post_title' => 'Page 1 Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block-theme-child/templates/page-1.html' ), + 'post_excerpt' => 'Description of page 1 template.', + ), + ), + 'template part: subdir with _-[]. characters' => array( + 'theme_dir' => 'themedir1/block_theme-[0.4.0]', + 'template' => 'large-header', + 'args' => array( + 'post_name' => 'large-header', + 'post_title' => 'Large Header Template Part', + 'post_content' => file_get_contents( $theme_root_dir . 'block_theme-[0.4.0]/parts/large-header.html' ), + 'post_excerpt' => 'Description of large header template.', + ), + ), + 'template: subdir with _-[]. characters' => array( + 'theme_dir' => 'themedir1/block_theme-[0.4.0]', + 'template' => 'page-large-header', + 'args' => array( + 'post_name' => 'page-large-header', + 'post_title' => 'Page Large Template', + 'post_content' => file_get_contents( $theme_root_dir . 'block_theme-[0.4.0]/templates/page-large-header.html' ), + 'post_excerpt' => 'Description of page large template.', + ), + ), + ); + } + + /** + * Tests that get_item() returns plugin-registered templates. + * + * @ticket 61804 + * + * @covers WP_REST_Templates_Controller::get_item + */ + public function test_get_item_from_registry() { + wp_set_current_user( self::$admin_id ); + + $template_name = 'test-plugin//test-template'; + $args = array( + 'content' => 'Template content', + 'title' => 'Test Template', + 'description' => 'Description of test template', + 'post_types' => array( 'post', 'page' ), + ); + + register_block_template( $template_name, $args ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching a registered template shouldn't cause an error." ); + + $data = $response->get_data(); + + $this->assertSame( 'default//test-template', $data['id'], 'Template ID mismatch.' ); + $this->assertSame( 'default', $data['theme'], 'Template theme mismatch.' ); + $this->assertSame( 'Template content', $data['content']['raw'], 'Template content mismatch.' ); + $this->assertSame( 'test-template', $data['slug'], 'Template slug mismatch.' ); + $this->assertSame( 'plugin', $data['source'], "Template source should be 'plugin'." ); + $this->assertSame( 'plugin', $data['origin'], "Template origin should be 'plugin'." ); + $this->assertSame( 'test-plugin', $data['author_text'], 'Template author text mismatch.' ); + $this->assertSame( 'Description of test template', $data['description'], 'Template description mismatch.' ); + $this->assertSame( 'Test Template', $data['title']['rendered'], 'Template title mismatch.' ); + $this->assertSame( 'test-plugin', $data['plugin'], 'Plugin name mismatch.' ); + + unregister_block_template( $template_name ); + + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/test-plugin//test-template' ); + $response = rest_get_server()->dispatch( $request ); + + $this->assertNotWPError( $response, "Fetching an unregistered template shouldn't cause an error." ); + $this->assertSame( 404, $response->get_status(), 'Fetching an unregistered template should return 404.' ); + } + + /** + * @ticket 54507 + * @dataProvider data_sanitize_template_id + */ + public function test_sanitize_template_id( $input_id, $sanitized_id ) { + $endpoint = new WP_REST_Templates_Controller( 'wp_template' ); + $this->assertSame( + $sanitized_id, + $endpoint->_sanitize_template_id( $input_id ) + ); + } + + public function data_sanitize_template_id() { + return array( + array( 'tt1-blocks/index', 'tt1-blocks//index' ), + array( 'tt1-blocks//index', 'tt1-blocks//index' ), + + array( 'theme-experiments/tt1-blocks/index', 'theme-experiments/tt1-blocks//index' ), + array( 'theme-experiments/tt1-blocks//index', 'theme-experiments/tt1-blocks//index' ), + ); + } + + /** + * @ticket 54422 + * @covers WP_REST_Templates_Controller::create_item + */ + public function test_create_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'author' => self::$admin_id, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $modified = get_post( $data['wp_id'] )->post_modified; + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $author_name = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + + $this->assertSame( + array( + 'id' => 'default//my_custom_template', + 'theme' => 'default', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'my_custom_template', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + 'modified' => mysql_to_rfc3339( $modified ), + 'author_text' => $author_name, + 'original_source' => 'user', + ), + $data + ); + } + + /** + * @ticket 54680 + * @covers WP_REST_Templates_Controller::create_item + * @covers WP_REST_Templates_Controller::get_item_schema + */ + public function test_create_item_with_numeric_slug() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => '404', + 'description' => 'Template shown when no content is found.', + 'title' => '404', + 'author' => self::$admin_id, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $modified = get_post( $data['wp_id'] )->post_modified; + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $author_name = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + + $this->assertSame( + array( + 'id' => 'default//404', + 'theme' => 'default', + 'content' => array( + 'raw' => '', + ), + 'slug' => '404', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Template shown when no content is found.', + 'title' => array( + 'raw' => '404', + 'rendered' => '404', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => false, + 'author' => self::$admin_id, + 'modified' => mysql_to_rfc3339( $modified ), + 'author_text' => $author_name, + 'original_source' => 'user', + ), + $data + ); + } + + /** + * @ticket 54422 + * @covers WP_REST_Templates_Controller::create_item + */ + public function test_create_item_raw() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template_raw', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + ), + 'content' => array( + 'raw' => 'Content', + ), + 'author' => self::$admin_id, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $modified = get_post( $data['wp_id'] )->post_modified; + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $author_name = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + + $this->assertSame( + array( + 'id' => 'default//my_custom_template_raw', + 'theme' => 'default', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'my_custom_template_raw', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => true, + 'author' => self::$admin_id, + 'modified' => mysql_to_rfc3339( $modified ), + 'author_text' => $author_name, + 'original_source' => 'user', + ), + $data + ); + } + + public function test_create_item_invalid_author() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( + array( + 'slug' => 'my_custom_template_invalid_author', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'author' => -1, + ) + ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_invalid_author', $response, 400 ); + } + + /** + * @covers WP_REST_Templates_Controller::update_item + */ + public function test_update_item() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/templates/default//my_template' ); + $request->set_body_params( + array( + 'title' => 'My new Index Title', + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'My new Index Title', $data['title']['raw'] ); + $this->assertSame( 'custom', $data['source'] ); + } + + /** + * @covers WP_REST_Templates_Controller::update_item + */ + public function test_update_item_raw() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'PUT', '/wp/v2/templates/default//my_template' ); + $request->set_body_params( + array( + 'title' => array( 'raw' => 'My new raw Index Title' ), + ) + ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'My new raw Index Title', $data['title']['raw'] ); + $this->assertSame( 'custom', $data['source'] ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + */ + public function test_delete_item() { + // Set up template post. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'my_test_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template.', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + ), + ); + $post_id = self::factory()->post->create( $args ); + wp_set_post_terms( $post_id, get_stylesheet(), 'wp_theme' ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/default//my_test_template' ); + $request->set_param( 'force', 'false' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertSame( 'My Template', $data['title']['raw'] ); + $this->assertSame( 'trash', $data['status'] ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + */ + public function test_delete_item_skip_trash() { + // Set up template post. + $args = array( + 'post_type' => 'wp_template', + 'post_name' => 'my_test_template', + 'post_title' => 'My Template', + 'post_content' => 'Content', + 'post_excerpt' => 'Description of my template.', + 'tax_input' => array( + 'wp_theme' => array( + get_stylesheet(), + ), + ), + ); + $post_id = self::factory()->post->create( $args ); + wp_set_post_terms( $post_id, get_stylesheet(), 'wp_theme' ); + + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/default//my_test_template' ); + $request->set_param( 'force', 'true' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 200, $response->get_status() ); + $data = $response->get_data(); + $this->assertTrue( $data['deleted'] ); + $this->assertNotEmpty( $data['previous'] ); + } + + /** + * @covers WP_REST_Templates_Controller::delete_item + */ + public function test_delete_item_fail() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'DELETE', '/wp/v2/templates/justrandom//template' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertErrorResponse( 'rest_template_not_found', $response, 404 ); + } + + /** + * @doesNotPerformAssertions + */ + public function test_prepare_item() { + // Controller does not implement prepare_item(). + } + + public function test_prepare_item_limit_fields() { + wp_set_current_user( self::$admin_id ); + + $endpoint = new WP_REST_Templates_Controller( 'wp_template' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/default//my_template' ); + $request->set_param( 'context', 'edit' ); + $request->set_param( '_fields', 'id,slug' ); + $obj = get_block_template( 'default//my_template', 'wp_template' ); + $response = $endpoint->prepare_item_for_response( $obj, $request ); + $this->assertSame( + array( + 'id', + 'slug', + ), + array_keys( $response->get_data() ) + ); + } + + /** + * @ticket 54422 + * @covers WP_REST_Templates_Controller::get_item_schema + */ + public function test_get_item_schema() { + $request = new WP_REST_Request( 'OPTIONS', '/wp/v2/templates' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $properties = $data['schema']['properties']; + $this->assertCount( 18, $properties ); + $this->assertArrayHasKey( 'id', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'slug', $properties ); + $this->assertArrayHasKey( 'theme', $properties ); + $this->assertArrayHasKey( 'type', $properties ); + $this->assertArrayHasKey( 'source', $properties ); + $this->assertArrayHasKey( 'origin', $properties ); + $this->assertArrayHasKey( 'content', $properties ); + $this->assertArrayHasKey( 'title', $properties ); + $this->assertArrayHasKey( 'description', $properties ); + $this->assertArrayHasKey( 'status', $properties ); + $this->assertArrayHasKey( 'wp_id', $properties ); + $this->assertArrayHasKey( 'has_theme_file', $properties ); + $this->assertArrayHasKey( 'is_custom', $properties ); + $this->assertArrayHasKey( 'author', $properties ); + $this->assertArrayHasKey( 'modified', $properties ); + $this->assertArrayHasKey( 'author_text', $properties ); + $this->assertArrayHasKey( 'original_source', $properties ); + $this->assertArrayHasKey( 'plugin', $properties ); + } + + protected function find_and_normalize_template_by_id( $templates, $id ) { + foreach ( $templates as $template ) { + if ( $template['id'] === $id ) { + unset( $template['content'] ); + unset( $template['_links'] ); + return $template; + } + } + + return null; + } + + /** + * @dataProvider data_create_item_with_is_wp_suggestion + * @ticket 56467 + * @covers WP_REST_Templates_Controller::create_item + * + * @param array $body_params Data set to test. + * @param array $expected Expected results. + */ + public function test_create_item_with_is_wp_suggestion( array $body_params, array $expected ) { + // Set up the user. + $body_params['author'] = self::$admin_id; + $expected['author'] = self::$admin_id; + wp_set_current_user( self::$admin_id ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/templates' ); + $request->set_body_params( $body_params ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $modified = get_post( $data['wp_id'] )->post_modified; + $expected['modified'] = mysql_to_rfc3339( $modified ); + $expected['author_text'] = get_user_by( 'id', self::$admin_id )->get( 'display_name' ); + $expected['original_source'] = 'user'; + + unset( $data['_links'] ); + unset( $data['wp_id'] ); + + $this->assertSame( $expected, $data ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_create_item_with_is_wp_suggestion() { + $expected = array( + 'id' => 'default//page-rigas', + 'theme' => 'default', + 'content' => array( + 'raw' => 'Content', + ), + 'slug' => 'page-rigas', + 'source' => 'custom', + 'origin' => null, + 'type' => 'wp_template', + 'description' => 'Just a description', + 'title' => array( + 'raw' => 'My Template', + 'rendered' => 'My Template', + ), + 'status' => 'publish', + 'has_theme_file' => false, + 'is_custom' => false, + 'author' => null, + ); + + return array( + 'is_wp_suggestion: true' => array( + 'body_params' => array( + 'slug' => 'page-rigas', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => true, + 'author' => null, + ), + 'expected' => $expected, + ), + 'is_wp_suggestion: false' => array( + 'body_params' => array( + 'slug' => 'page-hi', + 'description' => 'Just a description', + 'title' => 'My Template', + 'content' => 'Content', + 'is_wp_suggestion' => false, + 'author' => null, + ), + 'expected' => array_merge( + $expected, + array( + 'id' => 'default//page-hi', + 'slug' => 'page-hi', + 'is_custom' => true, + ) + ), + ), + ); + } + + /** + * @ticket 56467 + * @covers WP_REST_Templates_Controller::get_template_fallback + */ + public function test_get_template_fallback() { + wp_set_current_user( self::$admin_id ); + switch_theme( 'block-theme' ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + // Should fallback to `index.html`. + $request->set_param( 'slug', 'tag-status' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'tag' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'index', $response->get_data()['slug'], 'Should fallback to `index.html`.' ); + // Should fallback to `page.html`. + $request->set_param( 'slug', 'page-hello' ); + $request->set_param( 'is_custom', false ); + $request->set_param( 'template_prefix', 'page' ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'page', $response->get_data()['slug'], 'Should fallback to `page.html`.' ); + // Should fallback to `index.html`. + $request->set_param( 'slug', 'author' ); + $request->set_param( 'ignore_empty', true ); + $request->set_param( 'template_prefix', 'tag' ); + $request->set_param( 'is_custom', false ); + $response = rest_get_server()->dispatch( $request ); + $this->assertSame( 'index', $response->get_data()['slug'], 'Should fallback to `index.html` when ignore_empty is `true`.' ); + } + + /** + * @ticket 60909 + * @covers WP_REST_Templates_Controller::get_template_fallback + */ + public function test_get_template_fallback_not_found() { + wp_set_current_user( self::$admin_id ); + $request = new WP_REST_Request( 'GET', '/wp/v2/templates/lookup' ); + $request->set_param( 'slug', 'not-found' ); + $response = rest_get_server()->dispatch( $request ); + $data = $response->get_data(); + $this->assertEquals( new stdClass(), $data, 'Response should be an empty object when a fallback template is not found.' ); + } + + /** + * @ticket 57851 + * + * @covers WP_REST_Templates_Controller::prepare_item_for_database + */ + public function test_prepare_item_for_database() { + $endpoint = new WP_REST_Templates_Controller( 'wp_template_part' ); + + $prepare_item_for_database = new ReflectionMethod( $endpoint, 'prepare_item_for_database' ); + if ( PHP_VERSION_ID < 80100 ) { + $prepare_item_for_database->setAccessible( true ); + } + + $body_params = array( + 'title' => 'Untitled Template Part', + 'slug' => 'untitled-template-part', + 'content' => '', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/template-parts' ); + $request->set_body_params( $body_params ); + + $prepared = $prepare_item_for_database->invoke( $endpoint, $request ); + + $this->assertInstanceOf( 'stdClass', $prepared, 'The item could not be prepared for the database.' ); + + $this->assertObjectHasProperty( 'post_type', $prepared, 'The "post_type" was not included in the prepared template part.' ); + $this->assertObjectHasProperty( 'post_status', $prepared, 'The "post_status" was not included in the prepared template part.' ); + $this->assertObjectHasProperty( 'tax_input', $prepared, 'The "tax_input" was not included in the prepared template part.' ); + $this->assertArrayHasKey( 'wp_theme', $prepared->tax_input, 'The "wp_theme" tax was not included in the prepared template part.' ); + $this->assertArrayHasKey( 'wp_template_part_area', $prepared->tax_input, 'The "wp_template_part_area" tax was not included in the prepared template part.' ); + $this->assertObjectHasProperty( 'post_content', $prepared, 'The "post_content" was not included in the prepared template part.' ); + $this->assertObjectHasProperty( 'post_title', $prepared, 'The "post_title" was not included in the prepared template part.' ); + + $this->assertSame( 'wp_template_part', $prepared->post_type, 'The "post_type" in the prepared template part should be "wp_template_part".' ); + $this->assertSame( 'publish', $prepared->post_status, 'The post status in the prepared template part should be "publish".' ); + $this->assertSame( WP_TEMPLATE_PART_AREA_UNCATEGORIZED, $prepared->tax_input['wp_template_part_area'], 'The area in the prepared template part should be uncategorized.' ); + $this->assertSame( 'Untitled Template Part', $prepared->post_title, 'The title was not correct in the prepared template part.' ); + + $this->assertEmpty( $prepared->post_content, 'The content was not correct in the prepared template part.' ); + } + + /** + * @ticket 60671 + * + * @covers WP_REST_Templates_Controller::prepare_item_for_database + * @covers inject_ignored_hooked_blocks_metadata_attributes + */ + public function test_prepare_item_for_database_injects_hooked_block() { + register_block_type( + 'tests/hooked-block', + array( + 'block_hooks' => array( + 'tests/anchor-block' => 'after', + ), + ) + ); + + add_filter( 'rest_pre_insert_wp_template_part', 'inject_ignored_hooked_blocks_metadata_attributes' ); + + $endpoint = new WP_REST_Templates_Controller( 'wp_template_part' ); + + $prepare_item_for_database = new ReflectionMethod( $endpoint, 'prepare_item_for_database' ); + if ( PHP_VERSION_ID < 80100 ) { + $prepare_item_for_database->setAccessible( true ); + } + + $id = get_stylesheet() . '//' . 'my_template_part'; + $body_params = array( + 'id' => $id, + 'slug' => 'my_template_part', + 'content' => 'Hello', + ); + + $request = new WP_REST_Request( 'POST', '/wp/v2/template-parts' ); + $request->set_body_params( $body_params ); + + $prepared = $prepare_item_for_database->invoke( $endpoint, $request ); + $this->assertSame( + 'Hello', + $prepared->post_content, + 'The hooked block was not injected into the anchor block\'s ignoredHookedBlocks metadata.' + ); + } +} diff --git a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php index db4d229da3831..7187d1696c05a 100644 --- a/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php +++ b/tests/phpunit/tests/rest-api/wpRestUrlDetailsController.php @@ -1,16 +1,10 @@ get_transient_name(), '__return_null' ); + add_filter( 'pre_site_transient_' . $this->get_transient_name(), '__return_null' ); } public function tear_down() { @@ -102,7 +100,7 @@ public function tear_down() { } /** - * @covers WP_REST_URL_Details_Controller::get_routes + * @covers WP_REST_URL_Details_Controller::register_routes * * @ticket 54358 */ @@ -135,9 +133,9 @@ public function test_get_items() { $this->assertSame( array( 'title' => 'Example Website — - with encoded content.', - 'icon' => 'https://placeholder-site.com/favicon.ico?querystringaddedfortesting', + 'icon' => 'https://example.com/favicon.ico?querystringaddedfortesting', 'description' => 'Example description text here. Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore.', - 'image' => 'https://placeholder-site.com/images/home/screen-themes.png?3', + 'image' => 'https://example.com/images/home/screen-themes.png?3', ), $data ); @@ -290,7 +288,6 @@ public function test_get_items_fails_for_url_which_returns_empty_body_for_succes $expected = strtolower( 'Unable to retrieve body from response at this URL' ); $this->assertStringContainsString( $expected, strtolower( $data['message'] ), 'Response "message" does not contain "' . $expected . '"' ); - } /** @@ -303,7 +300,7 @@ public function test_can_filter_http_request_args_via_filter() { add_filter( 'rest_url_details_http_request_args', - static function( $args, $url ) { + static function ( $args, $url ) { return array_merge( $args, array( @@ -341,12 +338,12 @@ static function( $args, $url ) { */ public function test_will_return_from_cache_if_populated() { $transient_name = $this->get_transient_name(); - remove_filter( "pre_transient_{$transient_name}", '__return_null' ); + remove_filter( "pre_site_transient_{$transient_name}", '__return_null' ); // Force cache to return a known value as the remote URL http response body. add_filter( - "pre_transient_{$transient_name}", - static function() { + "pre_site_transient_{$transient_name}", + static function () { return 'This value from cache.'; } ); @@ -374,7 +371,7 @@ static function() { public function test_allows_filtering_data_retrieved_for_a_given_url() { add_filter( 'rest_prepare_url_details', - static function( $response ) { + static function ( $response ) { $data = $response->get_data(); @@ -388,7 +385,6 @@ static function( $response ) { ); return $response; - } ); @@ -425,7 +421,7 @@ public function test_allows_filtering_response() { */ add_filter( 'rest_prepare_url_details', - static function( $response, $url ) { + static function ( $response, $url ) { return new WP_REST_Response( array( 'status' => 418, @@ -452,7 +448,7 @@ static function( $response, $url ) { $this->assertSame( 418, $data['status'], 'Response "status" is not 418' ); - $expected = 'Response for URL https://placeholder-site.com altered via rest_prepare_url_details filter'; + $expected = 'Response for URL https://example.com altered via rest_prepare_url_details filter'; $this->assertSame( $expected, $data['response'], 'Response "response" is not "' . $expected . '"' ); } @@ -1048,28 +1044,46 @@ public function data_get_image() { ); } + /** + * @doesNotPerformAssertions + */ public function test_context_param() { - $this->markTestSkipped( 'Controller does not use context_param.' ); + // Controller does not use get_context_param(). } + /** + * @doesNotPerformAssertions + */ public function test_get_item() { - $this->markTestSkipped( 'Controller does not have get_item route.' ); + // Controller does not implement get_item(). } + /** + * @doesNotPerformAssertions + */ public function test_create_item() { - $this->markTestSkipped( 'Controller does not have create_item route.' ); + // Controller does not implement create_item(). } + /** + * @doesNotPerformAssertions + */ public function test_update_item() { - $this->markTestSkipped( 'Controller does not have update_item route.' ); + // Controller does not implement update_item(). } + /** + * @doesNotPerformAssertions + */ public function test_delete_item() { - $this->markTestSkipped( 'Controller does not have delete_item route.' ); + // Controller does not implement delete_item(). } + /** + * @doesNotPerformAssertions + */ public function test_prepare_item() { - $this->markTestSkipped( 'Controller does not have prepare_item route.' ); + // Controller does not implement prepare_item(). } /** @@ -1078,20 +1092,20 @@ public function test_prepare_item() { * * @return array faux/mocked response. */ - public function mock_success_request_to_remote_url( $response, $args ) { - return $this->mock_request_to_remote_url( 'success', $args ); + public function mock_success_request_to_remote_url( $response, $parsed_args ) { + return $this->mock_request_to_remote_url( 'success', $parsed_args ); } - public function mock_failed_request_to_remote_url( $response, $args ) { - return $this->mock_request_to_remote_url( 'failure', $args ); + public function mock_failed_request_to_remote_url( $response, $parsed_args ) { + return $this->mock_request_to_remote_url( 'failure', $parsed_args ); } - public function mock_request_to_remote_url_with_empty_body_response( $response, $args ) { - return $this->mock_request_to_remote_url( 'empty_body', $args ); + public function mock_request_to_remote_url_with_empty_body_response( $response, $parsed_args ) { + return $this->mock_request_to_remote_url( 'empty_body', $parsed_args ); } - private function mock_request_to_remote_url( $result_type, $args ) { - $this->request_args = $args; + private function mock_request_to_remote_url( $result_type, $parsed_args ) { + $this->request_args = $parsed_args; $types = array( 'success', @@ -1186,7 +1200,9 @@ private function get_transient_name() { protected function get_reflective_method( $method_name ) { $class = new ReflectionClass( WP_REST_URL_Details_Controller::class ); $method = $class->getMethod( $method_name ); - $method->setAccessible( true ); + if ( PHP_VERSION_ID < 80100 ) { + $method->setAccessible( true ); + } return $method; } } diff --git a/tests/phpunit/tests/rewrite.php b/tests/phpunit/tests/rewrite.php index 62697800cc269..2bb7254abfcef 100644 --- a/tests/phpunit/tests/rewrite.php +++ b/tests/phpunit/tests/rewrite.php @@ -8,6 +8,15 @@ class Tests_Rewrite extends WP_UnitTestCase { private $home_url; + /** + * Temporary storage for blog id for use with filters. + * + * Used in the `test_url_to_postid_of_http_site_when_current_site_uses_https()` method. + * + * @var int + */ + private $blog_id_35531; + public function set_up() { parent::set_up(); @@ -22,6 +31,7 @@ public function tear_down() { $wp_rewrite->init(); update_option( 'home', $this->home_url ); + unset( $this->blog_id_35531 ); parent::tear_down(); } @@ -173,7 +183,7 @@ public function filter_http_home_url( $url, $path, $orig_scheme, $_blog_id ) { public function test_url_to_postid_custom_post_type() { delete_option( 'rewrite_rules' ); - $post_type = rand_str( 12 ); + $post_type = 'url_to_postid'; register_post_type( $post_type, array( 'public' => true ) ); $id = self::factory()->post->create( array( 'post_type' => $post_type ) ); @@ -245,8 +255,17 @@ public function test_url_to_postid_hierarchical_with_matching_leaves() { $this->assertSame( $grandchild_id_2, url_to_postid( get_permalink( $grandchild_id_2 ) ) ); } - public function test_url_to_postid_home_has_path() { + /** + * @covers ::url_to_postid + */ + public function test_url_to_postid_url_has_only_path() { + $this->assertSame( 0, url_to_postid( '/example/' ) ); + } + /** + * @covers ::url_to_postid + */ + public function test_url_to_postid_home_has_only_path() { update_option( 'home', home_url( '/example/' ) ); $id = self::factory()->post->create( diff --git a/tests/phpunit/tests/rewrite/addRewriteEndpoint.php b/tests/phpunit/tests/rewrite/addRewriteEndpoint.php index 0c4c7a332d537..6527f32929917 100644 --- a/tests/phpunit/tests/rewrite/addRewriteEndpoint.php +++ b/tests/phpunit/tests/rewrite/addRewriteEndpoint.php @@ -2,6 +2,8 @@ /** * @group rewrite + * + * @covers ::add_rewrite_endpoint */ class Tests_Rewrite_AddRewriteEndpoint extends WP_UnitTestCase { private $qvs; @@ -108,5 +110,4 @@ public function test_page_endpoint_only_applies_on_page() { $this->assertTrue( is_404() ); $this->assertSame( '', get_query_var( 'page_endpoint' ) ); } - } diff --git a/tests/phpunit/tests/rewrite/addRewriteRule.php b/tests/phpunit/tests/rewrite/addRewriteRule.php index 02efc7bd0aa37..d667f58ceafa9 100644 --- a/tests/phpunit/tests/rewrite/addRewriteRule.php +++ b/tests/phpunit/tests/rewrite/addRewriteRule.php @@ -2,6 +2,8 @@ /** * @group rewrite + * + * @covers ::add_rewrite_rule */ class Tests_Rewrite_AddRewriteRule extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/rewrite/numericSlugs.php b/tests/phpunit/tests/rewrite/numericSlugs.php index e11c6b2a465d9..c174ce4875c4d 100644 --- a/tests/phpunit/tests/rewrite/numericSlugs.php +++ b/tests/phpunit/tests/rewrite/numericSlugs.php @@ -6,6 +6,7 @@ */ class Tests_Rewrite_NumericSlugs extends WP_UnitTestCase { private $old_current_user; + private $author_id; public function set_up() { parent::set_up(); @@ -23,7 +24,7 @@ public function test_go_to_year_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '2015', 'post_date' => '2015-02-01 01:00:00', @@ -53,7 +54,7 @@ public function test_url_to_postid_year_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '2015', 'post_date' => '2015-02-01 01:00:00', @@ -80,7 +81,7 @@ public function test_go_to_year_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '2015', 'post_date' => '2015-02-01 01:00:00', ) @@ -98,7 +99,7 @@ public function test_url_to_postid_year_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '2015', 'post_date' => '2015-02-01 01:00:00', ) @@ -114,7 +115,7 @@ public function test_go_to_month_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '02', 'post_date' => '2015-02-01 01:00:00', @@ -133,7 +134,7 @@ public function test_url_to_postid_month_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '02', 'post_date' => '2015-02-01 01:00:00', @@ -150,7 +151,7 @@ public function test_go_to_month_segment_collision_without_title_no_leading_zero array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '2', 'post_date' => '2015-02-01 01:00:00', @@ -169,7 +170,7 @@ public function test_url_to_postid_month_segment_collision_without_title_no_lead array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '2', 'post_date' => '2015-02-01 01:00:00', @@ -186,7 +187,7 @@ public function test_go_to_month_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '02', 'post_date' => '2015-02-01 01:00:00', ) @@ -204,7 +205,7 @@ public function test_url_to_postid_month_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '02', 'post_date' => '2015-02-01 01:00:00', ) @@ -220,7 +221,7 @@ public function test_go_to_month_segment_collision_with_title_no_leading_zero() array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '2', 'post_date' => '2015-02-01 01:00:00', ) @@ -238,7 +239,7 @@ public function test_url_to_postid_month_segment_collision_with_title_no_leading array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '2', 'post_date' => '2015-02-01 01:00:00', ) @@ -254,7 +255,7 @@ public function test_go_to_day_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '01', 'post_date' => '2015-02-01 01:00:00', @@ -273,7 +274,7 @@ public function test_url_to_postid_day_segment_collision_without_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '', 'post_name' => '01', 'post_date' => '2015-02-01 01:00:00', @@ -290,7 +291,7 @@ public function test_go_to_day_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '01', 'post_date' => '2015-02-01 01:00:00', ) @@ -308,7 +309,7 @@ public function test_url_to_postid_day_segment_collision_with_title() { array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '01', 'post_date' => '2015-02-01 01:00:00', ) @@ -324,7 +325,7 @@ public function test_numeric_slug_permalink_conflicts_should_only_be_resolved_fo array( 'post_author' => $this->author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), + 'post_content' => 'content', 'post_title' => '01', 'post_date' => '2015-02-01 01:00:00', ) diff --git a/tests/phpunit/tests/rewrite/oldDateRedirect.php b/tests/phpunit/tests/rewrite/oldDateRedirect.php index 7f4519875de48..963f103a4fdf4 100644 --- a/tests/phpunit/tests/rewrite/oldDateRedirect.php +++ b/tests/phpunit/tests/rewrite/oldDateRedirect.php @@ -2,6 +2,7 @@ /** * @group rewrite + * @covers wp_old_slug_redirect */ class Tests_Rewrite_OldDateRedirect extends WP_UnitTestCase { protected $old_date_redirect_url; @@ -86,6 +87,75 @@ public function test_old_date_slug_redirect() { $this->assertSame( $permalink, $this->old_date_redirect_url ); } + /** + * @ticket 36723 + */ + public function test_old_date_slug_redirect_cache() { + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $time = '2004-01-03 00:00:00'; + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_date' => $time, + 'post_date_gmt' => get_gmt_from_date( $time ), + 'post_name' => 'bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $this->go_to( $old_permalink ); + + wp_old_slug_redirect(); + $num_queries = get_num_queries(); + $this->assertSame( $permalink, $this->old_date_redirect_url ); + + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_date_redirect_url ); + $this->assertSame( $num_queries, get_num_queries() ); + } + + /** + * @ticket 36723 + */ + public function test_old_date_redirect_cache_invalidation() { + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $time = '2004-01-03 00:00:00'; + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_date' => $time, + 'post_date_gmt' => get_gmt_from_date( $time ), + 'post_name' => 'bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $this->go_to( $old_permalink ); + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_date_redirect_url ); + + $time = '2014-02-01 00:00:00'; + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_date' => $time, + 'post_date_gmt' => get_gmt_from_date( $time ), + 'post_name' => 'foo-bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $num_queries = get_num_queries(); + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_date_redirect_url ); + $this->assertGreaterThan( $num_queries, get_num_queries() ); + } + public function test_old_date_redirect_attachment() { $old_permalink = get_attachment_link( self::$attachment_id ); diff --git a/tests/phpunit/tests/rewrite/oldSlugRedirect.php b/tests/phpunit/tests/rewrite/oldSlugRedirect.php index 2083f9bcce861..cf38ba452eabf 100644 --- a/tests/phpunit/tests/rewrite/oldSlugRedirect.php +++ b/tests/phpunit/tests/rewrite/oldSlugRedirect.php @@ -3,21 +3,24 @@ /** * @group rewrite * @ticket 33920 + * @covers wp_old_slug_redirect */ class Tests_Rewrite_OldSlugRedirect extends WP_UnitTestCase { protected $old_slug_redirect_url; - protected $post_id; + protected static $post_id; - public function set_up() { - parent::set_up(); - - $this->post_id = self::factory()->post->create( + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$post_id = $factory->post->create( array( 'post_title' => 'Foo Bar', 'post_name' => 'foo-bar', ) ); + } + + public function set_up() { + parent::set_up(); add_filter( 'old_slug_redirect_url', array( $this, 'filter_old_slug_redirect_url' ), 10, 1 ); @@ -36,27 +39,88 @@ public function tear_down() { } public function test_old_slug_redirect() { - $old_permalink = user_trailingslashit( get_permalink( $this->post_id ) ); + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_name' => 'bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $this->go_to( $old_permalink ); + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_slug_redirect_url ); + } + + /** + * @ticket 36723 + */ + public function test_old_slug_redirect_cache() { + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); wp_update_post( array( - 'ID' => $this->post_id, + 'ID' => self::$post_id, 'post_name' => 'bar-baz', ) ); - $permalink = user_trailingslashit( get_permalink( $this->post_id ) ); + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); $this->go_to( $old_permalink ); + + wp_old_slug_redirect(); + $num_queries = get_num_queries(); + $this->assertSame( $permalink, $this->old_slug_redirect_url ); + + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_slug_redirect_url ); + $this->assertSame( $num_queries, get_num_queries() ); + } + + /** + * @ticket 36723 + */ + public function test_old_slug_redirect_cache_invalidation() { + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_name' => 'bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $this->go_to( $old_permalink ); + + wp_old_slug_redirect(); + $this->assertSame( $permalink, $this->old_slug_redirect_url ); + + wp_update_post( + array( + 'ID' => self::$post_id, + 'post_name' => 'foo-bar-baz', + ) + ); + + $permalink = user_trailingslashit( get_permalink( self::$post_id ) ); + + $num_queries = get_num_queries(); wp_old_slug_redirect(); $this->assertSame( $permalink, $this->old_slug_redirect_url ); + $this->assertSame( $num_queries + 1, get_num_queries() ); } public function test_old_slug_redirect_attachment() { $file = DIR_TESTDATA . '/images/canola.jpg'; $attachment_id = self::factory()->attachment->create_object( $file, - $this->post_id, + self::$post_id, array( 'post_mime_type' => 'image/jpeg', 'post_name' => 'my-attachment', @@ -67,7 +131,7 @@ public function test_old_slug_redirect_attachment() { wp_update_post( array( - 'ID' => $this->post_id, + 'ID' => self::$post_id, 'post_name' => 'bar-baz', ) ); @@ -86,7 +150,7 @@ public function test_old_slug_redirect_attachment() { ) ); - $permalink = user_trailingslashit( trailingslashit( get_permalink( $this->post_id ) ) . 'the-attachment' ); + $permalink = user_trailingslashit( trailingslashit( get_permalink( self::$post_id ) ) . 'the-attachment' ); $this->go_to( $old_permalink ); wp_old_slug_redirect(); @@ -96,21 +160,21 @@ public function test_old_slug_redirect_attachment() { public function test_old_slug_redirect_paged() { wp_update_post( array( - 'ID' => $this->post_id, + 'ID' => self::$post_id, 'post_content' => 'TestTest', ) ); - $old_permalink = user_trailingslashit( trailingslashit( get_permalink( $this->post_id ) ) . 'page/2' ); + $old_permalink = user_trailingslashit( trailingslashit( get_permalink( self::$post_id ) ) . 'page/2' ); wp_update_post( array( - 'ID' => $this->post_id, + 'ID' => self::$post_id, 'post_name' => 'bar-baz', ) ); - $permalink = user_trailingslashit( trailingslashit( get_permalink( $this->post_id ) ) . 'page/2' ); + $permalink = user_trailingslashit( trailingslashit( get_permalink( self::$post_id ) ) . 'page/2' ); $this->go_to( $old_permalink ); wp_old_slug_redirect(); @@ -121,11 +185,11 @@ public function test_old_slug_redirect_paged() { * @ticket 35031 */ public function test_old_slug_doesnt_redirect_when_reused() { - $old_permalink = user_trailingslashit( get_permalink( $this->post_id ) ); + $old_permalink = user_trailingslashit( get_permalink( self::$post_id ) ); wp_update_post( array( - 'ID' => $this->post_id, + 'ID' => self::$post_id, 'post_name' => 'bar-baz', ) ); diff --git a/tests/phpunit/tests/rewrite/permastructs.php b/tests/phpunit/tests/rewrite/permastructs.php index 4e2bc0594b216..ce98e06a68bb3 100644 --- a/tests/phpunit/tests/rewrite/permastructs.php +++ b/tests/phpunit/tests/rewrite/permastructs.php @@ -2,6 +2,9 @@ /** * @group rewrite + * + * @covers ::add_permastruct + * @covers ::remove_permastruct */ class Tests_Rewrite_Permastructs extends WP_UnitTestCase { diff --git a/tests/phpunit/tests/rewrite/rewriteTags.php b/tests/phpunit/tests/rewrite/rewriteTags.php index 8eeef7f7d93c9..31c28df15e283 100644 --- a/tests/phpunit/tests/rewrite/rewriteTags.php +++ b/tests/phpunit/tests/rewrite/rewriteTags.php @@ -20,19 +20,8 @@ public function set_up() { $this->queryreplace = $wp_rewrite->queryreplace; } - public function _invalid_rewrite_tags() { - return array( - array( 'foo', 'bar' ), - array( '%', 'bar' ), - array( '%a', 'bar' ), - array( 'a%', 'bar' ), - array( '%%', 'bar' ), - array( '', 'bar' ), - ); - } - /** - * @dataProvider _invalid_rewrite_tags + * @dataProvider data_add_rewrite_tag_invalid * * @param string $tag Rewrite tag. * @param string $regex Regex. @@ -46,6 +35,17 @@ public function test_add_rewrite_tag_invalid( $tag, $regex ) { $this->assertSameSets( $this->queryreplace, $wp_rewrite->queryreplace ); } + public function data_add_rewrite_tag_invalid() { + return array( + array( 'foo', 'bar' ), + array( '%', 'bar' ), + array( '%a', 'bar' ), + array( 'a%', 'bar' ), + array( '%%', 'bar' ), + array( '', 'bar' ), + ); + } + public function test_add_rewrite_tag_empty_query() { global $wp_rewrite; diff --git a/tests/phpunit/tests/rewrite/wpResolveNumericSlugConflicts.php b/tests/phpunit/tests/rewrite/wpResolveNumericSlugConflicts.php new file mode 100644 index 0000000000000..7a5232bc86c2f --- /dev/null +++ b/tests/phpunit/tests/rewrite/wpResolveNumericSlugConflicts.php @@ -0,0 +1,71 @@ +post->create( + array( + 'post_date' => '2020-01-05 12:00:00', + 'post_name' => 'post-with-date', + ) + ); + } + + /** + * @ticket 52252 + * @dataProvider data_should_not_throw_warning_for_malformed_date_queries + * + * @param string $permalink_structure Permalink structure. + * @param array $query_vars Query string parameters. + */ + public function test_should_not_throw_warning_for_malformed_date_queries( $permalink_structure, $query_vars ) { + $this->set_permalink_structure( $permalink_structure ); + + /* + * For malformed date queries, the function is unable to identify the requested post, + * and just returns the initial query vars. + */ + $this->assertSame( $query_vars, wp_resolve_numeric_slug_conflicts( $query_vars ) ); + } + + /** + * Data provider for test_should_not_throw_warning_for_malformed_date_queries(). + * + * @return array Test data. + */ + public function data_should_not_throw_warning_for_malformed_date_queries() { + return array( + '/%postname%/ with missing year' => array( + 'permalink_structure' => '/%postname%/', + 'query' => array( + 'monthnum' => 1, + 'day' => 15, + ), + ), + '/%postname%/ with month only' => array( + 'permalink_structure' => '/%postname%/', + 'query' => array( + 'monthnum' => 1, + ), + ), + '/%year%/%postname%/ with missing month' => array( + 'permalink_structure' => '/%year%/%postname%/', + 'query' => array( + 'year' => 2020, + 'day' => 15, + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/robots.php b/tests/phpunit/tests/robots.php index 60da725e75e66..356224e79b51d 100644 --- a/tests/phpunit/tests/robots.php +++ b/tests/phpunit/tests/robots.php @@ -1,12 +1,8 @@ original_script_modules = $wp_script_modules; + $this->original_wp_version = $wp_version; + $this->original_wp_scripts = $wp_scripts ?? null; + $wp_script_modules = null; + $this->script_modules = wp_script_modules(); + + $wp_scripts = new WP_Scripts(); + $wp_scripts->default_version = get_bloginfo( 'version' ); + } + + /** + * Tear down. + */ + public function tear_down() { + parent::tear_down(); + global $wp_script_modules, $wp_scripts, $wp_version; + $wp_script_modules = $this->original_script_modules; + $wp_version = $this->original_wp_version; + $wp_scripts = $this->original_wp_scripts; + } + + /** + * Gets a list of the enqueued script modules. + * + * @return array Enqueued script module URLs, keyed by script module identifier. + */ + public function get_enqueued_script_modules(): array { + $get_modules = function ( string $html, bool $in_footer ): array { + $modules = array(); + $p = new WP_HTML_Tag_Processor( $html ); + while ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) { + $this->assertSame( 'module', $p->get_attribute( 'type' ) ); + $this->assertIsString( $p->get_attribute( 'id' ) ); + $this->assertIsString( $p->get_attribute( 'src' ) ); + $this->assertStringEndsWith( '-js-module', $p->get_attribute( 'id' ) ); + + $id = preg_replace( '/-js-module$/', '', (string) $p->get_attribute( 'id' ) ); + $fetchpriority = $p->get_attribute( 'fetchpriority' ); + $modules[ $id ] = array_merge( + array( + 'url' => $p->get_attribute( 'src' ), + 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto', + 'in_footer' => $in_footer, + ), + ...array_map( + static function ( $attribute_name ) use ( $p ) { + return array( $attribute_name => $p->get_attribute( $attribute_name ) ); + }, + $p->get_attribute_names_with_prefix( 'data-' ) + ) + ); + } + return $modules; + }; + + $modules = array_merge( + $get_modules( get_echo( array( $this->script_modules, 'print_head_enqueued_script_modules' ) ), false ), + $get_modules( get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ), true ) + ); + + return $modules; + } + + /** + * Gets the script modules listed in the import map. + * + * @return array Import map entry URLs, keyed by script module identifier. + */ + public function get_import_map(): array { + $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_import_map' ) ) ); + if ( $p->next_tag( array( 'tag' => 'SCRIPT' ) ) ) { + $this->assertSame( 'importmap', $p->get_attribute( 'type' ) ); + $this->assertSame( 'wp-importmap', $p->get_attribute( 'id' ) ); + $data = json_decode( $p->get_modifiable_text(), true ); + $this->assertIsArray( $data ); + $this->assertArrayHasKey( 'imports', $data ); + return $data['imports']; + } else { + return array(); + } + } + + /** + * Gets a list of preloaded script modules. + * + * @return array Preloaded script module URLs, keyed by script module identifier. + */ + public function get_preloaded_script_modules(): array { + $preloads = array(); + + $p = new WP_HTML_Tag_Processor( get_echo( array( $this->script_modules, 'print_script_module_preloads' ) ) ); + while ( $p->next_tag( array( 'tag' => 'LINK' ) ) ) { + $this->assertSame( 'modulepreload', $p->get_attribute( 'rel' ) ); + $this->assertIsString( $p->get_attribute( 'id' ) ); + $this->assertIsString( $p->get_attribute( 'href' ) ); + $this->assertStringEndsWith( '-js-modulepreload', $p->get_attribute( 'id' ) ); + + $id = preg_replace( '/-js-modulepreload$/', '', $p->get_attribute( 'id' ) ); + $fetchpriority = $p->get_attribute( 'fetchpriority' ); + $preloads[ $id ] = array_merge( + array( + 'url' => $p->get_attribute( 'href' ), + 'fetchpriority' => is_string( $fetchpriority ) ? $fetchpriority : 'auto', + ), + ...array_map( + static function ( $attribute_name ) use ( $p ) { + return array( $attribute_name => $p->get_attribute( $attribute_name ) ); + }, + $p->get_attribute_names_with_prefix( 'data-' ) + ) + ); + } + + return $preloads; + } + + /** + * Test wp_script_modules(). + * + * @covers ::wp_script_modules() + */ + public function test_wp_script_modules() { + $this->assertSame( $this->script_modules, wp_script_modules() ); + } + + /** + * Test wp_register_script_module() with empty ID. + * + * @ticket 63486 + * + * @expectedIncorrectUsage WP_Script_Modules::register + * + * @covers ::wp_register_script_module + * @covers WP_Script_Modules::register + */ + public function test_register_with_empty_id() { + wp_register_script_module( '', '/null-and-void.js' ); + $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) ); + } + + /** + * Test wp_enqueue_script_module() with empty ID. + * + * @ticket 63486 + * + * @expectedIncorrectUsage WP_Script_Modules::enqueue + * + * @covers ::wp_enqueue_script_module + * @covers WP_Script_Modules::enqueue + */ + public function test_enqueue_with_empty_id() { + wp_enqueue_script_module( '', '/null-and-void.js' ); + $this->assertArrayNotHasKey( '', $this->get_registered_script_modules( wp_script_modules() ) ); + $this->assertNotContains( '', wp_script_modules()->get_queue() ); + } + + /** + * Tests various ways of registering, enqueueing, dequeuing, and deregistering a script module. + * + * This ensures that the global function aliases pass all the same parameters as the class methods. + * + * @ticket 56313 + * @ticket 63486 + * + * @dataProvider data_test_register_and_enqueue_script_module + * + * @covers ::wp_register_script_module() + * @covers WP_Script_Modules::register() + * @covers ::wp_enqueue_script_module() + * @covers WP_Script_Modules::enqueue() + * @covers ::wp_dequeue_script_module() + * @covers WP_Script_Modules::dequeue() + * @covers ::wp_deregister_script_module() + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::get_queue() + * @covers WP_Script_Modules::get_marked_for_enqueue() + * @covers WP_Script_Modules::set_fetchpriority() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::print_import_map() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_comprehensive_methods( bool $use_global_function, bool $only_enqueue ) { + global $wp_version; + $wp_version = '99.9.9'; + + $register = static function ( ...$args ) use ( $use_global_function ) { + if ( $use_global_function ) { + wp_register_script_module( ...$args ); + } else { + wp_script_modules()->register( ...$args ); + } + }; + + $reflection_class = new ReflectionClass( wp_script_modules() ); + $get_marked_for_enqueue = $reflection_class->getMethod( 'get_marked_for_enqueue' ); + $get_dependencies = $reflection_class->getMethod( 'get_dependencies' ); + if ( PHP_VERSION_ID < 80100 ) { + $get_marked_for_enqueue->setAccessible( true ); + $get_dependencies->setAccessible( true ); + } + + $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) { + if ( $use_global_function ) { + if ( $only_enqueue ) { + wp_enqueue_script_module( ...$args ); + } else { + wp_register_script_module( ...$args ); + wp_enqueue_script_module( $args[0] ); + } + } else { + if ( $only_enqueue ) { + wp_script_modules()->enqueue( ...$args ); + } else { + wp_script_modules()->register( ...$args ); + wp_script_modules()->enqueue( $args[0] ); + } + } + }; + + // Minimal args. + $register_and_enqueue( 'a', '/a.js' ); + $this->assertSame( array( 'a' ), wp_script_modules()->get_queue(), 'Expected queue to match.' ); + $marked_for_enqueue = $get_marked_for_enqueue->invoke( wp_script_modules() ); + $this->assertSame( wp_script_modules()->get_queue(), array_keys( $marked_for_enqueue ), 'Expected get_queue() to match keys returned by get_marked_for_enqueue().' ); + $this->assertIsArray( $marked_for_enqueue['a'], 'Expected script module "a" to have an array entry.' ); + $this->assertSame( '/a.js', $marked_for_enqueue['a']['src'], 'Expected script module "a" to have the given src.' ); + $this->assertSame( array(), $get_dependencies->invoke( wp_script_modules(), array( 'a' ) ) ); + + // One Dependency. + $register( 'b-dep', '/b-dep.js' ); + $register_and_enqueue( 'b', '/b.js', array( 'b-dep' ) ); + $this->assertTrue( wp_script_modules()->set_fetchpriority( 'b', 'low' ) ); + $this->assertSame( array( 'a', 'b' ), wp_script_modules()->get_queue() ); + $this->assertSame( + array( + 'b-dep' => array( + 'src' => '/b-dep.js', + 'version' => false, + 'dependencies' => array(), + 'in_footer' => false, + 'fetchpriority' => 'auto', + ), + ), + $get_dependencies->invoke( wp_script_modules(), array( 'b' ) ) + ); + + // Two dependencies with different formats and a false version. + $register( 'c-dep', '/c-static.js', array(), false, array( 'fetchpriority' => 'low' ) ); + $register( 'c-static-dep', '/c-static-dep.js', array(), false, array( 'fetchpriority' => 'high' ) ); + $register_and_enqueue( + 'c', + '/c.js', + array( + 'c-dep', + array( + 'id' => 'c-static-dep', + 'import' => 'static', + ), + ), + false + ); + $this->assertSame( array( 'a', 'b', 'c' ), wp_script_modules()->get_queue() ); + + // Two dependencies, one imported statically and the other dynamically, with a null version. + $register( 'd-static-dep', '/d-static-dep.js', array(), false, array( 'fetchpriority' => 'auto' ) ); + $register( 'd-dynamic-dep', '/d-dynamic-dep.js', array(), false, array( 'fetchpriority' => 'high' ) ); // Because this is a dynamic import dependency, the fetchpriority will not be reflected in the markup since no SCRIPT tag and no preload LINK are printed, and the importmap SCRIPT does not support designating a priority. + $register_and_enqueue( + 'd', + '/d.js', + array( + array( + 'id' => 'd-static-dep', + 'import' => 'static', + ), + array( + 'id' => 'd-dynamic-dep', + 'import' => 'dynamic', + ), + ), + null + ); + $this->assertSame( array( 'a', 'b', 'c', 'd' ), wp_script_modules()->get_queue() ); + + // No dependencies, with a string version version. + $register_and_enqueue( + 'e', + '/e.js', + array(), + '1.0.0' + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e' ), wp_script_modules()->get_queue() ); + + // No dependencies, with a string version and fetch priority. + $register_and_enqueue( + 'f', + '/f.js', + array(), + '2.0.0', + array( 'fetchpriority' => 'auto' ) + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f' ), wp_script_modules()->get_queue() ); + + // No dependencies, with a string version and fetch priority of low. + $register_and_enqueue( + 'g', + '/g.js', + array(), + '2.0.0', + array( 'fetchpriority' => 'low' ) + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g' ), wp_script_modules()->get_queue() ); + + // No dependencies, with a string version and fetch priority of high. + $register_and_enqueue( + 'h', + '/h.js', + array(), + '3.0.0', + array( 'fetchpriority' => 'high' ) + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() ); + + // Register and enqueue something which we'll dequeue right away. + $register_and_enqueue( + 'i', + '/i.js', + array(), + '3.0.0' + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() ); + + // Register and enqueue something which we'll deregister right away. + $register_and_enqueue( + 'j', + '/j.js', + array(), + '3.0.0' + ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j' ), wp_script_modules()->get_queue() ); + + // Make sure unregister functions work. + $deregister_id = 'j'; + $this->assertArrayHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) ); + if ( $use_global_function ) { + wp_deregister_script_module( $deregister_id ); + } else { + wp_script_modules()->deregister( $deregister_id ); + } + $this->assertArrayNotHasKey( 'j', $this->get_registered_script_modules( $this->script_modules ) ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i' ), wp_script_modules()->get_queue() ); + + // Make sure dequeue functions work. + $dequeue_id = 'i'; + $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) ); + if ( $use_global_function ) { + wp_dequeue_script_module( $dequeue_id ); + } else { + wp_script_modules()->dequeue( $dequeue_id ); + } + $this->assertArrayHasKey( 'i', $this->get_registered_script_modules( $this->script_modules ) ); + $this->assertSame( array( 'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h' ), wp_script_modules()->get_queue() ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + + $this->assertSame( + array( + 'preload_links' => array( + 'b-dep' => array( + 'url' => '/b-dep.js?ver=99.9.9', + 'fetchpriority' => 'low', // Propagates from 'b'. + ), + 'c-dep' => array( + 'url' => '/c-static.js?ver=99.9.9', + 'fetchpriority' => 'auto', // Not 'low' because the dependent script 'c' has a fetchpriority of 'auto'. + 'data-wp-fetchpriority' => 'low', + ), + 'c-static-dep' => array( + 'url' => '/c-static-dep.js?ver=99.9.9', + 'fetchpriority' => 'auto', // Propagated from 'c'. + 'data-wp-fetchpriority' => 'high', + ), + 'd-static-dep' => array( + 'url' => '/d-static-dep.js?ver=99.9.9', + 'fetchpriority' => 'auto', + ), + ), + 'script_tags' => array( + 'a' => array( + 'url' => '/a.js?ver=99.9.9', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'b' => array( + 'url' => '/b.js?ver=99.9.9', + 'fetchpriority' => 'low', + 'in_footer' => false, + ), + 'c' => array( + 'url' => '/c.js?ver=99.9.9', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'd' => array( + 'url' => '/d.js', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'e' => array( + 'url' => '/e.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'f' => array( + 'url' => '/f.js?ver=2.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'g' => array( + 'url' => '/g.js?ver=2.0.0', + 'fetchpriority' => 'low', + 'in_footer' => false, + ), + 'h' => array( + 'url' => '/h.js?ver=3.0.0', + 'fetchpriority' => 'high', + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'b-dep' => '/b-dep.js?ver=99.9.9', + 'c-dep' => '/c-static.js?ver=99.9.9', + 'c-static-dep' => '/c-static-dep.js?ver=99.9.9', + 'd-static-dep' => '/d-static-dep.js?ver=99.9.9', + 'd-dynamic-dep' => '/d-dynamic-dep.js?ver=99.9.9', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_test_register_and_enqueue_script_module(): array { + $data = array(); + + foreach ( array( true, false ) as $use_global_function ) { + foreach ( array( true, false ) as $only_enqueue ) { + $test_case = compact( 'use_global_function', 'only_enqueue' ); + $key_parts = array(); + foreach ( $test_case as $param_name => $param_value ) { + $key_parts[] = sprintf( '%s_%s', $param_name, json_encode( $param_value ) ); + } + $data[ join( '_', $key_parts ) ] = $test_case; + } + } + + return $data; + } + + /** + * Tests that a script module gets enqueued correctly after being registered. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::set_fetchpriority() + * @covers WP_Script_Modules::set_in_footer() + */ + public function test_wp_enqueue_script_module() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->register( + 'bar', + '/bar.js', + array(), + false, + array( + 'fetchpriority' => 'high', + 'in_footer' => true, + ) + ); + $this->script_modules->register( 'baz', '/baz.js' ); + $this->assertTrue( $this->script_modules->set_fetchpriority( 'baz', 'low' ) ); + $this->assertTrue( $this->script_modules->set_in_footer( 'baz', true ) ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + $this->script_modules->enqueue( 'baz' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 3, $enqueued_script_modules ); + $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] ); + $this->assertFalse( $enqueued_script_modules['foo']['in_footer'] ); + $this->assertStringStartsWith( '/bar.js', $enqueued_script_modules['bar']['url'] ); + $this->assertSame( 'high', $enqueued_script_modules['bar']['fetchpriority'] ); + $this->assertTrue( $enqueued_script_modules['bar']['in_footer'] ); + $this->assertStringStartsWith( '/baz.js', $enqueued_script_modules['baz']['url'] ); + $this->assertSame( 'low', $enqueued_script_modules['baz']['fetchpriority'] ); + $this->assertTrue( $enqueued_script_modules['baz']['in_footer'] ); + } + + /** + * Tests that no script is printed for a script without a src. + * + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::get_src() + */ + public function test_wp_enqueue_script_module_with_empty_src() { + wp_enqueue_script_module( 'with-src', '/src.js', array(), null ); + wp_register_script_module( 'without-src', '' ); + wp_register_script_module( 'without-src-but-filtered', '' ); + wp_enqueue_script_module( 'without-src' ); + wp_enqueue_script_module( 'without-src-but-filtered' ); + $this->assertSame( array( 'with-src', 'without-src', 'without-src-but-filtered' ), wp_script_modules()->get_queue() ); + add_filter( + 'script_module_loader_src', + static function ( $src, $id ) { + if ( 'without-src-but-filtered' === $id ) { + $src = '/was-empty-but-added-via-filter.js'; + } + return $src; + }, + 10, + 2 + ); + $actual = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + $expected = <<<'HTML' + + + +HTML; + $this->assertEqualHTML( + $expected, + $actual, + '', + 'Expected only one SCRIPT tag to be printed.' + ); + } + + /** + * Tests that a script module can be dequeued after being enqueued. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::dequeue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_dequeue_script_module() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->register( 'bar', '/bar.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + $this->script_modules->dequeue( 'foo' ); // Dequeued. + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + $this->assertArrayHasKey( 'bar', $enqueued_script_modules ); + } + + + /** + * Tests that a script module can be deregistered + * after being enqueued, and that will be removed + * from the enqueue list too. + * + * @ticket 60463 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::get_enqueued_script_modules() + */ + public function test_wp_deregister_script_module() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->register( 'bar', '/bar.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + $this->script_modules->deregister( 'foo' ); // Dequeued. + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + $this->assertArrayHasKey( 'bar', $enqueued_script_modules ); + } + + /** + * Tests that a script module is not deregistered + * if it has not been registered before, causing + * no errors. + * + * @ticket 60463 + * + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::get_enqueued_script_modules() + */ + public function test_wp_deregister_unexistent_script_module() { + $this->script_modules->deregister( 'unexistent' ); + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 0, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'unexistent', $enqueued_script_modules ); + } + + /** + * Tests that a script module is not deregistered + * if it has been deregistered previously, causing + * no errors. + * + * @ticket 60463 + * + * @covers WP_Script_Modules::get_enqueued_script_modules() + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::enqueue() + */ + public function test_wp_deregister_already_deregistered_script_module() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->deregister( 'foo' ); // Dequeued. + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 0, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + + $this->script_modules->deregister( 'foo' ); // Dequeued. + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 0, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + } + + /** + * Tests that a script module can be enqueued before it is registered, and will + * be handled correctly once registered. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_enqueue_script_module_works_before_register() { + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->enqueue( 'bar' ); // Not registered. + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] ); + $this->assertArrayNotHasKey( 'bar', $enqueued_script_modules ); + } + + /** + * Tests that a script module can be dequeued before it is registered and + * ensures that it is not enqueued after registration. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::dequeue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_dequeue_script_module_works_before_register() { + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + $this->script_modules->dequeue( 'foo' ); + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->register( 'bar', '/bar.js' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + $this->assertArrayHasKey( 'bar', $enqueued_script_modules ); + } + + /** + * Tests that dependencies for a registered module are added to the import map + * when the script module is enqueued. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_import_map_dependencies() { + $this->script_modules->register( 'foo', '/foo.js', array( 'dep' ) ); + $this->script_modules->register( 'dep', '/dep.js' ); + $this->script_modules->register( 'no-dep', '/no-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + + $import_map = $this->get_import_map(); + + $this->assertCount( 1, $import_map ); + $this->assertStringStartsWith( '/dep.js', $import_map['dep'] ); + $this->assertArrayNotHasKey( 'no-dep', $import_map ); + } + + /** + * Tests that dependencies are not duplicated in the import map when multiple + * script modules require the same dependency. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_import_map_no_duplicate_dependencies() { + $this->script_modules->register( 'foo', '/foo.js', array( 'dep' ) ); + $this->script_modules->register( 'bar', '/bar.js', array( 'dep' ) ); + $this->script_modules->register( 'dep', '/dep.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + + $import_map = $this->get_import_map(); + + $this->assertCount( 1, $import_map ); + $this->assertStringStartsWith( '/dep.js', $import_map['dep'] ); + } + + /** + * Tests that all recursive dependencies (both static and dynamic) are + * included in the import map. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_import_map_recursive_dependencies() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'static-dep', + array( + 'id' => 'dynamic-dep', + 'import' => 'dynamic', + ), + ) + ); + $this->script_modules->register( + 'static-dep', + '/static-dep.js', + array( + array( + 'id' => 'nested-static-dep', + 'import' => 'static', + ), + array( + 'id' => 'nested-dynamic-dep', + 'import' => 'dynamic', + ), + ) + ); + $this->script_modules->register( 'dynamic-dep', '/dynamic-dep.js' ); + $this->script_modules->register( 'nested-static-dep', '/nested-static-dep.js' ); + $this->script_modules->register( 'nested-dynamic-dep', '/nested-dynamic-dep.js' ); + $this->script_modules->register( 'no-dep', '/no-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + + $import_map = $this->get_import_map(); + + $this->assertStringStartsWith( '/static-dep.js', $import_map['static-dep'] ); + $this->assertStringStartsWith( '/dynamic-dep.js', $import_map['dynamic-dep'] ); + $this->assertStringStartsWith( '/nested-static-dep.js', $import_map['nested-static-dep'] ); + $this->assertStringStartsWith( '/nested-dynamic-dep.js', $import_map['nested-dynamic-dep'] ); + $this->assertArrayNotHasKey( 'no-dep', $import_map ); + } + + /** + * Tests that the import map is not printed at all if there are no + * dependencies. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_import_map_doesnt_print_if_no_dependencies() { + $this->script_modules->register( 'foo', '/foo.js' ); // No deps. + $this->script_modules->enqueue( 'foo' ); + + $import_map_markup = get_echo( array( $this->script_modules, 'print_import_map' ) ); + + $this->assertEmpty( $import_map_markup ); + } + + /** + * Tests that only static dependencies are preloaded and dynamic ones are + * excluded. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_wp_enqueue_preloaded_static_dependencies() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'static-dep', + array( + 'id' => 'dynamic-dep', + 'import' => 'dynamic', + ), + ) + // Note: The default fetchpriority=auto is upgraded to high because the dependent script module 'static-dep' has a high fetch priority. + ); + $this->script_modules->register( + 'static-dep', + '/static-dep.js', + array( + array( + 'id' => 'nested-static-dep', + 'import' => 'static', + ), + array( + 'id' => 'nested-dynamic-dep', + 'import' => 'dynamic', + ), + ), + false, + array( 'fetchpriority' => 'high' ) + ); + $this->script_modules->register( 'dynamic-dep', '/dynamic-dep.js' ); + $this->script_modules->register( 'nested-static-dep', '/nested-static-dep.js' ); + $this->script_modules->register( 'nested-dynamic-dep', '/nested-dynamic-dep.js' ); + $this->script_modules->register( 'no-dep', '/no-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + + $preloaded_script_modules = $this->get_preloaded_script_modules(); + + $this->assertCount( 2, $preloaded_script_modules ); + $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] ); + $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); // Not 'high' + $this->assertStringStartsWith( '/nested-static-dep.js', $preloaded_script_modules['nested-static-dep']['url'] ); + $this->assertSame( 'auto', $preloaded_script_modules['nested-static-dep']['fetchpriority'] ); // Auto because the enqueued script foo has the fetchpriority of auto. + $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules ); + $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules ); + $this->assertArrayNotHasKey( 'no-dep', $preloaded_script_modules ); + } + + /** + * Tests that static dependencies of dynamic dependencies are not preloaded. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_wp_dont_preload_static_dependencies_of_dynamic_dependencies() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'static-dep', + array( + 'id' => 'dynamic-dep', + 'import' => 'dynamic', + ), + ) + ); + $this->script_modules->register( 'static-dep', '/static-dep.js' ); + $this->script_modules->register( 'dynamic-dep', '/dynamic-dep.js', array( 'nested-static-dep' ) ); + $this->script_modules->register( 'nested-static-dep', '/nested-static-dep.js' ); + $this->script_modules->register( 'no-dep', '/no-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + + $preloaded_script_modules = $this->get_preloaded_script_modules(); + + $this->assertCount( 1, $preloaded_script_modules ); + $this->assertStringStartsWith( '/static-dep.js', $preloaded_script_modules['static-dep']['url'] ); + $this->assertSame( 'auto', $preloaded_script_modules['static-dep']['fetchpriority'] ); + $this->assertArrayNotHasKey( 'dynamic-dep', $preloaded_script_modules ); + $this->assertArrayNotHasKey( 'nested-dynamic-dep', $preloaded_script_modules ); + $this->assertArrayNotHasKey( 'no-dep', $preloaded_script_modules ); + } + + /** + * Tests that preloaded dependencies don't include enqueued script modules. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_wp_preloaded_dependencies_filter_enqueued_script_modules() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'dep', + 'enqueued-dep', + ) + ); + $this->script_modules->register( 'dep', '/dep.js' ); + $this->script_modules->register( 'enqueued-dep', '/enqueued-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'enqueued-dep' ); // Not preloaded. + + $preloaded_script_modules = $this->get_preloaded_script_modules(); + + $this->assertCount( 1, $preloaded_script_modules ); + $this->assertArrayHasKey( 'dep', $preloaded_script_modules ); + $this->assertArrayNotHasKey( 'enqueued-dep', $preloaded_script_modules ); + } + + /** + * Tests that enqueued script modules with dependants correctly add both the + * script module and its dependencies to the import map. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_enqueued_script_modules_with_dependants_add_import_map() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'dep', + 'enqueued-dep', + ) + ); + $this->script_modules->register( 'dep', '/dep.js' ); + $this->script_modules->register( 'enqueued-dep', '/enqueued-dep.js' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'enqueued-dep' ); // Also in the import map. + + $import_map = $this->get_import_map(); + + $this->assertCount( 2, $import_map ); + $this->assertArrayHasKey( 'dep', $import_map ); + $this->assertArrayHasKey( 'enqueued-dep', $import_map ); + } + + /** + * Tests the functionality of the `get_src` method to ensure + * proper URLs with version strings are returned. + * + * @ticket 56313 + * + * @covers WP_Script_Modules::get_src() + */ + public function test_get_src() { + $get_src = new ReflectionMethod( $this->script_modules, 'get_src' ); + if ( PHP_VERSION_ID < 80100 ) { + $get_src->setAccessible( true ); + } + + $this->script_modules->register( + 'module_with_version', + 'http://example.com/module.js', + array(), + '1.0' + ); + + $result = $get_src->invoke( $this->script_modules, 'module_with_version' ); + $this->assertSame( 'http://example.com/module.js?ver=1.0', $result ); + + $this->script_modules->register( + 'module_without_version', + 'http://example.com/module.js', + array(), + null + ); + + $result = $get_src->invoke( $this->script_modules, 'module_without_version' ); + $this->assertSame( 'http://example.com/module.js', $result ); + + $this->script_modules->register( + 'module_with_wp_version', + 'http://example.com/module.js', + array(), + false + ); + + $result = $get_src->invoke( $this->script_modules, 'module_with_wp_version' ); + $this->assertSame( 'http://example.com/module.js?ver=' . get_bloginfo( 'version' ), $result ); + + $this->script_modules->register( + 'module_with_existing_query_string', + 'http://example.com/module.js?foo=bar', + array(), + '1.0' + ); + + $result = $get_src->invoke( $this->script_modules, 'module_with_existing_query_string' ); + $this->assertSame( 'http://example.com/module.js?foo=bar&ver=1.0', $result ); + + // Filter the version to include the ID in the final URL, to test the filter, this should affect the tests below. + add_filter( + 'script_module_loader_src', + function ( $src, $id ) { + return add_query_arg( 'script_module_id', urlencode( $id ), $src ); + }, + 10, + 2 + ); + + $result = $get_src->invoke( $this->script_modules, 'module_without_version' ); + $this->assertSame( 'http://example.com/module.js?script_module_id=module_without_version', $result ); + + $result = $get_src->invoke( $this->script_modules, 'module_with_existing_query_string' ); + $this->assertSame( 'http://example.com/module.js?foo=bar&ver=1.0&script_module_id=module_with_existing_query_string', $result ); + } + + /** + * Tests that the correct version is propagated to the import map, enqueued + * script modules and preloaded script modules. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::print_import_map() + * @covers WP_Script_Modules::print_script_module_preloads() + * @covers WP_Script_Modules::get_version_query_string() + */ + public function test_version_is_propagated_correctly() { + $this->script_modules->register( + 'foo', + '/foo.js', + array( + 'dep', + ), + '1.0', + array( 'fetchpriority' => 'auto' ) + ); + $this->script_modules->register( 'dep', '/dep.js', array(), '2.0', array( 'fetchpriority' => 'high' ) ); + $this->script_modules->enqueue( 'foo' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] ); + + $import_map = $this->get_import_map(); + $this->assertSame( '/dep.js?ver=2.0', $import_map['dep'] ); + + $preloaded_script_modules = $this->get_preloaded_script_modules(); + $this->assertSame( '/dep.js?ver=2.0', $preloaded_script_modules['dep']['url'] ); + $this->assertSame( 'auto', $preloaded_script_modules['dep']['fetchpriority'] ); // Because 'foo' has a priority of 'auto'. + } + + /** + * Tests that a script module is not registered when calling enqueue without a + * valid src. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_enqueue_script_module_doesnt_register_without_a_valid_src() { + $this->script_modules->enqueue( 'foo' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 0, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + } + + /** + * Tests that a script module is registered when calling enqueue with a valid + * src. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_enqueue_script_module_registers_with_valid_src() { + $this->script_modules->enqueue( 'foo', '/foo.js' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] ); + } + + /** + * Tests that a script module is registered when calling enqueue with a valid + * src the second time. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + */ + public function test_wp_enqueue_script_module_registers_with_valid_src_the_second_time() { + $this->script_modules->enqueue( 'foo' ); // Not valid src. + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 0, $enqueued_script_modules ); + $this->assertArrayNotHasKey( 'foo', $enqueued_script_modules ); + + $this->script_modules->enqueue( 'foo', '/foo.js' ); // Valid src. + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertStringStartsWith( '/foo.js', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'auto', $enqueued_script_modules['foo']['fetchpriority'] ); + } + + /** + * Tests that a script module is registered with all the params when calling + * enqueue. + * + * @ticket 56313 + * @ticket 63486 + * + * @covers WP_Script_Modules::register() + * @covers WP_Script_Modules::enqueue() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::print_import_map() + */ + public function test_wp_enqueue_script_module_registers_all_params() { + $this->script_modules->enqueue( 'foo', '/foo.js', array( 'dep' ), '1.0', array( 'fetchpriority' => 'low' ) ); + $this->script_modules->register( 'dep', '/dep.js' ); + + $enqueued_script_modules = $this->get_enqueued_script_modules(); + $import_map = $this->get_import_map(); + + $this->assertCount( 1, $enqueued_script_modules ); + $this->assertSame( '/foo.js?ver=1.0', $enqueued_script_modules['foo']['url'] ); + $this->assertSame( 'low', $enqueued_script_modules['foo']['fetchpriority'] ); + $this->assertCount( 1, $import_map ); + $this->assertStringStartsWith( '/dep.js', $import_map['dep'] ); + } + + /** + * @ticket 61510 + */ + public function test_print_script_module_data_prints_enqueued_module_data() { + $this->script_modules->enqueue( '@test/module', '/example.js' ); + add_action( + 'script_module_data_@test/module', + function ( $data ) { + $data['foo'] = 'bar'; + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $expected = << +{"foo":"bar"} + + +HTML; + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * @ticket 61510 + */ + public function test_print_script_module_data_prints_dependency_module_data() { + $this->script_modules->register( '@test/dependency', '/dependency.js' ); + $this->script_modules->enqueue( '@test/module', '/example.js', array( '@test/dependency' ) ); + add_action( + 'script_module_data_@test/dependency', + function ( $data ) { + $data['foo'] = 'bar'; + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $expected = << +{"foo":"bar"} + + +HTML; + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * @ticket 61510 + */ + public function test_print_script_module_data_does_not_print_nondependency_module_data() { + $this->script_modules->register( '@test/other', '/dependency.js' ); + $this->script_modules->enqueue( '@test/module', '/example.js' ); + add_action( + 'script_module_data_@test/other', + function ( $data ) { + $data['foo'] = 'bar'; + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $this->assertSame( '', $actual ); + } + + /** + * @ticket 61510 + */ + public function test_print_script_module_data_does_not_print_empty_data() { + $this->script_modules->enqueue( '@test/module', '/example.js' ); + add_action( + 'script_module_data_@test/module', + function ( $data ) { + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $this->assertSame( '', $actual ); + } + + /** + * @ticket 61510 + * + * @dataProvider data_special_chars_script_encoding + * @param string $input Raw input string. + * @param string $expected Expected output string. + * @param string $charset Blog charset option. + */ + public function test_print_script_module_data_encoding( $input, $expected, $charset ) { + add_filter( + 'pre_option_blog_charset', + function () use ( $charset ) { + return $charset; + } + ); + + $this->script_modules->enqueue( '@test/module', '/example.js' ); + add_action( + 'script_module_data_@test/module', + function ( $data ) use ( $input ) { + $data[''] = $input; + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $expected = << +{"":"{$expected}"} + + +HTML; + + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array + */ + public static function data_special_chars_script_encoding(): array { + return array( + // UTF-8 + 'Solidus' => array( '/', '/', 'UTF-8' ), + 'Double quote' => array( '"', '\\"', 'UTF-8' ), + 'Single quote' => array( '\'', '\'', 'UTF-8' ), + 'Less than' => array( '<', '\u003C', 'UTF-8' ), + 'Greater than' => array( '>', '\u003E', 'UTF-8' ), + 'Ampersand' => array( '&', '&', 'UTF-8' ), + 'Newline' => array( "\n", "\\n", 'UTF-8' ), + 'Tab' => array( "\t", "\\t", 'UTF-8' ), + 'Form feed' => array( "\f", "\\f", 'UTF-8' ), + 'Carriage return' => array( "\r", "\\r", 'UTF-8' ), + 'Line separator' => array( "\u{2028}", "\u{2028}", 'UTF-8' ), + 'Paragraph separator' => array( "\u{2029}", "\u{2029}", 'UTF-8' ), + + /* + * The following is the Flag of England emoji + * PHP: "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}" + */ + 'Flag of england' => array( '🏴󠁧󠁢󠁥󠁮󠁧󠁿', '🏴󠁧󠁢󠁥󠁮󠁧󠁿', 'UTF-8' ), + 'Malicious script closer' => array( '', '\u003C/script\u003E', 'UTF-8' ), + 'Entity-encoded malicious script closer' => array( '</script>', '</script>', 'UTF-8' ), + + // Non UTF-8 + 'Solidus non-utf8' => array( '/', '/', 'iso-8859-1' ), + 'Less than non-utf8' => array( '<', '\u003C', 'iso-8859-1' ), + 'Greater than non-utf8' => array( '>', '\u003E', 'iso-8859-1' ), + 'Ampersand non-utf8' => array( '&', '&', 'iso-8859-1' ), + 'Newline non-utf8' => array( "\n", "\\n", 'iso-8859-1' ), + 'Tab non-utf8' => array( "\t", "\\t", 'iso-8859-1' ), + 'Form feed non-utf8' => array( "\f", "\\f", 'iso-8859-1' ), + 'Carriage return non-utf8' => array( "\r", "\\r", 'iso-8859-1' ), + 'Line separator non-utf8' => array( "\u{2028}", "\u2028", 'iso-8859-1' ), + 'Paragraph separator non-utf8' => array( "\u{2029}", "\u2029", 'iso-8859-1' ), + /* + * The following is the Flag of England emoji + * PHP: "\u{1F3F4}\u{E0067}\u{E0062}\u{E0065}\u{E006E}\u{E0067}\u{E007F}" + */ + 'Flag of england non-utf8' => array( '🏴󠁧󠁢󠁥󠁮󠁧󠁿', "\ud83c\udff4\udb40\udc67\udb40\udc62\udb40\udc65\udb40\udc6e\udb40\udc67\udb40\udc7f", 'iso-8859-1' ), + 'Malicious script closer non-utf8' => array( '', '\u003C/script\u003E', 'iso-8859-1' ), + 'Entity-encoded malicious script closer non-utf8' => array( '</script>', '</script>', 'iso-8859-1' ), + + ); + } + + /** + * @ticket 61510 + * + * @dataProvider data_invalid_script_module_data + * @param mixed $data Data to return in filter. + */ + public function test_print_script_module_data_does_not_print_invalid_data( $data ) { + $this->script_modules->enqueue( '@test/module', '/example.js' ); + add_action( + 'script_module_data_@test/module', + function ( $_ ) use ( $data ) { + return $data; + } + ); + + $actual = get_echo( array( $this->script_modules, 'print_script_module_data' ) ); + + $this->assertSame( '', $actual ); + } + + /** + * Data provider for test_fetchpriority_values. + * + * @return array + */ + public function data_provider_fetchpriority_values(): array { + return array( + 'auto' => array( 'fetchpriority' => 'auto' ), + 'low' => array( 'fetchpriority' => 'low' ), + 'high' => array( 'fetchpriority' => 'high' ), + ); + } + + /** + * Tests that valid fetchpriority values are correctly added to the registered module. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @covers WP_Script_Modules::set_fetchpriority + * + * @dataProvider data_provider_fetchpriority_values + * + * @param string $fetchpriority The fetchpriority value to test. + */ + public function test_fetchpriority_values( string $fetchpriority ) { + $this->script_modules->register( 'test-script', '/test-script.js', array(), null, array( 'fetchpriority' => $fetchpriority ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( $fetchpriority, $registered_modules['test-script']['fetchpriority'] ); + + $this->script_modules->register( 'test-script-2', '/test-script-2.js' ); + $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', $fetchpriority ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( $fetchpriority, $registered_modules['test-script-2']['fetchpriority'] ); + + $this->assertTrue( $this->script_modules->set_fetchpriority( 'test-script-2', '' ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['test-script-2']['fetchpriority'] ); + } + + /** + * Tests ways of setting in_footer. + * + * @ticket 63486 + * @ticket 63486 + * + * @covers ::wp_register_script_module + * @covers ::wp_enqueue_script_module + * @covers WP_Script_Modules::set_in_footer + */ + public function test_in_footer_methods() { + wp_register_script_module( 'default', '/default.js', array(), null ); + wp_enqueue_script_module( 'default' ); + + wp_register_script_module( 'in-footer-via-register', '/in-footer-via-register.js', array(), null, array( 'in_footer' => true ) ); + wp_enqueue_script_module( 'in-footer-via-register' ); + + wp_enqueue_script_module( 'in-footer-via-enqueue', '/in-footer-via-enqueue.js', array(), null, array( 'in_footer' => true ) ); + + wp_enqueue_script_module( 'not-in-footer-via-enqueue', '/not-in-footer-via-enqueue.js', array(), null, array( 'in_footer' => false ) ); + + wp_enqueue_script_module( 'in-footer-via-override', '/in-footer-via-override.js', array(), null ); + wp_script_modules()->set_in_footer( 'in-footer-via-override', true ); + + wp_enqueue_script_module( 'not-in-footer-via-override', '/not-in-footer-via-override.js', array(), null, array( 'in_footer' => true ) ); + wp_script_modules()->set_in_footer( 'not-in-footer-via-override', false ); + + $actual_head = get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) ); + $actual_footer = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + + $expected = <<<'HTML' + + + + +HTML; + + $this->assertEqualHTML( + $actual_head, + $expected, + '', + 'Expected equal script modules in the HEAD.' + ); + + $expected = <<<'HTML' + + + + +HTML; + $this->assertEqualHTML( + $actual_footer, + $expected, + '', + 'Expected equal script modules in the footer.' + ); + } + + /** + * Tests that a script module with an invalid fetchpriority value gets a value of auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @expectedIncorrectUsage WP_Script_Modules::register + */ + public function test_register_script_module_having_fetchpriority_with_invalid_value() { + $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => 'silly' ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `silly`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] ); + } + + /** + * Tests that a script module with an invalid fetchpriority value type gets a value of auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @expectedIncorrectUsage WP_Script_Modules::register + */ + public function test_register_script_module_having_fetchpriority_with_invalid_value_type() { + $this->script_modules->register( 'foo', '/foo.js', array(), false, array( 'fetchpriority' => array( 'WHY AM I NOT A STRING???' ) ) ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + $this->assertArrayHasKey( 'WP_Script_Modules::register', $this->caught_doing_it_wrong ); + $this->assertStringContainsString( 'Invalid fetchpriority `array`', $this->caught_doing_it_wrong['WP_Script_Modules::register'] ); + } + + /** + * Tests that a setting the fetchpriority for script module with an invalid value is ignored so that it remains auto. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::register + * @covers WP_Script_Modules::set_fetchpriority + * @expectedIncorrectUsage WP_Script_Modules::set_fetchpriority + */ + public function test_set_fetchpriority_with_invalid_value() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->set_fetchpriority( 'foo', 'silly' ); + $registered_modules = $this->get_registered_script_modules( $this->script_modules ); + $this->assertSame( 'auto', $registered_modules['foo']['fetchpriority'] ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_provider_to_test_fetchpriority_bumping(): array { + return array( + 'enqueue_bajo' => array( + 'enqueues' => array( 'bajo' ), + 'expected' => array( + 'preload_links' => array(), + 'script_tags' => array( + 'bajo' => array( + 'url' => '/bajo.js', + 'fetchpriority' => 'low', // Priority of 'low' not 'high' because the 'auto' dependent was not enqueued. + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'dyno' => '/dyno.js', + ), + ), + ), + 'enqueue_bajo_and_auto' => array( + 'enqueues' => array( 'bajo', 'auto' ), + 'expected' => array( + 'preload_links' => array(), + 'script_tags' => array( + 'bajo' => array( + 'url' => '/bajo.js', + 'fetchpriority' => 'auto', + 'in_footer' => false, + 'data-wp-fetchpriority' => 'low', + ), + 'auto' => array( + 'url' => '/auto.js', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'dyno' => '/dyno.js', + 'bajo' => '/bajo.js', + ), + ), + ), + 'enqueue_auto' => array( + 'enqueues' => array( 'auto' ), + 'expected' => array( + 'preload_links' => array( + 'bajo' => array( + 'url' => '/bajo.js', + 'fetchpriority' => 'auto', + 'data-wp-fetchpriority' => 'low', + ), + ), + 'script_tags' => array( + 'auto' => array( + 'url' => '/auto.js', + 'fetchpriority' => 'auto', // Priority of 'auto' not 'high' because the 'alto' dependent was not enqueued. + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'bajo' => '/bajo.js', + 'dyno' => '/dyno.js', + ), + ), + ), + 'enqueue_alto' => array( + 'enqueues' => array( 'alto' ), + 'expected' => array( + 'preload_links' => array( + 'bajo' => array( + 'url' => '/bajo.js', + 'fetchpriority' => 'high', + 'data-wp-fetchpriority' => 'low', + ), + 'auto' => array( + 'url' => '/auto.js', + 'fetchpriority' => 'high', + ), + ), + 'script_tags' => array( + 'alto' => array( + 'url' => '/alto.js', + 'fetchpriority' => 'high', + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'auto' => '/auto.js', + 'bajo' => '/bajo.js', + 'dyno' => '/dyno.js', + ), + ), + ), + ); + } + + /** + * Tests a higher fetchpriority on a dependent script module causes the fetchpriority of a dependency script module to be bumped. + * + * @ticket 61734 + * + * @covers WP_Script_Modules::print_enqueued_script_modules + * @covers WP_Script_Modules::get_dependents + * @covers WP_Script_Modules::get_recursive_dependents + * @covers WP_Script_Modules::get_highest_fetchpriority + * @covers WP_Script_Modules::print_script_module_preloads + * + * @dataProvider data_provider_to_test_fetchpriority_bumping + */ + public function test_fetchpriority_bumping( array $enqueues, array $expected ) { + $this->script_modules->register( + 'dyno', + '/dyno.js', + array(), + null, + array( 'fetchpriority' => 'low' ) // This won't show up anywhere since it is a dynamic import dependency. + ); + + $this->script_modules->register( + 'bajo', + '/bajo.js', + array( + array( + 'id' => 'dyno', + 'import' => 'dynamic', + ), + ), + null, + array( 'fetchpriority' => 'low' ) + ); + + $this->script_modules->register( + 'auto', + '/auto.js', + array( + array( + 'id' => 'bajo', + 'import' => 'static', + ), + ), + null, + array( 'fetchpriority' => 'auto' ) + ); + $this->script_modules->register( + 'alto', + '/alto.js', + array( 'auto' ), + null, + array( 'fetchpriority' => 'high' ) + ); + + foreach ( $enqueues as $enqueue ) { + $this->script_modules->enqueue( $enqueue ); + } + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + $this->assertSame( + $expected, + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + } + + /** + * Tests bumping fetchpriority with complex dependency graph. + * + * @ticket 61734 + * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3280065818 + * + * @covers WP_Script_Modules::print_enqueued_script_modules + * @covers WP_Script_Modules::get_dependents + * @covers WP_Script_Modules::get_recursive_dependents + * @covers WP_Script_Modules::get_highest_fetchpriority + * @covers WP_Script_Modules::print_script_module_preloads + */ + public function test_fetchpriority_bumping_a_to_z() { + wp_register_script_module( 'a', '/a.js', array( 'b' ), null, array( 'fetchpriority' => 'low' ) ); + wp_register_script_module( 'b', '/b.js', array( 'c' ), null, array( 'fetchpriority' => 'auto' ) ); + wp_register_script_module( 'c', '/c.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'auto' ) ); + wp_register_script_module( 'd', '/d.js', array( 'z' ), null, array( 'fetchpriority' => 'high' ) ); + wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'auto' ) ); + + wp_register_script_module( 'x', '/x.js', array( 'd', 'y' ), null, array( 'fetchpriority' => 'high' ) ); + wp_register_script_module( 'y', '/y.js', array( 'z' ), null, array( 'fetchpriority' => 'auto' ) ); + wp_register_script_module( 'z', '/z.js', array(), null, array( 'fetchpriority' => 'auto' ) ); + + // The fetch priorities are derived from these enqueued dependents. + wp_enqueue_script_module( 'a' ); + wp_enqueue_script_module( 'x' ); + + $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ); + $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + $expected = <<<'HTML' + + + + + + + + + +HTML; + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * Tests bumping fetchpriority with complex dependency graph. + * + * @ticket 61734 + * @link https://github.com/WordPress/wordpress-develop/pull/9770#issuecomment-3284266884 + * + * @covers WP_Script_Modules::print_enqueued_script_modules + * @covers WP_Script_Modules::get_dependents + * @covers WP_Script_Modules::get_recursive_dependents + * @covers WP_Script_Modules::get_highest_fetchpriority + * @covers WP_Script_Modules::print_script_module_preloads + */ + public function test_fetchpriority_propagation() { + // The high fetchpriority for this module will be disregarded because its enqueued dependent has a non-high priority. + wp_register_script_module( 'a', '/a.js', array( 'd', 'e' ), null, array( 'fetchpriority' => 'high' ) ); + wp_register_script_module( 'b', '/b.js', array( 'e' ), null ); + wp_register_script_module( 'c', '/c.js', array( 'e', 'f' ), null ); + wp_register_script_module( 'd', '/d.js', array(), null ); + // The low fetchpriority for this module will be disregarded because its enqueued dependent has a non-low priority. + wp_register_script_module( 'e', '/e.js', array(), null, array( 'fetchpriority' => 'low' ) ); + wp_register_script_module( 'f', '/f.js', array(), null ); + + wp_register_script_module( 'x', '/x.js', array( 'a' ), null, array( 'fetchpriority' => 'low' ) ); + wp_register_script_module( 'y', '/y.js', array( 'b' ), null, array( 'fetchpriority' => 'auto' ) ); + wp_register_script_module( 'z', '/z.js', array( 'c' ), null, array( 'fetchpriority' => 'high' ) ); + + wp_enqueue_script_module( 'x' ); + wp_enqueue_script_module( 'y' ); + wp_enqueue_script_module( 'z' ); + + $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ); + $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + $expected = <<<'HTML' + + + + + + + + + + +HTML; + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * Tests that default script modules are printed as expected. + * + * @ticket 63486 + * + * @covers ::wp_default_script_modules + * @covers WP_Script_Modules::print_script_module_preloads + * @covers WP_Script_Modules::print_head_enqueued_script_modules + * @covers WP_Script_Modules::print_enqueued_script_modules + */ + public function test_default_script_modules() { + wp_default_script_modules(); + wp_enqueue_script_module( '@wordpress/a11y' ); + wp_enqueue_script_module( '@wordpress/block-library/navigation/view' ); + + $actual_preloads = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ) ); + $this->assertEqualHTML( + "\n", + $actual_preloads + ); + + $actual_head_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_head_enqueued_script_modules' ) ) ); + $this->assertEqualHTML( + '', + $actual_head_script_modules + ); + + $actual_footer_script_modules = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ) ); + $expected = <<<'HTML' + + + +HTML; + $this->assertEqualHTML( + $expected, + $actual_footer_script_modules + ); + } + + /** + * Tests expected priority is used when a dependent is registered but not enqueued. + * + * @ticket 64429 + * + * @covers ::wp_default_script_modules + * @covers WP_Script_Modules::print_enqueued_script_modules + * @covers WP_Script_Modules::get_highest_fetchpriority + */ + public function test_priority_of_dependency_for_non_enqueued_dependent() { + wp_default_script_modules(); + wp_register_script_module( 'not-enqueued', 'https://example.com/not-enqueued.js', array( '@wordpress/a11y' ), null, array( 'priority' => 'high' ) ); + wp_enqueue_script_module( '@wordpress/a11y' ); + + $actual = $this->normalize_markup_for_snapshot( get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ) ); + $this->assertEqualHTML( + "\n", + $actual + ); + } + + /** + * Tests that a dependent with high priority for default script modules with a low fetch priority are printed as expected. + * + * @covers ::wp_default_script_modules + * @covers WP_Script_Modules::print_script_module_preloads + * @covers WP_Script_Modules::print_enqueued_script_modules + */ + public function test_dependent_of_default_script_modules() { + wp_default_script_modules(); + wp_enqueue_script_module( + 'super-important', + '/super-important-module.js', + array( '@wordpress/a11y', '@wordpress/block-library/navigation/view' ), + null, + array( 'fetchpriority' => 'high' ) + ); + + $actual = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ); + $actual .= get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + + $actual = $this->normalize_markup_for_snapshot( $actual ); + + $expected = <<<'HTML' + + + + + +HTML; + $this->assertEqualHTML( $expected, $actual ); + } + + /** + * Tests that VIPS script modules are not registered in Core. + * + * The wasm-vips library is plugin-only and should not be included + * in WordPress Core builds due to its large size (~16MB per file). + * + * @ticket 64906 + * + * @covers ::wp_default_script_modules + */ + public function test_vips_script_modules_not_registered_in_core() { + wp_default_script_modules(); + wp_enqueue_script_module( '@wordpress/vips/loader' ); + + $actual = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + + $this->assertStringNotContainsString( 'vips', $actual ); + } + + /** + * Normalizes markup for snapshot. + * + * @param string $markup Markup. + * @return string Normalized markup. + */ + private function normalize_markup_for_snapshot( string $markup ): string { + $processor = new WP_HTML_Tag_Processor( $markup ); + $clean_url = static function ( string $url ): string { + $url = preg_replace( '#^https?://[^/]+#', '', $url ); + return remove_query_arg( 'ver', $url ); + }; + while ( $processor->next_tag() ) { + if ( 'LINK' === $processor->get_tag() && is_string( $processor->get_attribute( 'href' ) ) ) { + $processor->set_attribute( 'href', $clean_url( $processor->get_attribute( 'href' ) ) ); + } elseif ( 'SCRIPT' === $processor->get_tag() && is_string( $processor->get_attribute( 'src' ) ) ) { + $processor->set_attribute( 'src', $clean_url( $processor->get_attribute( 'src' ) ) ); + } + } + return $processor->get_updated_html(); + } + + /** + * Tests that manipulating the queue works as expected. + * + * @ticket 63676 + * + * @covers WP_Script_Modules::get_queue + * @covers WP_Script_Modules::queue + * @covers WP_Script_Modules::dequeue + */ + public function test_direct_queue_manipulation() { + $this->script_modules->register( 'foo', '/foo.js' ); + $this->script_modules->register( 'bar', '/bar.js' ); + $this->script_modules->register( 'baz', '/baz.js' ); + $this->assertSame( array(), $this->script_modules->get_queue(), 'Expected queue to be empty.' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'foo' ); + $this->script_modules->enqueue( 'bar' ); + $this->assertSame( array( 'foo', 'bar' ), $this->script_modules->get_queue(), 'Expected two deduplicated queued items.' ); + $this->script_modules->dequeue( 'foo' ); + $this->script_modules->dequeue( 'foo' ); + $this->script_modules->enqueue( 'baz' ); + $this->assertSame( array( 'bar', 'baz' ), $this->script_modules->get_queue(), 'Expected items tup be updated after dequeue and enqueue.' ); + $this->script_modules->dequeue( 'baz' ); + $this->script_modules->dequeue( 'bar' ); + $this->assertSame( array(), $this->script_modules->get_queue(), 'Expected queue to be empty after dequeueing both items.' ); + } + + /** + * Gets registered script modules. + * + * @param WP_Script_Modules $script_modules + * @return array Registered modules. + */ + private function get_registered_script_modules( WP_Script_Modules $script_modules ): array { + $reflection_class = new ReflectionClass( $script_modules ); + $registered_property = $reflection_class->getProperty( 'registered' ); + if ( PHP_VERSION_ID < 80100 ) { + $registered_property->setAccessible( true ); + } + return $registered_property->getValue( $script_modules ); + } + + /** + * Returns the inline text of the first SCRIPT tag with the given id, or null if not found. + * + * @param string $markup The HTML markup to search. + * @param string $id The id attribute to match. + * @return string|null The inline script text, or null if no matching tag was found. + */ + private function get_inline_script_text( string $markup, string $id ): ?string { + $processor = new WP_HTML_Tag_Processor( $markup ); + while ( $processor->next_tag( 'SCRIPT' ) ) { + if ( $id === $processor->get_attribute( 'id' ) ) { + return $processor->get_modifiable_text(); + } + } + return null; + } + + /** + * Data provider. + * + * @return array + */ + public static function data_invalid_script_module_data(): array { + return array( + 'null' => array( null ), + 'stdClass' => array( new stdClass() ), + 'number 1' => array( 1 ), + 'string' => array( 'string' ), + ); + } + + /** + * Tests that script modules identified as dependencies of classic scripts are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_included_module_appears_in_importmap() { + $this->script_modules->register( 'dependency', '/dep.js' ); + $this->script_modules->register( 'example', '/example.js', array( 'dependency' ) ); + $this->script_modules->register( 'example2', '/example2.js' ); + + // Nothing printed now. + $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Initial enqueued script modules was wrong.' ); + $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Initial module preloads was wrong.' ); + $this->assertSame( array(), $this->get_import_map(), 'Initial import map was wrong.' ); + + // Enqueuing a script with a module dependency should add it to the import map. + wp_enqueue_script( + 'classic', + '/classic.js', + array( 'classic-dependency' ), + false, + array( + 'strategy' => 'defer', + 'module_dependencies' => array( + 'example', + array( + 'id' => 'example2', + ), + ), + ) + ); + + $this->assertSame( array(), $this->get_enqueued_script_modules(), 'Final enqueued script modules was wrong.' ); + $this->assertSame( array(), $this->get_preloaded_script_modules(), 'Final module preloads was wrong.' ); + $this->assertEqualSets( + array( 'example', 'example2', 'dependency' ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that dynamic dependencies of enqueued script modules are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_import_map_includes_dynamic_dependencies_of_enqueued_modules() { + $this->script_modules->register( 'dependency-of-enqueued', '/dependency-of-enqueued.js' ); + $this->script_modules->enqueue( + 'enqueued', + '/enqueued.js', + array( + array( + 'id' => 'dependency-of-enqueued', + 'import' => 'dynamic', + ), + ) + ); + + $enqueued = $this->get_enqueued_script_modules(); + $this->assertCount( 1, $enqueued, 'Enqueue count was wrong.' ); + $this->assertArrayHasKey( 'enqueued', $enqueued, 'Missing "enqueued" script module enqueue.' ); + $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' ); + $this->assertEqualSets( + array( 'dependency-of-enqueued' ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that script module dependencies of enqueued classic scripts (including transitive ones) are included in the import map. + * + * @ticket 61500 + * + * @covers WP_Script_Modules::get_import_map + */ + public function test_import_map_includes_dependencies_of_classic_scripts_recursive() { + $this->script_modules->register( 'classic-transitive-dependency', '/classic-transitive-dependency.js' ); + $this->script_modules->register( 'dependency-of-not-enqueued', '/dependency-of-not-enqueued.js' ); + $this->script_modules->register( 'not-enqueued', '/not-enqueued.js', array( 'dependency-of-not-enqueued' ) ); + + // Enqueuing a script with a module dependency should add it to the import map. + wp_register_script( + 'classic-transitive-dep', + '/classic-transitive-dep.js', + array(), + false, + array( + 'in_footer' => true, + 'module_dependencies' => array( 'classic-transitive-dependency' ), + ) + ); + wp_enqueue_script( + 'classic', + '/classic.js', + array( 'classic-transitive-dep' ), + false, + array( + 'in_footer' => true, + 'module_dependencies' => array( 'not-enqueued' ), + ) + ); + + $enqueued = $this->get_enqueued_script_modules(); + $this->assertCount( 0, $enqueued, 'Enqueue count was wrong.' ); + $this->assertCount( 0, $this->get_preloaded_script_modules(), 'Module preload count was wrong.' ); + $this->assertEqualSets( + array( + 'classic-transitive-dependency', + 'not-enqueued', + 'dependency-of-not-enqueued', + ), + array_keys( $this->get_import_map() ), + 'Import map keys were wrong.' + ); + } + + /** + * Tests that WP_Scripts emits a _doing_it_wrong() notice for missing script module dependencies. + * + * @ticket 61500 + * @ticket 64229 + * @covers WP_Script_Modules::get_import_map + */ + public function test_wp_scripts_doing_it_wrong_for_missing_script_module_dependencies() { + $expected_incorrect_usage = 'WP_Scripts::add_data'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + wp_enqueue_script( + 'registered-dep', + '/registered-dep.js', + array(), + null, + array( + 'strategy' => 'defer', + 'module_dependencies' => array( 'does-not-exist' ), + ) + ); + + $import_map = $this->get_import_map(); + $this->assertSame( array(), $import_map, 'Expected importmap to be empty.' ); + $markup = get_echo( 'wp_print_scripts' ); + + /* + * In the future, we may want to have missing script module dependencies for classic scripts to cause the + * classic script to not be printed. This would align the behavior with script modules that have missing + * script module dependencies, and classic scripts that have missing classic script dependencies. Nevertheless, + * since script module dependencies rely on dynamic imports, the dependency may not be as strong. This means + * the classic script may still work or have a fallback in case the script module fails to dynamically import. + * This same change could be made for script modules as well, where if a script module has a missing dynamic + * script module dependency, this might similarly not be sufficient reason to omit printing the dependent script module. + */ + $this->assertStringContainsString( 'registered-dep.js', $markup, 'Expected script to be present, even though it has a missing script module dependency.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + "Expected $expected_incorrect_usage to trigger a _doing_it_wrong() notice for missing dependency." + ); + + $this->assertStringContainsString( + 'The script with the handle "registered-dep" was enqueued with script module dependencies ("module_dependencies") that are not registered: does-not-exist', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ], + 'Expected _doing_it_wrong() notice to indicate missing script module dependencies for enqueued script.' + ); + } + + /** + * Tests various ways of printing and dependency ordering of script modules. + * + * This ensures that the global function aliases pass all the same parameters as the class methods. + * + * @ticket 63486 + * + * @dataProvider data_test_register_and_enqueue_script_module + * + * @covers ::wp_register_script_module() + * @covers WP_Script_Modules::register() + * @covers ::wp_enqueue_script_module() + * @covers WP_Script_Modules::enqueue() + * @covers ::wp_dequeue_script_module() + * @covers WP_Script_Modules::dequeue() + * @covers ::wp_deregister_script_module() + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::set_fetchpriority() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::print_import_map() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_script_module_printing_and_dependency_ordering( bool $use_global_function, bool $only_enqueue ) { + global $wp_version; + $wp_version = '99.9.9'; + + $register = static function ( ...$args ) use ( $use_global_function ) { + if ( $use_global_function ) { + wp_register_script_module( ...$args ); + } else { + wp_script_modules()->register( ...$args ); + } + }; + + $register_and_enqueue = static function ( ...$args ) use ( $use_global_function, $only_enqueue ) { + if ( $use_global_function ) { + if ( $only_enqueue ) { + wp_enqueue_script_module( ...$args ); + } else { + wp_register_script_module( ...$args ); + wp_enqueue_script_module( $args[0] ); + } + } else { + if ( $only_enqueue ) { + wp_script_modules()->enqueue( ...$args ); + } else { + wp_script_modules()->register( ...$args ); + wp_script_modules()->enqueue( $args[0] ); + } + } + }; + + $deregister = static function ( array $ids ) use ( $use_global_function ) { + foreach ( $ids as $id ) { + if ( $use_global_function ) { + wp_deregister_script_module( $id ); + } else { + wp_script_modules()->deregister( $id ); + } + } + }; + + // Test script module is placed in footer when in_footer is true. + $register_and_enqueue( 'a', '/a.js', array(), '1.0.0', array( 'in_footer' => true ) ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + $this->assertSame( + array( + 'preload_links' => array(), + 'script_tags' => array( + 'a' => array( + 'url' => '/a.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + ), + 'import_map' => array(), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + + $deregister( array( 'a' ) ); + + // Test that dependant also gets placed in footer when its dependency is in footer. + $register_and_enqueue( 'b', '/b.js', array(), '1.0.0', array( 'in_footer' => true ) ); + $register_and_enqueue( 'c', '/c.js', array( 'b' ), '1.0.0' ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + $this->assertSame( + array( + 'preload_links' => array(), + 'script_tags' => array( + 'b' => array( + 'url' => '/b.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + 'c' => array( + 'url' => '/c.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + ), + 'import_map' => array( + 'b' => '/b.js?ver=1.0.0', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + + $deregister( array( 'b', 'c' ) ); + + // Test that registered dependency in footer doesn't place dependant in footer. + $register( 'd', '/d.js', array(), '1.0.0', array( 'in_footer' => true ) ); + $register_and_enqueue( 'e', '/e.js', array( 'd' ), '1.0.0' ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + $this->assertSame( + array( + 'preload_links' => array( + 'd' => array( + 'url' => '/d.js?ver=1.0.0', + 'fetchpriority' => 'auto', + ), + ), + 'script_tags' => array( + 'e' => array( + 'url' => '/e.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'd' => '/d.js?ver=1.0.0', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + + $deregister( array( 'd', 'e' ) ); + + // Test if one of the dependency is in footer, the dependant and other dependant dependencies are also placed in footer. + $register_and_enqueue( 'f', '/f.js', array(), '1.0.0' ); + $register_and_enqueue( 'g', '/g.js', array( 'f' ), '1.0.0', array( 'in_footer' => true ) ); + $register_and_enqueue( 'h', '/h.js', array( 'g' ), '1.0.0' ); + $register_and_enqueue( 'i', '/i.js', array( 'h' ), '1.0.0' ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + + $this->assertSame( + array( + 'preload_links' => array(), + 'script_tags' => array( + 'f' => array( + 'url' => '/f.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'g' => array( + 'url' => '/g.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + 'h' => array( + 'url' => '/h.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + 'i' => array( + 'url' => '/i.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + ), + 'import_map' => array( + 'f' => '/f.js?ver=1.0.0', + 'g' => '/g.js?ver=1.0.0', + 'h' => '/h.js?ver=1.0.0', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + + $deregister( array( 'f', 'g', 'h', 'i' ) ); + + // Test dependency ordering when all scripts modules are enqueued in head. + // Expected order: j, k, l, m. + $register_and_enqueue( 'm', '/m.js', array( 'j', 'l' ), '1.0.0' ); + $register_and_enqueue( 'k', '/k.js', array( 'j' ), '1.0.0' ); + $register_and_enqueue( 'l', '/l.js', array( 'k' ), '1.0.0' ); + $register_and_enqueue( 'j', '/j.js', array(), '1.0.0' ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + + $this->assertSame( + array( + 'preload_links' => array(), + 'script_tags' => array( + 'j' => array( + 'url' => '/j.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'k' => array( + 'url' => '/k.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'l' => array( + 'url' => '/l.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'm' => array( + 'url' => '/m.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + ), + 'import_map' => array( + 'j' => '/j.js?ver=1.0.0', + 'l' => '/l.js?ver=1.0.0', + 'k' => '/k.js?ver=1.0.0', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + + $deregister( array( 'j', 'k', 'l', 'm' ) ); + + // Test dependency ordering when scripts modules are enqueued in both head and footer. + // Expected order: q, n, o, p, r. + $register_and_enqueue( 'n', '/n.js', array( 'q' ), '1.0.0' ); + $register_and_enqueue( 'q', '/q.js', array(), '1.0.0' ); + $register_and_enqueue( 'o', '/o.js', array( 'n' ), '1.0.0', array( 'in_footer' => true ) ); + $register_and_enqueue( 'r', '/r.js', array( 'q', 'o', 'p' ), '1.0.0' ); + $register_and_enqueue( 'p', '/p.js', array(), '1.0.0', array( 'in_footer' => true ) ); + + $actual = array( + 'preload_links' => $this->get_preloaded_script_modules(), + 'script_tags' => $this->get_enqueued_script_modules(), + 'import_map' => $this->get_import_map(), + ); + + $this->assertSame( + array( + 'preload_links' => array(), + 'script_tags' => array( + 'q' => array( + 'url' => '/q.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'n' => array( + 'url' => '/n.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => false, + ), + 'o' => array( + 'url' => '/o.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + 'p' => array( + 'url' => '/p.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + 'r' => array( + 'url' => '/r.js?ver=1.0.0', + 'fetchpriority' => 'auto', + 'in_footer' => true, + ), + ), + 'import_map' => array( + 'q' => '/q.js?ver=1.0.0', + 'n' => '/n.js?ver=1.0.0', + 'o' => '/o.js?ver=1.0.0', + 'p' => '/p.js?ver=1.0.0', + ), + ), + $actual, + "Snapshot:\n" . var_export( $actual, true ) + ); + } + + /** + * Tests various ways of printing and dependency ordering of script modules. + * + * @ticket 63486 + * + * @dataProvider data_test_register_and_enqueue_script_module + * + * @covers ::wp_register_script_module() + * @covers WP_Script_Modules::register() + * @covers ::wp_enqueue_script_module() + * @covers WP_Script_Modules::enqueue() + * @covers ::wp_dequeue_script_module() + * @covers WP_Script_Modules::dequeue() + * @covers ::wp_deregister_script_module() + * @covers WP_Script_Modules::deregister() + * @covers WP_Script_Modules::set_fetchpriority() + * @covers WP_Script_Modules::print_head_enqueued_script_modules() + * @covers WP_Script_Modules::print_enqueued_script_modules() + * @covers WP_Script_Modules::print_import_map() + * @covers WP_Script_Modules::print_script_module_preloads() + */ + public function test_static_import_dependency_with_dynamic_imports_depending_on_static_import_dependency() { + $get_dependency = function ( string $id, string $import ): array { + return compact( 'id', 'import' ); + }; + + wp_register_script_module( 'enqueued', '/enqueued.js', array( $get_dependency( 'static1', 'static' ) ), null ); + wp_register_script_module( 'static1', '/static1.js', array( $get_dependency( 'dynamic1', 'dynamic' ) ), null ); + wp_register_script_module( 'dynamic1', '/dynamic1.js', array( $get_dependency( 'static2', 'static' ) ), null ); + wp_register_script_module( 'static2', '/static2.js', array(), null ); + + wp_enqueue_script_module( 'enqueued' ); + $import_map = $this->get_import_map(); + $preload_links = get_echo( array( wp_script_modules(), 'print_script_module_preloads' ) ); + $script_modules = get_echo( array( wp_script_modules(), 'print_enqueued_script_modules' ) ); + + $this->assertEquals( + array( + 'static1' => '/static1.js', + 'dynamic1' => '/dynamic1.js', + 'static2' => '/static2.js', + ), + $import_map, + "Expected import map to match snapshot:\n" . var_export( $import_map, true ) + ); + $this->assertEqualHTML( + "\n", + $preload_links, + '', + 'Expected preload links to match.' + ); + $this->assertEqualHTML( + "\n", + $script_modules, + '', + 'Expected script modules to match.' + ); + } + + /** + * Tests that a missing script module dependency triggers a _doing_it_wrong() notice. + * + * @ticket 64229 + * @covers WP_Script_Modules::sort_item_dependencies + */ + public function test_missing_script_module_dependency_triggers_incorrect_usage() { + $expected_incorrect_usage = 'WP_Script_Modules::register'; + $this->setExpectedIncorrectUsage( $expected_incorrect_usage ); + + $this->script_modules->enqueue( 'main-module', '/main-module.js', array( 'missing-mod-dep' ) ); + + $markup = get_echo( array( $this->script_modules, 'print_enqueued_script_modules' ) ); + $this->assertStringNotContainsString( 'main-module.js', $markup, 'Expected script module to be absent.' ); + + $this->assertArrayHasKey( + $expected_incorrect_usage, + $this->caught_doing_it_wrong, + 'Expected WP_Script_Modules::register to be reported via doing_it_wrong().' + ); + + // Assert the message mentions the missing dependency handle. + $this->assertStringContainsString( + 'The script module with the ID "main-module" was enqueued with dependencies that are not registered: missing-mod-dep', + $this->caught_doing_it_wrong[ $expected_incorrect_usage ] + ); + } + + /** + * Tests that set_translations() returns false for unregistered module. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::set_translations + */ + public function test_set_translations_returns_false_for_unregistered_module() { + $result = $this->script_modules->set_translations( 'unregistered-module', 'default' ); + $this->assertFalse( $result ); + } + + /** + * Tests that set_translations() returns true for registered module. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::set_translations + */ + public function test_set_translations_returns_true_for_registered_module() { + $this->script_modules->register( 'test-module', '/test-module.js' ); + $result = $this->script_modules->set_translations( 'test-module', 'test-domain' ); + $this->assertTrue( $result ); + } + + /** + * Tests that wp_set_script_module_translations() wrapper works. + * + * @ticket 65015 + * + * @covers ::wp_set_script_module_translations + * @covers WP_Script_Modules::set_translations + */ + public function test_wp_set_script_module_translations_wrapper() { + wp_register_script_module( 'test-module', '/test-module.js' ); + $result = wp_set_script_module_translations( 'test-module', 'test-domain' ); + $this->assertTrue( $result ); + } + + /** + * Tests that wp_set_script_module_translations() returns false for unregistered module. + * + * @ticket 65015 + * + * @covers ::wp_set_script_module_translations + * @covers WP_Script_Modules::set_translations + */ + public function test_wp_set_script_module_translations_returns_false_for_unregistered() { + $result = wp_set_script_module_translations( 'unregistered-module', 'default' ); + $this->assertFalse( $result ); + } + + /** + * Tests that get_registered() returns null for unregistered module. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::get_registered + */ + public function test_get_registered_returns_null_for_unregistered_module() { + $result = $this->script_modules->get_registered( 'unregistered-module' ); + $this->assertNull( $result ); + } + + /** + * Tests that get_registered() returns correct src for registered module. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::get_registered + */ + public function test_get_registered_returns_array_for_registered_module() { + $this->script_modules->register( 'test-module', '/test-module.js' ); + $result = $this->script_modules->get_registered( 'test-module' ); + $this->assertIsArray( $result, 'get_registered() should return an array for a registered module.' ); + $this->assertSame( '/test-module.js', $result['src'], 'src should match the value passed to register().' ); + $this->assertFalse( $result['version'], 'version should default to false.' ); + $this->assertSame( array(), $result['dependencies'], 'dependencies should default to an empty array.' ); + $this->assertFalse( $result['in_footer'], 'in_footer should default to false.' ); + $this->assertSame( 'auto', $result['fetchpriority'], 'fetchpriority should default to auto.' ); + } + + /** + * Tests that print_script_module_translations() outputs nothing when no translations are set. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::print_script_module_translations + */ + public function test_print_script_module_translations_outputs_nothing_when_no_translations() { + $this->script_modules->register( 'test-module', '/test-module.js' ); + $this->script_modules->enqueue( 'test-module' ); + + $output = get_echo( array( $this->script_modules, 'print_script_module_translations' ) ); + + $this->assertEmpty( $output ); + } + + /** + * Tests that print_script_module_translations() outputs nothing for non-enqueued modules. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::print_script_module_translations + */ + public function test_print_script_module_translations_outputs_nothing_for_non_enqueued() { + $this->script_modules->register( 'test-module', '/test-module.js' ); + $this->script_modules->set_translations( 'test-module', 'default' ); + + $output = get_echo( array( $this->script_modules, 'print_script_module_translations' ) ); + + $this->assertEmpty( $output ); + } + + /** + * Tests that print_script_module_translations() auto-detects translations + * for enqueued modules without requiring an explicit set_translations() call. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::print_script_module_translations + * @covers ::load_script_module_textdomain + */ + public function test_print_script_module_translations_outputs_set_locale_data() { + $this->script_modules->register( 'test-module', '/wp-includes/js/test-module.js' ); + $this->script_modules->enqueue( 'test-module' ); + + // Provide test translations via the pre_load_script_translations filter + // so no fixture files are needed. + add_filter( + 'pre_load_script_translations', + static function ( $translations, $file, $handle ) { + if ( 'test-module' !== $handle ) { + return $translations; + } + return wp_json_encode( + array( + 'domain' => 'messages', + 'locale_data' => array( + 'messages' => array( + '' => array( + 'domain' => 'messages', + 'lang' => 'es', + ), + 'Hello' => array( 'Hola' ), + ), + ), + ) + ); + }, + 10, + 3 + ); + + $filtered = array(); + add_filter( + 'load_script_textdomain_relative_path', + static function ( $relative, $src, $is_module ) use ( &$filtered ) { + $filtered[] = compact( 'relative', 'src', 'is_module' ); + return $relative; + }, + 10, + 3 + ); + + $output = get_echo( array( $this->script_modules, 'print_script_module_translations' ) ); + + $this->assertCount( 1, $filtered, 'load_script_textdomain_relative_path filter should fire once for the enqueued module.' ); + foreach ( $filtered as $filter_args ) { + $this->assertIsString( $filter_args['relative'], 'Filter should receive a string $relative argument.' ); + $this->assertIsString( $filter_args['src'], 'Filter should receive a string $src argument.' ); + $this->assertTrue( $filter_args['is_module'], 'Filter should receive $is_module=true for script modules.' ); + } + + $script_text = $this->get_inline_script_text( $output, 'wp-script-module-translation-data-test-module' ); + $this->assertNotNull( $script_text, 'Translation inline script should be printed with the expected ID.' ); + $this->assertStringContainsString( 'wp.i18n.setLocaleData', $script_text, 'Output should call wp.i18n.setLocaleData().' ); + $this->assertStringContainsString( 'Hola', $script_text, 'Output should contain the translated string.' ); + } + + /** + * Tests that print_script_module_translations() also outputs translations + * for dependencies of enqueued modules (not just directly enqueued ones). + * + * @ticket 65015 + * + * @covers WP_Script_Modules::print_script_module_translations + * @covers ::load_script_module_textdomain + */ + public function test_print_script_module_translations_includes_dependencies() { + $this->script_modules->register( 'dep-module', '/wp-includes/js/dep-module.js' ); + $this->script_modules->register( 'main-module', '/wp-includes/js/main-module.js', array( 'dep-module' ) ); + $this->script_modules->enqueue( 'main-module' ); + + add_filter( + 'pre_load_script_translations', + static function ( $translations, $file, $handle ) { + if ( 'dep-module' !== $handle ) { + return $translations; + } + return wp_json_encode( + array( + 'locale_data' => array( + 'messages' => array( + '' => array( + 'domain' => 'messages', + 'lang' => 'es', + ), + 'World' => array( 'Mundo' ), + ), + ), + ) + ); + }, + 10, + 3 + ); + + $filtered = array(); + add_filter( + 'load_script_textdomain_relative_path', + static function ( $relative, $src, $is_module ) use ( &$filtered ) { + $filtered[] = compact( 'relative', 'src', 'is_module' ); + return $relative; + }, + 10, + 3 + ); + + $output = get_echo( array( $this->script_modules, 'print_script_module_translations' ) ); + + // With auto-detection, the filter fires for every enqueued module and its dependencies. + $this->assertCount( 2, $filtered, 'load_script_textdomain_relative_path filter should fire for the enqueued module and its dependency.' ); + foreach ( $filtered as $filter_args ) { + $this->assertIsString( $filter_args['relative'], 'Filter should receive a string $relative argument.' ); + $this->assertIsString( $filter_args['src'], 'Filter should receive a string $src argument.' ); + $this->assertTrue( $filter_args['is_module'], 'Filter should receive $is_module=true for script modules.' ); + } + + $script_text = $this->get_inline_script_text( $output, 'wp-script-module-translation-data-dep-module' ); + $this->assertNotNull( $script_text, 'Dependency module translations should be printed.' ); + $this->assertStringContainsString( 'Mundo', $script_text, 'Output should contain the dependency translation.' ); + } + + /** + * Tests that set_translations() can override the auto-detected text domain. + * + * @ticket 65015 + * + * @covers WP_Script_Modules::print_script_module_translations + * @covers WP_Script_Modules::set_translations + */ + public function test_print_script_module_translations_respects_set_translations_override() { + $this->script_modules->register( 'test-module', '/wp-includes/js/test-module.js' ); + $this->script_modules->enqueue( 'test-module' ); + $this->script_modules->set_translations( 'test-module', 'my-plugin' ); + + $seen_domain = null; + add_filter( + 'pre_load_script_translations', + static function ( $translations, $file, $handle, $domain ) use ( &$seen_domain ) { + if ( 'test-module' !== $handle ) { + return $translations; + } + $seen_domain = $domain; + return wp_json_encode( + array( + 'locale_data' => array( + 'messages' => array( + '' => array( + 'domain' => 'my-plugin', + 'lang' => 'es', + ), + 'Hello' => array( 'Hola' ), + ), + ), + ) + ); + }, + 10, + 4 + ); + + $output = get_echo( array( $this->script_modules, 'print_script_module_translations' ) ); + + $this->assertSame( 'my-plugin', $seen_domain, 'load_script_module_textdomain() should be called with the overridden domain.' ); + $this->assertStringContainsString( 'Hola', $output, 'Output should contain the translated string loaded under the overridden domain.' ); + } +} diff --git a/tests/phpunit/tests/shortcode.php b/tests/phpunit/tests/shortcode.php index ce0b3f858db56..7467d1ed7e6a3 100644 --- a/tests/phpunit/tests/shortcode.php +++ b/tests/phpunit/tests/shortcode.php @@ -6,6 +6,14 @@ class Tests_Shortcode extends WP_UnitTestCase { protected $shortcodes = array( 'test-shortcode-tag', 'footag', 'bartag', 'baztag', 'dumptag', 'hyphen', 'hyphen-foo', 'hyphen-foo-bar', 'url', 'img' ); + private $atts = null; + private $content = null; + private $tagname = null; + + private $filter_atts_out = null; + private $filter_atts_pairs = null; + private $filter_atts_atts = null; + public function set_up() { parent::set_up(); @@ -13,10 +21,12 @@ public function set_up() { add_shortcode( $shortcode, array( $this, 'shortcode_' . str_replace( '-', '_', $shortcode ) ) ); } - $this->atts = null; - $this->content = null; - $this->tagname = null; - + $this->atts = null; + $this->content = null; + $this->tagname = null; + $this->filter_atts_out = null; + $this->filter_atts_pairs = null; + $this->filter_atts_atts = null; } public function tear_down() { @@ -38,7 +48,7 @@ public function shortcode_test_shortcode_tag( $atts, $content = null, $tagname = // [footag foo="bar"] public function shortcode_footag( $atts ) { - $foo = isset( $atts['foo'] ) ? $atts['foo'] : ''; + $foo = $atts['foo'] ?? ''; return "foo = $foo"; } @@ -95,9 +105,13 @@ public function shortcode_img( $atts ) { return $out; } + /** + * @ticket 59249 + */ public function test_noatts() { do_shortcode( '[test-shortcode-tag /]' ); - $this->assertSame( '', $this->atts ); + $this->assertIsArray( $this->atts ); + $this->assertEmpty( $this->atts ); $this->assertSame( 'test-shortcode-tag', $this->tagname ); } @@ -171,9 +185,13 @@ public function test_two_atts() { $this->assertSame( 'test-shortcode-tag', $this->tagname ); } + /** + * @ticket 59249 + */ public function test_noatts_enclosing() { do_shortcode( '[test-shortcode-tag]content[/test-shortcode-tag]' ); - $this->assertSame( '', $this->atts ); + $this->assertIsArray( $this->atts ); + $this->assertEmpty( $this->atts ); $this->assertSame( 'content', $this->content ); $this->assertSame( 'test-shortcode-tag', $this->tagname ); } @@ -198,10 +216,14 @@ public function test_two_atts_enclosing() { $this->assertSame( 'test-shortcode-tag', $this->tagname ); } + /** + * @ticket 59249 + */ public function test_unclosed() { $out = do_shortcode( '[test-shortcode-tag]' ); $this->assertSame( '', $out ); - $this->assertSame( '', $this->atts ); + $this->assertIsArray( $this->atts ); + $this->assertEmpty( $this->atts ); $this->assertSame( 'test-shortcode-tag', $this->tagname ); } @@ -394,7 +416,19 @@ public function test_shortcode_unautop() { $this->assertSame( $test_string, shortcode_unautop( wpautop( $test_string ) ) ); } - public function data_test_strip_shortcodes() { + /** + * @ticket 10326 + * + * @dataProvider data_strip_shortcodes + * + * @param string $expected Expected output. + * @param string $content Content to run strip_shortcodes() on. + */ + public function test_strip_shortcodes( $expected, $content ) { + $this->assertSame( $expected, strip_shortcodes( $content ) ); + } + + public function data_strip_shortcodes() { return array( array( 'before', 'before[gallery]' ), array( 'after', '[gallery]after' ), @@ -409,18 +443,6 @@ public function data_test_strip_shortcodes() { ); } - /** - * @ticket 10326 - * - * @dataProvider data_test_strip_shortcodes - * - * @param string $expected Expected output. - * @param string $content Content to run strip_shortcodes() on. - */ - public function test_strip_shortcodes( $expected, $content ) { - $this->assertSame( $expected, strip_shortcodes( $content ) ); - } - /** * @ticket 37767 */ @@ -671,10 +693,11 @@ public function test_registration_good( $input, $expected ) { private function sub_registration( $input, $expected ) { add_shortcode( $input, '' ); $actual = shortcode_exists( $input ); - $this->assertSame( $expected, $actual ); if ( $actual ) { remove_shortcode( $input ); } + + $this->assertSame( $expected, $actual ); } public function data_registration_bad() { @@ -771,7 +794,6 @@ public function test_php_and_js_shortcode_attribute_regexes_match() { $js = str_replace( "\'", "'", $matches[1] ); $this->assertSame( $php, $js ); - } /** @@ -816,7 +838,7 @@ public function test_pre_do_shortcode_tag() { // Pass arguments. $arr = array( - 'return' => 'p11', + 'output' => 'p11', 'key' => $str, 'atts' => array( 'a' => 'b', @@ -854,9 +876,9 @@ public function filter_pre_do_shortcode_tag_p11() { return 'p11'; } - public function filter_pre_do_shortcode_tag_attr( $return, $key, $atts, $m ) { + public function filter_pre_do_shortcode_tag_attr( $output, $key, $atts, $m ) { $arr = array( - 'return' => $return, + 'output' => $output, 'key' => $key, 'atts' => $atts, 'm' => $m, @@ -886,7 +908,7 @@ public function test_do_shortcode_tag_filter() { // Pass arguments. $arr = array( - 'return' => 'foobar', + 'output' => 'foobar', 'key' => $str, 'atts' => array( 'a' => 'b', @@ -916,17 +938,17 @@ public function shortcode_do_shortcode_tag( $atts = array(), $content = '' ) { return 'foo'; } - public function filter_do_shortcode_tag_replace( $return ) { - return str_replace( 'oo', 'ee', $return ); + public function filter_do_shortcode_tag_replace( $output ) { + return str_replace( 'oo', 'ee', $output ); } - public function filter_do_shortcode_tag_generate( $return ) { + public function filter_do_shortcode_tag_generate( $output ) { return 'foobar'; } - public function filter_do_shortcode_tag_attr( $return, $key, $atts, $m ) { + public function filter_do_shortcode_tag_attr( $output, $key, $atts, $m ) { $arr = array( - 'return' => $return, + 'output' => $output, 'key' => $key, 'atts' => $atts, 'm' => $m, @@ -989,4 +1011,13 @@ public function test_positional_atts_mixed_quotes() { ); $this->assertSame( 'test-shortcode-tag', $this->tagname ); } + + /** + * @ticket 59249 + */ + public function test_shortcode_parse_atts_empty() { + $out = shortcode_parse_atts( '' ); + $this->assertIsArray( $out, 'Return value is not an array' ); + $this->assertEmpty( $out, 'Returned array is not empty' ); + } } diff --git a/tests/phpunit/tests/site-health.php b/tests/phpunit/tests/site-health.php deleted file mode 100644 index 956160361ac4b..0000000000000 --- a/tests/phpunit/tests/site-health.php +++ /dev/null @@ -1,110 +0,0 @@ -get_test_scheduled_events(); - - $this->assertSame( 'critical', $cron_health['status'] ); - $this->assertSame( __( 'It was not possible to check your scheduled events' ), $cron_health['label'] ); - $this->assertWPError( $wp_site_health->has_late_cron() ); - $this->assertWPError( $wp_site_health->has_missed_cron() ); - } - - /** - * Ensure Site Health reports correctly cron job reports. - * - * @dataProvider data_cron_health_checks - * @ticket 47223 - */ - public function test_cron_health_checks( $times, $expected_status, $expected_label, $expected_late, $expected_missed ) { - $wp_site_health = new WP_Site_Health(); - - /* - * Clear the cron array. - * - * The core jobs may register as late/missed in the test suite as they - * are not run. Clearing the array ensures the site health tests are only - * reported based on the jobs set in the test. - */ - _set_cron_array( array() ); - $times = (array) $times; - foreach ( $times as $job => $time ) { - $timestamp = strtotime( $time ); - wp_schedule_event( $timestamp, 'daily', __FUNCTION__ . "_{$job}" ); - } - - $cron_health = $wp_site_health->get_test_scheduled_events(); - - $this->assertSame( $expected_status, $cron_health['status'] ); - $this->assertSame( $expected_label, $cron_health['label'] ); - $this->assertSame( $expected_late, $wp_site_health->has_late_cron() ); - $this->assertSame( $expected_missed, $wp_site_health->has_missed_cron() ); - } - - /** - * Data provider for Site Health cron reports. - * - * The test suite runs with `DISABLE_WP_CRON === true` so the - * missed and late tests need to account for the extended periods - * allowed for with this flag enabled. - * - * 1. string|array Times to schedule (run through strtotime()) - * 2. string Expected status - * 3. string Expected label - * 4. bool Expected outcome has_late_cron() - * 5. bool Expected outcome has_missed_cron() - */ - public function data_cron_health_checks() { - return array( - array( - '+5 minutes', - 'good', - __( 'Scheduled events are running' ), - false, - false, - ), - array( - '-50 minutes', - 'recommended', - __( 'A scheduled event is late' ), - true, - false, - ), - array( - '-500 minutes', - 'recommended', - __( 'A scheduled event has failed' ), - false, - true, - ), - array( - array( - '-50 minutes', - '-500 minutes', - ), - 'recommended', - __( 'A scheduled event has failed' ), - true, - true, - ), - ); - } -} diff --git a/tests/phpunit/tests/sitemaps/functions.php b/tests/phpunit/tests/sitemaps/functions.php index f67335733bffb..0620d465d4795 100644 --- a/tests/phpunit/tests/sitemaps/functions.php +++ b/tests/phpunit/tests/sitemaps/functions.php @@ -63,7 +63,7 @@ public function test_wp_get_sitemap_providers() { /** * Test get_sitemap_url() with plain permalinks. * - * @dataProvider plain_permalinks_provider + * @dataProvider data_get_sitemap_url_plain_permalinks */ public function test_get_sitemap_url_plain_permalinks( $name, $subtype_name, $page, $expected ) { $actual = get_sitemap_url( $name, $subtype_name, $page ); @@ -74,7 +74,7 @@ public function test_get_sitemap_url_plain_permalinks( $name, $subtype_name, $pa /** * Test get_sitemap_url() with pretty permalinks. * - * @dataProvider pretty_permalinks_provider + * @dataProvider data_get_sitemap_url_pretty_permalinks */ public function test_get_sitemap_url_pretty_permalinks( $name, $subtype_name, $page, $expected ) { $this->set_permalink_structure( '/%postname%/' ); @@ -96,7 +96,7 @@ public function test_get_sitemap_url_pretty_permalinks( $name, $subtype_name, $p * @type string|false $4 Sitemap URL. * } */ - public function plain_permalinks_provider() { + public function data_get_sitemap_url_plain_permalinks() { return array( array( 'posts', 'post', 1, home_url( '/?sitemap=posts&sitemap-subtype=post&paged=1' ) ), array( 'posts', 'post', 0, home_url( '/?sitemap=posts&sitemap-subtype=post&paged=1' ) ), @@ -128,7 +128,7 @@ public function plain_permalinks_provider() { * @type string|false $4 Sitemap URL. * } */ - public function pretty_permalinks_provider() { + public function data_get_sitemap_url_pretty_permalinks() { return array( array( 'posts', 'post', 1, home_url( '/wp-sitemap-posts-post-1.xml' ) ), array( 'posts', 'post', 0, home_url( '/wp-sitemap-posts-post-1.xml' ) ), diff --git a/tests/phpunit/tests/sitemaps/sitemaps.php b/tests/phpunit/tests/sitemaps/sitemaps.php index a4fa6b14d74c2..85f9965245842 100644 --- a/tests/phpunit/tests/sitemaps/sitemaps.php +++ b/tests/phpunit/tests/sitemaps/sitemaps.php @@ -95,7 +95,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { /** * Helper function to get all sitemap entries data. * - * @return array A list of sitemap entires. + * @return array A list of sitemap entries. */ public function _get_sitemap_entries() { $entries = array(); @@ -251,13 +251,16 @@ public function test_get_url_list_page_with_home() { $post_list = $providers['posts']->get_url_list( 1, 'page' ); + $post_list_sorted = wp_list_sort( $post_list, 'lastmod', 'DESC' ); + $expected = $this->_get_expected_url_list( 'page', self::$pages ); // Add the homepage to the front of the URL list. array_unshift( $expected, array( - 'loc' => home_url( '/' ), + 'loc' => home_url( '/' ), + 'lastmod' => $post_list_sorted[0]['lastmod'], ) ); @@ -378,7 +381,8 @@ public function _get_expected_url_list( $type, $ids ) { return array_map( static function ( $post ) { return array( - 'loc' => get_permalink( $post ), + 'loc' => get_permalink( $post ), + 'lastmod' => get_post_modified_time( DATE_W3C, true, $post ), ); }, $posts diff --git a/tests/phpunit/tests/sitemaps/wpSitemapsPosts.php b/tests/phpunit/tests/sitemaps/wpSitemapsPosts.php index b60903e18a712..70b2fbc250cc3 100644 --- a/tests/phpunit/tests/sitemaps/wpSitemapsPosts.php +++ b/tests/phpunit/tests/sitemaps/wpSitemapsPosts.php @@ -29,7 +29,7 @@ public function test_get_sitemap_entries_homepage() { } /** - * Test ability to filter object subtypes. + * Tests ability to filter object subtypes. */ public function test_filter_sitemaps_post_types() { $posts_provider = new WP_Sitemaps_Posts(); @@ -42,7 +42,7 @@ public function test_filter_sitemaps_post_types() { } /** - * Test `wp_sitemaps_posts_show_on_front_entry` filter. + * Tests `wp_sitemaps_posts_show_on_front_entry` filter. */ public function test_posts_show_on_front_entry() { $posts_provider = new WP_Sitemaps_Posts(); @@ -59,15 +59,53 @@ public function test_posts_show_on_front_entry() { $url_list = $posts_provider->get_url_list( 1, 'page' ); $sitemap_entry = array_shift( $url_list ); - $this->assertArrayHasKey( 'lastmod', $sitemap_entry ); + $this->assertEqualSetsWithIndex( + array( + 'loc' => home_url( '/' ), + 'lastmod' => '2000-01-01', + ), + $sitemap_entry + ); } /** * Callback for 'wp_sitemaps_posts_show_on_front_entry' filter. */ public function _show_on_front_entry( $sitemap_entry ) { - $sitemap_entry['lastmod'] = wp_date( DATE_W3C, time() ); + $sitemap_entry['lastmod'] = '2000-01-01'; return $sitemap_entry; } + + /** + * Tests that sticky posts are not moved to the front of the first page of the post sitemap. + * + * @ticket 55633 + */ + public function test_posts_sticky_posts_not_moved_to_front() { + $factory = self::factory(); + + // Create 4 posts, and stick the last one. + $post_ids = $factory->post->create_many( 4 ); + $last_post_id = end( $post_ids ); + stick_post( $last_post_id ); + + $posts_provider = new WP_Sitemaps_Posts(); + + $url_list = $posts_provider->get_url_list( 1, 'post' ); + + $this->assertCount( count( $post_ids ), $url_list, 'The post count did not match.' ); + + $expected = array(); + + foreach ( $post_ids as $post_id ) { + $expected[] = array( + 'loc' => home_url( "?p={$post_id}" ), + 'lastmod' => get_post_modified_time( DATE_W3C, true, $post_id ), + ); + } + + // Check that the URL list is still in the order of the post IDs (i.e., sticky post wasn't moved to the front). + $this->assertSame( $expected, $url_list, 'The post order did not match.' ); + } } diff --git a/tests/phpunit/tests/sitemaps/wpSitemapsRegistry.php b/tests/phpunit/tests/sitemaps/wpSitemapsRegistry.php index 8032a38dcce58..51d9c19d6c018 100644 --- a/tests/phpunit/tests/sitemaps/wpSitemapsRegistry.php +++ b/tests/phpunit/tests/sitemaps/wpSitemapsRegistry.php @@ -31,4 +31,40 @@ public function test_add_provider_prevent_duplicates() { $this->assertCount( 1, $providers ); $this->assertSame( $providers['foo'], $provider1, 'Can not confirm sitemap registration is working.' ); } + + /** + * Tests that `WP_Sitemaps_Registry::get_provider()` returns `null` when + * the `$name` argument is not a string. + * + * @ticket 56336 + * + * @covers WP_Sitemaps_Registry::get_provider + * + * @dataProvider data_get_provider_should_return_null_with_non_string_name + * + * @param mixed $name The non-string name. + */ + public function test_get_provider_should_return_null_with_non_string_name( $name ) { + $registry = new WP_Sitemaps_Registry(); + $this->assertNull( $registry->get_provider( $name ) ); + } + + /** + * Data provider with non-string values. + * + * @return array + */ + public function data_get_provider_should_return_null_with_non_string_name() { + return array( + 'array' => array( array() ), + 'object' => array( new stdClass() ), + 'bool (true)' => array( true ), + 'bool (false)' => array( false ), + 'null' => array( null ), + 'integer (0)' => array( 0 ), + 'integer (1)' => array( 1 ), + 'float (0.0)' => array( 0.0 ), + 'float (1.1)' => array( 1.1 ), + ); + } } diff --git a/tests/phpunit/tests/sitemaps/wpSitemapsUsers.php b/tests/phpunit/tests/sitemaps/wpSitemapsUsers.php index d52e191835bcd..eae9c03b27adf 100644 --- a/tests/phpunit/tests/sitemaps/wpSitemapsUsers.php +++ b/tests/phpunit/tests/sitemaps/wpSitemapsUsers.php @@ -2,6 +2,8 @@ /** * @group sitemaps + * + * @coversDefaultClass WP_Sitemaps_Users */ class Tests_Sitemaps_wpSitemapsUsers extends WP_UnitTestCase { @@ -10,14 +12,14 @@ class Tests_Sitemaps_wpSitemapsUsers extends WP_UnitTestCase { * * @var array */ - public static $users; + private static $users; /** * Editor ID for use in some tests. * * @var int */ - public static $editor_id; + private static $editor_id; /** * Set up fixtures. @@ -32,6 +34,8 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { /** * Test getting a URL list for a users sitemap page via * WP_Sitemaps_Users::get_url_list(). + * + * @covers ::get_url_list */ public function test_get_url_list_users() { // Set up the user to an editor to assign posts to other users. @@ -40,7 +44,7 @@ public function test_get_url_list_users() { // Create a set of posts for each user and generate the expected URL list data. $expected = array_map( static function ( $user_id ) { - $post = self::factory()->post->create_and_get( array( 'post_author' => $user_id ) ); + self::factory()->post->create( array( 'post_author' => $user_id ) ); return array( 'loc' => get_author_posts_url( $user_id ), @@ -55,4 +59,34 @@ static function ( $user_id ) { $this->assertSameSets( $expected, $url_list ); } + + /** + * @covers ::get_url_list + * @covers ::get_users_query_args + */ + public function test_get_url_list_skips_users_with_only_attachments_and_pages() { + // Set up the user to an editor to assign posts to other users. + wp_set_current_user( self::$editor_id ); + + foreach ( self::$users as $user_id ) { + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'attachment', + ) + ); + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'page', + ) + ); + } + + $user_provider = new WP_Sitemaps_Users(); + + $url_list = $user_provider->get_url_list( 1 ); + + $this->assertEmpty( $url_list ); + } } diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php new file mode 100644 index 0000000000000..a7b5594bdf0d1 --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRules.php @@ -0,0 +1,561 @@ + 'prefetch', + 'eagerness' => 'conservative', + ); + private $prerender_config = array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ); + + public function set_up() { + parent::set_up(); + + add_filter( + 'template_directory_uri', + static function () { + return content_url( 'themes/template' ); + } + ); + + add_filter( + 'stylesheet_directory_uri', + static function () { + return content_url( 'themes/stylesheet' ); + } + ); + + update_option( 'permalink_structure', '/%year%/%monthnum%/%day%/%postname%/' ); + } + + /** + * Tests speculation rules output with prefetch for the different eagerness levels. + * + * @ticket 62503 + * @dataProvider data_eagerness + */ + public function test_wp_get_speculation_rules_with_prefetch( string $eagerness ) { + remove_all_filters( 'wp_speculation_rules_configuration' ); + add_filter( + 'wp_speculation_rules_configuration', + static function () use ( $eagerness ) { + return array( + 'mode' => 'prefetch', + 'eagerness' => $eagerness, + ); + } + ); + + $rules = wp_get_speculation_rules(); + + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertIsArray( $rules['prefetch'] ); + foreach ( $rules['prefetch'] as $entry ) { + $this->assertIsArray( $entry ); + $this->assertArrayHasKey( 'source', $entry ); + $this->assertSame( 'document', $entry['source'] ); + $this->assertArrayHasKey( 'eagerness', $entry ); + $this->assertSame( $eagerness, $entry['eagerness'] ); + } + } + + /** + * Tests speculation rules output with prerender for the different eagerness levels. + * + * @ticket 62503 + * @dataProvider data_eagerness + */ + public function test_wp_get_speculation_rules_with_prerender( string $eagerness ) { + remove_all_filters( 'wp_speculation_rules_configuration' ); + add_filter( + 'wp_speculation_rules_configuration', + static function () use ( $eagerness ) { + return array( + 'mode' => 'prerender', + 'eagerness' => $eagerness, + ); + } + ); + + $rules = wp_get_speculation_rules(); + + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prerender', $rules ); + $this->assertIsArray( $rules['prerender'] ); + foreach ( $rules['prerender'] as $entry ) { + $this->assertIsArray( $entry ); + $this->assertArrayHasKey( 'source', $entry ); + $this->assertSame( 'document', $entry['source'] ); + $this->assertArrayHasKey( 'eagerness', $entry ); + $this->assertSame( $eagerness, $entry['eagerness'] ); + } + } + + public static function data_eagerness(): array { + return array( + array( 'conservative' ), + array( 'moderate' ), + array( 'eager' ), + ); + } + + /** + * Tests that the number of entries included for prefetch configuration is correct. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_prefetch_entries() { + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prefetch_config; + } + ); + + $rules = wp_get_speculation_rules(); + + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertCount( 4, $rules['prefetch'][0]['where']['and'] ); + $this->assertArrayHasKey( 'not', $rules['prefetch'][0]['where']['and'][3] ); + $this->assertArrayHasKey( 'selector_matches', $rules['prefetch'][0]['where']['and'][3]['not'] ); + $this->assertSame( '.no-prefetch, .no-prefetch a', $rules['prefetch'][0]['where']['and'][3]['not']['selector_matches'] ); + } + + /** + * Tests that the number of entries included for prerender configuration is correct. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_prerender_entries() { + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prerender_config; + } + ); + + $rules = wp_get_speculation_rules(); + + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prerender', $rules ); + $this->assertCount( 5, $rules['prerender'][0]['where']['and'] ); + $this->assertArrayHasKey( 'not', $rules['prerender'][0]['where']['and'][3] ); + $this->assertArrayHasKey( 'selector_matches', $rules['prerender'][0]['where']['and'][3]['not'] ); + $this->assertSame( '.no-prerender, .no-prerender a', $rules['prerender'][0]['where']['and'][3]['not']['selector_matches'] ); + $this->assertArrayHasKey( 'not', $rules['prerender'][0]['where']['and'][4] ); + $this->assertArrayHasKey( 'selector_matches', $rules['prerender'][0]['where']['and'][4]['not'] ); + $this->assertSame( '.no-prefetch, .no-prefetch a', $rules['prerender'][0]['where']['and'][4]['not']['selector_matches'] ); + } + + /** + * Tests the default exclude paths and ensures they cannot be altered via filter. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_href_exclude_paths() { + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prefetch_config; + } + ); + + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prefetch'][0]['where']['and'][1]['not']['href_matches']; + + $this->assertSameSets( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + + // Add filter that attempts to replace base exclude paths with a custom path to exclude. + add_filter( + 'wp_speculation_rules_href_exclude_paths', + static function () { + return array( 'custom-file.php' ); + } + ); + + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prefetch'][0]['where']['and'][1]['not']['href_matches']; + + // Ensure the base exclude paths are still present and that the custom path was formatted correctly. + $this->assertSameSets( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + '/custom-file.php', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + + /** + * Tests the default exclude paths and ensures they cannot be altered via filter. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_href_exclude_paths_without_pretty_permalinks() { + update_option( 'permalink_structure', '' ); + + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prefetch_config; + } + ); + + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prefetch'][0]['where']['and'][1]['not']['href_matches']; + + $this->assertSameSets( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?*(^|&)*nonce*=*', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + + /** + * Tests that exclude paths can be altered specifically based on the mode used. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_href_exclude_paths_with_mode() { + // Add filter that adds an exclusion only if the mode is 'prerender'. + add_filter( + 'wp_speculation_rules_href_exclude_paths', + static function ( $exclude_paths, $mode ) { + if ( 'prerender' === $mode ) { + $exclude_paths[] = '/products/*'; + } + return $exclude_paths; + }, + 10, + 2 + ); + + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prerender_config; + } + ); + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prerender'][0]['where']['and'][1]['not']['href_matches']; + + // Ensure the additional exclusion is present because the mode is 'prerender'. + // Also ensure keys are sequential starting from 0 (that is, that array_is_list()). + $this->assertSame( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + '/products/*', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + + // Redo with 'prefetch'. + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prefetch_config; + } + ); + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prefetch'][0]['where']['and'][1]['not']['href_matches']; + + // Ensure the additional exclusion is not present because the mode is 'prefetch'. + $this->assertSame( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + + /** + * Tests filter that explicitly adds non-sequential keys. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_with_filtering_bad_keys() { + + add_filter( + 'wp_speculation_rules_href_exclude_paths', + static function ( array $exclude_paths ): array { + $exclude_paths[] = '/next/'; + array_unshift( $exclude_paths, '/unshifted/' ); + $exclude_paths[-1] = '/negative-one/'; + $exclude_paths[100] = '/one-hundred/'; + $exclude_paths['a'] = '/letter-a/'; + return $exclude_paths; + } + ); + + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prerender_config; + } + ); + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prerender'][0]['where']['and'][1]['not']['href_matches']; + $this->assertSame( + array( + '/wp-*.php', + '/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/*\\?(.+)', + '/unshifted/', + '/next/', + '/negative-one/', + '/one-hundred/', + '/letter-a/', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + + /** + * Tests scenario when the home_url and site_url have different paths. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_different_home_and_site_urls() { + add_filter( + 'site_url', + static function (): string { + return 'https://example.com/wp/'; + } + ); + add_filter( + 'home_url', + static function (): string { + return 'https://example.com/blog/'; + } + ); + add_filter( + 'wp_speculation_rules_href_exclude_paths', + static function ( array $exclude_paths ): array { + $exclude_paths[] = '/store/*'; + return $exclude_paths; + } + ); + + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prerender_config; + } + ); + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $href_exclude_paths = $rules['prerender'][0]['where']['and'][1]['not']['href_matches']; + $this->assertSame( + array( + '/wp/wp-*.php', + '/wp/wp-admin/*', + '/wp-content/uploads/*', + '/wp-content/*', + '/wp-content/plugins/*', + '/wp-content/themes/stylesheet/*', + '/wp-content/themes/template/*', + '/blog/*\\?(.+)', + '/blog/store/*', + ), + $href_exclude_paths, + 'Snapshot: ' . var_export( $href_exclude_paths, true ) + ); + } + + /** + * Tests that passing an invalid configuration to the function does not lead to unexpected problems. + * + * This is mostly an integration test as it is resolved as part of wp_get_speculation_rules_configuration(). + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_with_invalid_configuration() { + add_filter( + 'wp_speculation_rules_configuration', + static function () { + return array( + 'mode' => 'none', + 'eagerness' => 'none', + ); + } + ); + $rules = wp_get_speculation_rules(); + + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertSame( 'conservative', $rules['prefetch'][0]['eagerness'] ); + } + + /** + * Tests that passing no configuration (`null`) results in no speculation rules being returned. + * + * This is used to effectively disable the feature. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_with_null() { + add_filter( 'wp_speculation_rules_configuration', '__return_null' ); + + $rules = wp_get_speculation_rules(); + $this->assertNull( $rules ); + } + + /** + * Tests that the 'wp_load_speculation_rules' action allows providing additional rules. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_with_additional_rules() { + $filtered_obj = null; + add_action( + 'wp_load_speculation_rules', + static function ( $speculation_rules ) use ( &$filtered_obj ) { + $filtered_obj = $speculation_rules; + + /* + * In practice, these rules would ensure that links marked with the classes would be opt in to + * prerendering with moderate and eager eagerness respectively. + */ + $speculation_rules->add_rule( + 'prerender', + 'prerender-moderate-marked-links', + array( + 'source' => 'document', + 'where' => array( + 'selector_matches' => '.moderate-prerender, .moderate-prerender a', + ), + 'eagerness' => 'moderate', + ) + ); + $speculation_rules->add_rule( + 'prerender', + 'prerender-eager-marked-links', + array( + 'source' => 'document', + 'where' => array( + 'selector_matches' => '.eager-prerender, .eager-prerender a', + ), + 'eagerness' => 'eager', + ) + ); + } + ); + + add_filter( + 'wp_speculation_rules_configuration', + function () { + return $this->prefetch_config; + } + ); + $rules = wp_get_speculation_rules(); + $this->assertInstanceOf( WP_Speculation_Rules::class, $rules ); + $this->assertSame( $filtered_obj, $rules ); + + $rules = $rules->jsonSerialize(); + + $this->assertArrayHasKey( 'prefetch', $rules ); + $this->assertCount( 1, $rules['prefetch'] ); + $this->assertArrayHasKey( 'prerender', $rules ); + $this->assertCount( 2, $rules['prerender'] ); + $this->assertSame( 'conservative', $rules['prefetch'][0]['eagerness'] ); + $this->assertSame( 'moderate', $rules['prerender'][0]['eagerness'] ); + $this->assertSame( 'eager', $rules['prerender'][1]['eagerness'] ); + } +} diff --git a/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php new file mode 100644 index 0000000000000..a4eb9c8a6878b --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpGetSpeculationRulesConfiguration.php @@ -0,0 +1,267 @@ +assertSame( + array( + 'mode' => 'auto', + 'eagerness' => 'auto', + ), + $filter_default + ); + $this->assertSame( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + $config_default + ); + } + + /** + * Tests that the speculative loading is disabled by default when not using pretty permalinks. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_configuration_without_pretty_permalinks() { + update_option( 'permalink_structure', '' ); + $this->assertNull( wp_get_speculation_rules_configuration() ); + } + + /** + * Tests that the speculative loading is disabled by default for logged-in users. + * + * @ticket 62503 + */ + public function test_wp_get_speculation_rules_configuration_with_logged_in_user() { + wp_set_current_user( self::factory()->user->create( array( 'role' => 'administrator' ) ) ); + $this->assertNull( wp_get_speculation_rules_configuration() ); + } + + /** + * Tests that the configuration can be filtered and leads to the expected results. + * + * @ticket 62503 + * @dataProvider data_wp_get_speculation_rules_configuration_filter + */ + public function test_wp_get_speculation_rules_configuration_filter( $filter_value, $expected ) { + add_filter( + 'wp_speculation_rules_configuration', + function () use ( $filter_value ) { + return $filter_value; + } + ); + + $this->assertSame( $expected, wp_get_speculation_rules_configuration() ); + } + + public static function data_wp_get_speculation_rules_configuration_filter(): array { + return array( + 'conservative prefetch' => array( + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'moderate prefetch' => array( + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'moderate', + ), + ), + 'eager prefetch' => array( + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + ), + 'conservative prerender' => array( + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + 'moderate prerender' => array( + array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ), + ), + 'eager prerender' => array( + array( + 'mode' => 'prerender', + 'eagerness' => 'eager', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'eager', + ), + ), + 'auto' => array( + array( + 'mode' => 'auto', + 'eagerness' => 'auto', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'auto mode only' => array( + array( + 'mode' => 'auto', + 'eagerness' => 'eager', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + ), + 'auto eagerness only' => array( + array( + 'mode' => 'prerender', + 'eagerness' => 'auto', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + // 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules. + 'immediate eagerness' => array( + array( + 'mode' => 'auto', + 'eagerness' => 'immediate', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'null' => array( + null, + null, + ), + 'false' => array( + false, + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'true' => array( + true, + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'missing mode' => array( + array( + 'eagerness' => 'eager', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + ), + 'missing eagerness' => array( + array( + 'mode' => 'prerender', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + 'empty array' => array( + array(), + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + 'invalid mode' => array( + array( + 'mode' => 'invalid', + 'eagerness' => 'eager', + ), + array( + 'mode' => 'prefetch', + 'eagerness' => 'eager', + ), + ), + 'invalid eagerness' => array( + array( + 'mode' => 'prerender', + 'eagerness' => 'invalid', + ), + array( + 'mode' => 'prerender', + 'eagerness' => 'conservative', + ), + ), + 'invalid type' => array( + 42, + array( + 'mode' => 'prefetch', + 'eagerness' => 'conservative', + ), + ), + ); + } +} diff --git a/tests/phpunit/tests/speculative-loading/wpPrintSpeculationRules.php b/tests/phpunit/tests/speculative-loading/wpPrintSpeculationRules.php new file mode 100644 index 0000000000000..e2887f8b0b6ce --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpPrintSpeculationRules.php @@ -0,0 +1,89 @@ +original_wp_theme_features = $GLOBALS['_wp_theme_features']; + } + + public function tear_down() { + $GLOBALS['_wp_theme_features'] = $this->original_wp_theme_features; + parent::tear_down(); + } + + /** + * Tests that the hook for printing speculation rules is set up. + * + * @ticket 62503 + */ + public function test_hook() { + $this->assertSame( 10, has_action( 'wp_footer', 'wp_print_speculation_rules' ) ); + } + + /** + * Tests speculation rules script output with HTML5 support. + * + * @ticket 62503 + */ + public function test_wp_print_speculation_rules_with_html5_support() { + add_theme_support( 'html5', array( 'script' ) ); + + add_filter( + 'wp_speculation_rules_configuration', + static function () { + return array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ); + } + ); + + $output = get_echo( 'wp_print_speculation_rules' ); + $this->assertStringContainsString( '' ), '', $output ); + $rules = json_decode( $json, true ); + $this->assertIsArray( $rules ); + $this->assertArrayHasKey( 'prerender', $rules ); + } + + /** + * Tests speculation rules script output without HTML5 support. + * + * @ticket 62503 + */ + public function test_wp_print_speculation_rules_without_html5_support() { + remove_theme_support( 'html5' ); + + add_filter( + 'wp_speculation_rules_configuration', + static function () { + return array( + 'mode' => 'prerender', + 'eagerness' => 'moderate', + ); + } + ); + + $output = get_echo( 'wp_print_speculation_rules' ); + $this->assertStringContainsString( '' ), '', $output ); + $rules = json_decode( $json, true ); + $this->assertIsArray( $rules ); + $this->assertArrayHasKey( 'prerender', $rules ); + } +} diff --git a/tests/phpunit/tests/speculative-loading/wpSpeculationRules.php b/tests/phpunit/tests/speculative-loading/wpSpeculationRules.php new file mode 100644 index 0000000000000..9a59583f3f20c --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpSpeculationRules.php @@ -0,0 +1,369 @@ +setExpectedIncorrectUsage( 'WP_Speculation_Rules::add_rule' ); + } + + $result = $speculation_rules->add_rule( $mode, $id, $rule ); + if ( $expected ) { + $this->assertTrue( $result ); + } else { + $this->assertFalse( $result ); + } + } + + /** + * Tests that adding a speculation rule with a duplicate ID results in the expected behavior. + * + * @ticket 62503 + * @covers ::add_rule + */ + public function test_add_rule_with_duplicate() { + $speculation_rules = new WP_Speculation_Rules(); + + $this->assertTrue( $speculation_rules->add_rule( 'prerender', 'my-custom-rule', array( 'where' => array( 'href_matches' => '/*' ) ) ) ); + + // It should be possible to add a rule of the same ID for another mode. + $this->assertTrue( $speculation_rules->add_rule( 'prefetch', 'my-custom-rule', array( 'where' => array( 'href_matches' => '/*' ) ) ) ); + + // But it should not be possible to add a rule of the same ID to a mode where it's already present. + $this->setExpectedIncorrectUsage( 'WP_Speculation_Rules::add_rule' ); + $this->assertFalse( $speculation_rules->add_rule( 'prerender', 'my-custom-rule', array( 'urls' => array( 'https://important-url.com/' ) ) ) ); + } + + public static function data_add_rule(): array { + return array( + 'basic-prefetch' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + true, + ), + 'basic-prefetch-no-source' => array( + 'prefetch', + 'test-rule-2', + array( + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + true, + ), + 'basic-prefetch-no-eagerness' => array( + 'prefetch', + 'test-rule-3', + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + ), + true, + ), + 'basic-prerender' => array( + 'prerender', + 'test-rule-1', + array( + 'source' => 'list', + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + 'eagerness' => 'eager', + ), + true, + ), + 'basic-prerender-no-source' => array( + 'prerender', + 'test-rule-2', + array( + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + 'eagerness' => 'eager', + ), + true, + ), + 'basic-prerender-no-eagerness' => array( + 'prerender', + 'test-rule-3', + array( + 'source' => 'list', + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + ), + true, + ), + 'invalid-mode' => array( + 'load-fast', // Only 'prefetch' and 'prerender' are allowed. + 'test-rule-1', + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + false, + ), + 'invalid-id-characters' => array( + 'prefetch', + 'test rule 1', // Spaces are not allowed. + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + false, + ), + 'invalid-id-start' => array( + 'prefetch', + '1_test_rule', // The first character must be a lower-case letter. + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + false, + ), + 'invalid-source' => array( + 'prerender', + 'test-rule-1', + array( + 'source' => 'magic', // Only 'list' and 'document' are allowed. + 'where' => array( 'selector_matches' => '.prerender' ), + 'eagerness' => 'eager', + ), + false, + ), + 'missing-keys' => array( + 'prefetch', + 'test-rule-1', + array(), // The minimum requirements are presence of either a 'where' or 'urls' key. + false, + ), + 'conflicting-keys' => array( + 'prefetch', + 'test-rule-1', + array( // Only 'where' or 'urls' is allowed, but not both. + 'where' => array( 'selector_matches' => '.prefetch' ), + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + ), + false, + ), + 'conflicting-list-source' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'list', // Source 'list' can only be used with key 'urls', but not 'where'. + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'eager', + ), + false, + ), + 'conflicting-document-source' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'document', // Source 'document' can only be used with key 'where', but not 'urls'. + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + 'eagerness' => 'eager', + ), + false, + ), + 'invalid-eagerness' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'fast', // Only 'immediate', 'eager, 'moderate', and 'conservative' are allowed. + ), + false, + ), + 'immediate-eagerness-list' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'list', + 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ), + 'eagerness' => 'immediate', + ), + true, + ), + // 'immediate' is a valid eagerness, but for safety WordPress does not allow it for document-level rules. + 'immediate-eagerness-document' => array( + 'prefetch', + 'test-rule-1', + array( + 'source' => 'document', + 'where' => array( 'selector_matches' => '.prefetch' ), + 'eagerness' => 'immediate', + ), + false, + ), + ); + } + + /** + * Tests that checking for existence of a rule works as expected. + * + * @ticket 62503 + * @covers ::has_rule + */ + public function test_has_rule() { + $speculation_rules = new WP_Speculation_Rules(); + + $this->assertFalse( $speculation_rules->has_rule( 'prerender', 'my-custom-rule' ), 'Custom rule should not be marked as present before it is added' ); + + $speculation_rules->add_rule( 'prerender', 'my-custom-rule', array( 'urls' => array( 'https://url-to-prerender.com/' ) ) ); + $this->assertTrue( $speculation_rules->has_rule( 'prerender', 'my-custom-rule' ), 'Custom rule should be marked as present after it has been added' ); + $this->assertFalse( $speculation_rules->has_rule( 'prefetch', 'my-custom-rule' ), 'Custom rule should not be marked as present for different mode even after it has been added' ); + } + + /** + * Tests that transforming a speculation rules object into JSON-encodable data works as expected. + * + * @ticket 62503 + * @covers ::jsonSerialize + */ + public function test_jsonSerialize() { + $prefetch_rule_1 = array( 'where' => array( 'href_matches' => '/*' ) ); + $prefetch_rule_2 = array( 'where' => array( 'selector_matches' => '.prefetch-opt-in' ) ); + $prerender_rule_1 = array( 'urls' => array( 'https://example.org/high-priority-url/', 'https://example.org/another-high-priority-url/' ) ); + $prerender_rule_2 = array( + 'where' => array( + 'or' => array( + array( 'selector_matches' => '.prerender-opt-in' ), + array( 'selector_matches' => '.prerender-fast' ), + ), + ), + 'eagerness' => 'moderate', + ); + + $speculation_rules = new WP_Speculation_Rules(); + $this->assertSame( array(), $speculation_rules->jsonSerialize(), 'Speculation rules JSON data should be empty before adding any rules' ); + + $speculation_rules->add_rule( 'prefetch', 'prefetch-rule-1', $prefetch_rule_1 ); + $this->assertSame( + array( + 'prefetch' => array( $prefetch_rule_1 ), + ), + $speculation_rules->jsonSerialize(), + 'Speculation rules JSON data should only contain a single "prefetch" entry when only that rule is added' + ); + + $speculation_rules->add_rule( 'prefetch', 'prefetch-rule-2', $prefetch_rule_2 ); + $speculation_rules->add_rule( 'prerender', 'prerender-rule-1', $prerender_rule_1 ); + $speculation_rules->add_rule( 'prerender', 'prerender-rule-2', $prerender_rule_2 ); + $this->assertSame( + array( + 'prefetch' => array( + $prefetch_rule_1, + $prefetch_rule_2, + ), + 'prerender' => array( + $prerender_rule_1, + $prerender_rule_2, + ), + ), + $speculation_rules->jsonSerialize(), + 'Speculation rules JSON data should contain all added rules' + ); + } + + /** + * Tests that the mode validation method correctly identifies valid and invalid values. + * + * @ticket 62503 + * @covers ::is_valid_mode + * @dataProvider data_is_valid_mode + */ + public function test_is_valid_mode( $mode, $expected ) { + if ( $expected ) { + $this->assertTrue( WP_Speculation_Rules::is_valid_mode( $mode ) ); + } else { + $this->assertFalse( WP_Speculation_Rules::is_valid_mode( $mode ) ); + } + } + + public static function data_is_valid_mode(): array { + return array( + 'prefetch' => array( 'prefetch', true ), + 'prerender' => array( 'prerender', true ), + 'auto' => array( 'auto', false ), + 'none' => array( 'none', false ), + '42' => array( 42, false ), + 'empty string' => array( '', false ), + ); + } + + /** + * Tests that the eagerness validation method correctly identifies valid and invalid values. + * + * @ticket 62503 + * @covers ::is_valid_eagerness + * @dataProvider data_is_valid_eagerness + */ + public function test_is_valid_eagerness( $eagerness, $expected ) { + if ( $expected ) { + $this->assertTrue( WP_Speculation_Rules::is_valid_eagerness( $eagerness ) ); + } else { + $this->assertFalse( WP_Speculation_Rules::is_valid_eagerness( $eagerness ) ); + } + } + + public static function data_is_valid_eagerness(): array { + return array( + 'conservative' => array( 'conservative', true ), + 'moderate' => array( 'moderate', true ), + 'eager' => array( 'eager', true ), + 'immediate' => array( 'immediate', true ), + 'auto' => array( 'auto', false ), + 'none' => array( 'none', false ), + '42' => array( 42, false ), + 'empty string' => array( '', false ), + ); + } + + /** + * Tests that the source validation method correctly identifies valid and invalid values. + * + * @ticket 62503 + * @covers ::is_valid_source + * @dataProvider data_is_valid_source + */ + public function test_is_valid_source( $source, $expected ) { + if ( $expected ) { + $this->assertTrue( WP_Speculation_Rules::is_valid_source( $source ) ); + } else { + $this->assertFalse( WP_Speculation_Rules::is_valid_source( $source ) ); + } + } + + public static function data_is_valid_source(): array { + return array( + 'list' => array( 'list', true ), + 'document' => array( 'document', true ), + 'auto' => array( 'auto', false ), + 'none' => array( 'none', false ), + '42' => array( 42, false ), + 'empty string' => array( '', false ), + ); + } +} diff --git a/tests/phpunit/tests/speculative-loading/wpUrlPatternPrefixer.php b/tests/phpunit/tests/speculative-loading/wpUrlPatternPrefixer.php new file mode 100644 index 0000000000000..f818ed4b2f4c9 --- /dev/null +++ b/tests/phpunit/tests/speculative-loading/wpUrlPatternPrefixer.php @@ -0,0 +1,94 @@ + $base_path ) ); + + $this->assertSame( + $expected, + $p->prefix_path_pattern( $path_pattern, 'demo' ) + ); + } + + public static function data_prefix_path_pattern(): array { + return array( + array( '/', '/my-page/', '/my-page/' ), + array( '/', 'my-page/', '/my-page/' ), + array( '/wp/', '/my-page/', '/wp/my-page/' ), + array( '/wp/', 'my-page/', '/wp/my-page/' ), + array( '/wp/', '/blog/2023/11/new-post/', '/wp/blog/2023/11/new-post/' ), + array( '/wp/', 'blog/2023/11/new-post/', '/wp/blog/2023/11/new-post/' ), + array( '/subdir', '/my-page/', '/subdir/my-page/' ), + array( '/subdir', 'my-page/', '/subdir/my-page/' ), + // Missing trailing slash still works, does not consider "cut-off" directory names. + array( '/subdir', '/subdirectory/my-page/', '/subdir/subdirectory/my-page/' ), + array( '/subdir', 'subdirectory/my-page/', '/subdir/subdirectory/my-page/' ), + // A base path containing a : must be enclosed in braces to avoid confusion. + array( '/scope:0/', '/*/foo', '{/scope\\:0}/*/foo' ), + ); + } + + /** + * Tests the values of the default URL pattern contexts. + * + * @ticket 62503 + * @covers ::get_default_contexts + */ + public function test_get_default_contexts() { + $contexts = WP_URL_Pattern_Prefixer::get_default_contexts(); + + $this->assertArrayHasKey( 'home', $contexts ); + $this->assertArrayHasKey( 'site', $contexts ); + $this->assertSame( '/', $contexts['home'] ); + $this->assertSame( '/', $contexts['site'] ); + } + + /** + * Tests the values of the default URL pattern contexts when using subdirectories. + * + * @ticket 62503 + * @covers ::get_default_contexts + * @dataProvider data_default_contexts_with_subdirectories + */ + public function test_get_default_contexts_with_subdirectories( string $context, string $unescaped, string $expected ) { + add_filter( + $context . '_url', + static function () use ( $unescaped ) { + return $unescaped; + } + ); + + $contexts = WP_URL_Pattern_Prefixer::get_default_contexts(); + + $this->assertArrayHasKey( $context, $contexts ); + $this->assertSame( $expected, $contexts[ $context ] ); + } + + public static function data_default_contexts_with_subdirectories(): array { + return array( + array( 'home', 'https://example.com/subdir/', '/subdir/' ), + array( 'site', 'https://example.com/subdir/wp/', '/subdir/wp/' ), + // If the context URL has URL pattern special characters it may need escaping. + array( 'home', 'https://example.com/scope:0.*/', '/scope\\:0.\\*/' ), + array( 'site', 'https://example.com/scope:0.*/wp+/', '/scope\\:0.\\*/wp\\+/' ), + ); + } +} diff --git a/tests/phpunit/tests/style-engine/styleEngine.php b/tests/phpunit/tests/style-engine/styleEngine.php new file mode 100644 index 0000000000000..f6e0444faf74b --- /dev/null +++ b/tests/phpunit/tests/style-engine/styleEngine.php @@ -0,0 +1,855 @@ +|`, to var( --wp--preset--* ) values. Default `false`. + * @type string $selector Optional. When a selector is passed, the value of `$css` in the return value will comprise a full CSS rule `$selector { ...$css_declarations }`, + * otherwise, the value will be a concatenated string of CSS declarations. + * } + * @param string $expected_output The expected output. + */ + public function test_wp_style_engine_get_styles( $block_styles, $options, $expected_output ) { + $generated_styles = wp_style_engine_get_styles( $block_styles, $options ); + + $this->assertSame( $expected_output, $generated_styles ); + } + + /** + * Data provider for test_wp_style_engine_get_styles(). + * + * @return array + */ + public function data_wp_style_engine_get_styles() { + return array( + 'default_return_value' => array( + 'block_styles' => array(), + 'options' => null, + 'expected_output' => array(), + ), + + 'inline_invalid_block_styles_empty' => array( + 'block_styles' => 'hello world!', + 'options' => null, + 'expected_output' => array(), + ), + + 'inline_invalid_block_styles_unknown_style' => array( + 'block_styles' => array( + 'pageBreakAfter' => 'verso', + ), + 'options' => null, + 'expected_output' => array(), + ), + + 'inline_invalid_block_styles_unknown_definition' => array( + 'block_styles' => array( + 'pageBreakAfter' => 'verso', + ), + 'options' => null, + 'expected_output' => array(), + ), + + 'inline_invalid_block_styles_unknown_property' => array( + 'block_styles' => array( + 'spacing' => array( + 'gap' => '1000vw', + ), + ), + 'options' => null, + 'expected_output' => array(), + ), + + 'valid_inline_css_and_classnames_as_default_context' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|texas-flood', + ), + 'spacing' => array( + 'margin' => '111px', + 'padding' => '0', + ), + 'border' => array( + 'color' => 'var:preset|color|cool-caramel', + 'width' => '2rem', + 'style' => 'dotted', + ), + ), + 'options' => array( 'convert_vars_to_classnames' => true ), + 'expected_output' => array( + 'css' => 'border-style:dotted;border-width:2rem;padding:0;margin:111px;', + 'declarations' => array( + 'border-style' => 'dotted', + 'border-width' => '2rem', + 'padding' => '0', + 'margin' => '111px', + ), + 'classnames' => 'has-text-color has-texas-flood-color has-border-color has-cool-caramel-border-color', + ), + ), + + 'inline_valid_dimension_preset_style' => array( + 'block_styles' => array( + 'dimensions' => array( + 'width' => 'var:preset|dimension|large', + 'height' => 'var:preset|dimension|modestly-small', + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'height:var(--wp--preset--dimension--modestly-small);width:var(--wp--preset--dimension--large);', + 'declarations' => array( + 'height' => 'var(--wp--preset--dimension--modestly-small)', + 'width' => 'var(--wp--preset--dimension--large)', + ), + ), + ), + + 'inline_valid_box_model_style' => array( + 'block_styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + 'margin' => array( + 'top' => '12rem', + 'left' => '2vh', + 'bottom' => '2px', + 'right' => '10em', + ), + ), + 'border' => array( + 'radius' => array( + 'topLeft' => '99px', + 'topRight' => '98px', + 'bottomLeft' => '97px', + 'bottomRight' => '96px', + ), + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'border-top-left-radius:99px;border-top-right-radius:98px;border-bottom-left-radius:97px;border-bottom-right-radius:96px;padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;margin-top:12rem;margin-left:2vh;margin-bottom:2px;margin-right:10em;', + 'declarations' => array( + 'border-top-left-radius' => '99px', + 'border-top-right-radius' => '98px', + 'border-bottom-left-radius' => '97px', + 'border-bottom-right-radius' => '96px', + 'padding-top' => '42px', + 'padding-left' => '2%', + 'padding-bottom' => '44px', + 'padding-right' => '5rem', + 'margin-top' => '12rem', + 'margin-left' => '2vh', + 'margin-bottom' => '2px', + 'margin-right' => '10em', + ), + ), + ), + + 'inline_valid_border_radius_presets' => array( + 'block_styles' => array( + 'border' => array( + 'radius' => array( + 'topLeft' => 'var:preset|border-radius|large', + 'topRight' => 'var:preset|border-radius|large', + 'bottomLeft' => 'var:preset|border-radius|large', + 'bottomRight' => 'var:preset|border-radius|large', + ), + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'border-top-left-radius:var(--wp--preset--border-radius--large);border-top-right-radius:var(--wp--preset--border-radius--large);border-bottom-left-radius:var(--wp--preset--border-radius--large);border-bottom-right-radius:var(--wp--preset--border-radius--large);', + 'declarations' => array( + 'border-top-left-radius' => 'var(--wp--preset--border-radius--large)', + 'border-top-right-radius' => 'var(--wp--preset--border-radius--large)', + 'border-bottom-left-radius' => 'var(--wp--preset--border-radius--large)', + 'border-bottom-right-radius' => 'var(--wp--preset--border-radius--large)', + ), + ), + ), + + 'inline_valid_dimensions_style' => array( + 'block_styles' => array( + 'dimensions' => array( + 'minHeight' => '50vh', + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'min-height:50vh;', + 'declarations' => array( + 'min-height' => '50vh', + ), + ), + ), + + 'inline_valid_aspect_ratio_style' => array( + 'block_styles' => array( + 'dimensions' => array( + 'aspectRatio' => '4/3', + 'minHeight' => 'unset', + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'aspect-ratio:4/3;min-height:unset;', + 'declarations' => array( + 'aspect-ratio' => '4/3', + 'min-height' => 'unset', + ), + 'classnames' => 'has-aspect-ratio', + ), + ), + + 'inline_valid_shadow_style' => array( + 'block_styles' => array( + 'shadow' => 'inset 5em 1em gold', + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'box-shadow:inset 5em 1em gold;', + 'declarations' => array( + 'box-shadow' => 'inset 5em 1em gold', + ), + ), + ), + + 'inline_valid_typography_style' => array( + 'block_styles' => array( + 'typography' => array( + 'fontSize' => 'clamp(2em, 2vw, 4em)', + 'fontFamily' => 'Roboto,Oxygen-Sans,Ubuntu,sans-serif', + 'fontStyle' => 'italic', + 'fontWeight' => '800', + 'lineHeight' => '1.3', + 'textColumns' => '2', + 'textDecoration' => 'underline', + 'textTransform' => 'uppercase', + 'letterSpacing' => '2', + 'writingMode' => 'vertical-rl', + ), + ), + 'options' => null, + 'expected_output' => array( + 'css' => 'font-size:clamp(2em, 2vw, 4em);font-family:Roboto,Oxygen-Sans,Ubuntu,sans-serif;font-style:italic;font-weight:800;line-height:1.3;column-count:2;text-decoration:underline;text-transform:uppercase;letter-spacing:2;writing-mode:vertical-rl;', + 'declarations' => array( + 'font-size' => 'clamp(2em, 2vw, 4em)', + 'font-family' => 'Roboto,Oxygen-Sans,Ubuntu,sans-serif', + 'font-style' => 'italic', + 'font-weight' => '800', + 'line-height' => '1.3', + 'column-count' => '2', + 'text-decoration' => 'underline', + 'text-transform' => 'uppercase', + 'letter-spacing' => '2', + 'writing-mode' => 'vertical-rl', + ), + ), + ), + + 'style_block_with_selector' => array( + 'block_styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ), + 'options' => array( 'selector' => '.wp-selector > p' ), + 'expected_output' => array( + 'css' => '.wp-selector > p{padding-top:42px;padding-left:2%;padding-bottom:44px;padding-right:5rem;}', + 'declarations' => array( + 'padding-top' => '42px', + 'padding-left' => '2%', + 'padding-bottom' => '44px', + 'padding-right' => '5rem', + ), + ), + ), + + 'elements_with_css_var_value' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|my-little-pony', + ), + 'typography' => array( + 'fontSize' => 'var:preset|font-size|cabbage-patch', + 'fontFamily' => 'var:preset|font-family|transformers', + ), + ), + 'options' => array( + 'selector' => '.wp-selector', + ), + 'expected_output' => array( + 'css' => '.wp-selector{color:var(--wp--preset--color--my-little-pony);font-size:var(--wp--preset--font-size--cabbage-patch);font-family:var(--wp--preset--font-family--transformers);}', + 'declarations' => array( + 'color' => 'var(--wp--preset--color--my-little-pony)', + 'font-size' => 'var(--wp--preset--font-size--cabbage-patch)', + 'font-family' => 'var(--wp--preset--font-family--transformers)', + + ), + 'classnames' => 'has-text-color has-my-little-pony-color has-cabbage-patch-font-size has-transformers-font-family', + ), + ), + + 'elements_with_invalid_preset_style_property' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|invalid_property|my-little-pony', + ), + ), + 'options' => array( 'selector' => '.wp-selector' ), + 'expected_output' => array( + 'classnames' => 'has-text-color', + ), + ), + + 'valid_classnames_deduped' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|copper-socks', + 'background' => 'var:preset|color|splendid-carrot', + 'gradient' => 'var:preset|gradient|like-wow-dude', + ), + 'typography' => array( + 'fontSize' => 'var:preset|font-size|fantastic', + 'fontFamily' => 'var:preset|font-family|totally-awesome', + ), + ), + 'options' => array( 'convert_vars_to_classnames' => true ), + 'expected_output' => array( + 'classnames' => 'has-text-color has-copper-socks-color has-background has-splendid-carrot-background-color has-like-wow-dude-gradient-background has-fantastic-font-size has-totally-awesome-font-family', + ), + ), + + 'valid_classnames_and_css_vars' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:preset|color|teal-independents', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'color:var(--wp--preset--color--teal-independents);', + 'declarations' => array( + 'color' => 'var(--wp--preset--color--teal-independents)', + ), + 'classnames' => 'has-text-color has-teal-independents-color', + ), + ), + + 'valid_classnames_with_null_style_values' => array( + 'block_styles' => array( + 'color' => array( + 'text' => '#fff', + 'background' => null, + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'color:#fff;', + 'declarations' => array( + 'color' => '#fff', + ), + 'classnames' => 'has-text-color', + ), + ), + + 'invalid_classnames_preset_value' => array( + 'block_styles' => array( + 'color' => array( + 'text' => 'var:cheese|color|fantastic', + 'background' => 'var:preset|fromage|fantastic', + ), + 'spacing' => array( + 'margin' => 'var:cheese|spacing|margin', + 'padding' => 'var:preset|spacing|padding', + ), + ), + 'options' => array( 'convert_vars_to_classnames' => true ), + 'expected_output' => array( + 'classnames' => 'has-text-color has-background', + ), + ), + + 'valid_spacing_single_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => 'var:preset|spacing|10', + 'padding' => 'var:preset|spacing|20', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'padding:var(--wp--preset--spacing--20);margin:var(--wp--preset--spacing--10);', + 'declarations' => array( + 'padding' => 'var(--wp--preset--spacing--20)', + 'margin' => 'var(--wp--preset--spacing--10)', + ), + ), + ), + + 'valid_spacing_multi_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => array( + 'left' => 'var:preset|spacing|10', + 'right' => 'var:preset|spacing|20', + 'top' => '1rem', + 'bottom' => '1rem', + ), + 'padding' => array( + 'left' => 'var:preset|spacing|30', + 'right' => 'var:preset|spacing|40', + 'top' => '14px', + 'bottom' => '14px', + ), + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'padding-left:var(--wp--preset--spacing--30);padding-right:var(--wp--preset--spacing--40);padding-top:14px;padding-bottom:14px;margin-left:var(--wp--preset--spacing--10);margin-right:var(--wp--preset--spacing--20);margin-top:1rem;margin-bottom:1rem;', + 'declarations' => array( + 'padding-left' => 'var(--wp--preset--spacing--30)', + 'padding-right' => 'var(--wp--preset--spacing--40)', + 'padding-top' => '14px', + 'padding-bottom' => '14px', + 'margin-left' => 'var(--wp--preset--spacing--10)', + 'margin-right' => 'var(--wp--preset--spacing--20)', + 'margin-top' => '1rem', + 'margin-bottom' => '1rem', + ), + ), + ), + + 'invalid_spacing_multi_preset_values' => array( + 'block_styles' => array( + 'spacing' => array( + 'margin' => array( + 'left' => 'var:preset|spaceman|10', + 'right' => 'var:preset|spaceman|20', + 'top' => '1rem', + 'bottom' => '0', + ), + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'margin-top:1rem;margin-bottom:0;', + 'declarations' => array( + 'margin-top' => '1rem', + 'margin-bottom' => '0', + ), + ), + ), + + 'invalid_classnames_options' => array( + 'block_styles' => array( + 'typography' => array( + 'fontSize' => array( + 'tomodachi' => 'friends', + ), + 'fontFamily' => array( + 'oishii' => 'tasty', + ), + ), + ), + 'options' => array(), + 'expected_output' => array(), + ), + + 'inline_valid_box_model_style_with_sides' => array( + 'block_styles' => array( + 'border' => array( + 'top' => array( + 'color' => '#fe1', + 'width' => '1.5rem', + 'style' => 'dashed', + ), + 'right' => array( + 'color' => '#fe2', + 'width' => '1.4rem', + 'style' => 'solid', + ), + 'bottom' => array( + 'color' => '#fe3', + 'width' => '1.3rem', + ), + 'left' => array( + 'color' => 'var:preset|color|swampy-yellow', + 'width' => '0.5rem', + 'style' => 'dotted', + ), + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'border-top-color:#fe1;border-top-width:1.5rem;border-top-style:dashed;border-right-color:#fe2;border-right-width:1.4rem;border-right-style:solid;border-bottom-color:#fe3;border-bottom-width:1.3rem;border-left-color:var(--wp--preset--color--swampy-yellow);border-left-width:0.5rem;border-left-style:dotted;', + 'declarations' => array( + 'border-top-color' => '#fe1', + 'border-top-width' => '1.5rem', + 'border-top-style' => 'dashed', + 'border-right-color' => '#fe2', + 'border-right-width' => '1.4rem', + 'border-right-style' => 'solid', + 'border-bottom-color' => '#fe3', + 'border-bottom-width' => '1.3rem', + 'border-left-color' => 'var(--wp--preset--color--swampy-yellow)', + 'border-left-width' => '0.5rem', + 'border-left-style' => 'dotted', + ), + ), + ), + + 'inline_invalid_box_model_style_with_sides' => array( + 'block_styles' => array( + 'border' => array( + 'top' => array( + 'top' => '#fe1', + 'right' => '1.5rem', + 'cheese' => 'dashed', + ), + 'right' => array( + 'right' => '#fe2', + 'top' => '1.4rem', + 'bacon' => 'solid', + ), + 'bottom' => array( + 'color' => 'var:preset|color|terrible-lizard', + 'bottom' => '1.3rem', + ), + 'left' => array( + 'left' => null, + 'width' => null, + 'top' => 'dotted', + ), + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => 'border-bottom-color:var(--wp--preset--color--terrible-lizard);', + 'declarations' => array( + 'border-bottom-color' => 'var(--wp--preset--color--terrible-lizard)', + ), + ), + ), + + 'inline_background_image_url_with_background_size' => array( + 'block_styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.com/image.jpg', + ), + 'backgroundPosition' => 'center', + 'backgroundRepeat' => 'no-repeat', + 'backgroundSize' => 'cover', + 'backgroundAttachment' => 'fixed', + ), + ), + 'options' => array(), + 'expected_output' => array( + 'css' => "background-image:url('https://example.com/image.jpg');background-position:center;background-repeat:no-repeat;background-size:cover;background-attachment:fixed;", + 'declarations' => array( + 'background-image' => "url('https://example.com/image.jpg')", + 'background-position' => 'center', + 'background-repeat' => 'no-repeat', + 'background-size' => 'cover', + 'background-attachment' => 'fixed', + ), + ), + ), + ); + } + + /** + * Tests adding rules to a store and retrieving a generated stylesheet. + * + * @ticket 56467 + * + * @covers ::wp_style_engine_get_styles + */ + public function test_should_store_block_styles_using_context() { + $block_styles = array( + 'spacing' => array( + 'padding' => array( + 'top' => '42px', + 'left' => '2%', + 'bottom' => '44px', + 'right' => '5rem', + ), + ), + ); + + $generated_styles = wp_style_engine_get_styles( + $block_styles, + array( + 'context' => 'block-supports', + 'selector' => 'article', + ) + ); + $store = WP_Style_Engine::get_store( 'block-supports' ); + $rule = $store->get_all_rules()['article']; + + $this->assertSame( $generated_styles['css'], $rule->get_css() ); + } + + /** + * Tests that passing no context does not store styles. + * + * @ticket 56467 + * + * @covers ::wp_style_engine_get_styles + */ + public function test_should_not_store_block_styles_without_context() { + $block_styles = array( + 'typography' => array( + 'fontSize' => '999px', + ), + ); + + wp_style_engine_get_styles( + $block_styles, + array( + 'selector' => '#font-size-rulez', + ) + ); + + $all_stores = WP_Style_Engine_CSS_Rules_Store::get_stores(); + + $this->assertEmpty( $all_stores ); + } + + /** + * Tests adding rules to a store and retrieving a generated stylesheet. + * + * @ticket 56467 + * + * @covers ::wp_style_engine_get_stylesheet_from_context + */ + public function test_should_get_stored_stylesheet_from_context() { + $css_rules = array( + array( + 'selector' => '.frodo', + 'declarations' => array( + 'color' => 'brown', + 'height' => '10px', + 'width' => '30px', + 'border-style' => 'dotted', + ), + ), + array( + 'selector' => '.samwise', + 'declarations' => array( + 'color' => 'brown', + 'height' => '20px', + 'width' => '50px', + 'border-style' => 'solid', + ), + ), + ); + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( + $css_rules, + array( + 'context' => 'test-store', + ) + ); + + $this->assertSame( $compiled_stylesheet, wp_style_engine_get_stylesheet_from_context( 'test-store' ) ); + } + + /** + * Tests returning a generated stylesheet from a set of rules. + * + * @ticket 56467 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_return_stylesheet_from_css_rules() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'unset', + ), + ), + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + 'align-self' => 'safe center', + ), + ), + array( + 'selector' => '.radagast', + 'declarations' => array( + 'color' => 'brown', + 'height' => '60px', + 'border-style' => 'dashed', + 'align-self' => 'stretch', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.saruman{color:white;height:100px;border-style:solid;align-self:unset;}.gandalf{color:grey;height:90px;border-style:dotted;align-self:safe center;}.radagast{color:brown;height:60px;border-style:dashed;align-self:stretch;}', $compiled_stylesheet ); + } + + /** + * Tests that incoming styles are deduped and merged. + * + * @ticket 58811 + * @ticket 56467 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_dedupe_and_merge_css_rules() { + $css_rules = array( + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + array( + 'selector' => '.gandalf', + 'declarations' => array( + 'color' => 'white', + 'height' => '190px', + 'padding' => '10px', + 'margin-bottom' => '100px', + ), + ), + array( + 'selector' => '.dumbledore', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + array( + 'selector' => '.rincewind', + 'declarations' => array( + 'color' => 'grey', + 'height' => '90px', + 'border-style' => 'dotted', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.gandalf{color:white;height:190px;border-style:dotted;padding:10px;margin-bottom:100px;}.dumbledore{color:grey;height:90px;border-style:dotted;}.rincewind{color:grey;height:90px;border-style:dotted;}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules and merging their declarations. + * + * @ticket 61099 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_merge_declarations_for_rules_groups() { + $css_rules = array( + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'white', + 'height' => '100px', + 'border-style' => 'solid', + 'align-self' => 'stretch', + ), + ), + array( + 'selector' => '.saruman', + 'rules_group' => '@container (min-width: 700px)', + 'declarations' => array( + 'color' => 'black', + 'font-family' => 'The-Great-Eye', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '@container (min-width: 700px){.saruman{color:black;height:100px;border-style:solid;align-self:stretch;font-family:The-Great-Eye;}}', $compiled_stylesheet ); + } + + /** + * Tests returning a generated stylesheet from a set of nested rules. + * + * @ticket 61099 + * + * @covers ::wp_style_engine_get_stylesheet_from_css_rules + */ + public function test_should_return_stylesheet_with_nested_rules() { + $css_rules = array( + array( + 'rules_group' => '.foo', + 'selector' => '@media (orientation: landscape)', + 'declarations' => array( + 'background-color' => 'blue', + ), + ), + array( + 'rules_group' => '.foo', + 'selector' => '@media (min-width > 1024px)', + 'declarations' => array( + 'background-color' => 'cotton-blue', + ), + ), + ); + + $compiled_stylesheet = wp_style_engine_get_stylesheet_from_css_rules( $css_rules, array( 'prettify' => false ) ); + + $this->assertSame( '.foo{@media (orientation: landscape){background-color:blue;}}.foo{@media (min-width > 1024px){background-color:cotton-blue;}}', $compiled_stylesheet ); + } +} diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php new file mode 100644 index 0000000000000..b510edcbfd0cb --- /dev/null +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssDeclarations.php @@ -0,0 +1,321 @@ + '10px', + 'font-size' => '2rem', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + + $this->assertSame( $input_declarations, $css_declarations->get_declarations() ); + } + + /** + * Tests that declarations are added. + * + * @ticket 56467 + * + * @covers ::add_declarations + * @covers ::add_declaration + */ + public function test_should_add_declarations() { + $input_declarations = array( + 'padding' => '20px', + 'color' => 'var(--wp--preset--elbow-patches)', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations(); + $css_declarations->add_declarations( $input_declarations ); + + $this->assertSame( $input_declarations, $css_declarations->get_declarations() ); + } + + /** + * Tests that new declarations are added to existing declarations. + * + * @ticket 56467 + * + * @covers ::add_declarations + * @covers ::add_declaration + */ + public function test_should_add_new_declarations_to_existing() { + $input_declarations = array( + 'border-width' => '1%', + 'background-color' => 'var(--wp--preset--english-mustard)', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $extra_declaration = array( + 'letter-spacing' => '1.5px', + ); + $css_declarations->add_declarations( $extra_declaration ); + + $this->assertSame( array_merge( $input_declarations, $extra_declaration ), $css_declarations->get_declarations() ); + } + + /** + * Tests that properties are sanitized before storing. + * + * @ticket 56467 + * + * @covers ::sanitize_property + */ + public function test_should_sanitize_properties() { + $input_declarations = array( + '^--wp--style--sleepy-potato$' => '40px', + '' => 'var(--wp--preset--english-mustard)', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + + $this->assertSame( + array( + '--wp--style--sleepy-potato' => '40px', + 'background-color' => 'var(--wp--preset--english-mustard)', + ), + $css_declarations->get_declarations() + ); + } + + /** + * Test that values with HTML tags are escaped, and CSS properties are run through safecss_filter_attr(). + * + * @ticket 56467 + * + * @covers ::get_declarations_string + * @covers ::filter_declaration + */ + public function test_should_strip_html_tags_and_remove_unsafe_css_properties() { + $input_declarations = array( + 'font-size' => '', + 'padding' => '', + 'potato' => 'uppercase', + 'cheese' => '10px', + 'margin-right' => '10em', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $safe_style_css_mock_action = new MockAction(); + + // filter_declaration() is called in get_declarations_string(). + add_filter( 'safe_style_css', array( $safe_style_css_mock_action, 'filter' ) ); + $css_declarations_string = $css_declarations->get_declarations_string(); + + $this->assertSame( + 3, // Values with HTML tags are removed first by wp_strip_all_tags(). + $safe_style_css_mock_action->get_call_count(), + '"safe_style_css" filters were not applied to CSS declaration properties.' + ); + + $this->assertSame( + 'margin-right:10em;', + $css_declarations_string, + 'Unallowed CSS properties or values with HTML tags were not removed.' + ); + } + + /** + * Tests that calc, clamp, min, max, and minmax CSS functions are allowed. + * + * @ticket 56467 + * + * @covers ::get_declarations_string + * @covers ::filter_declaration + */ + public function test_should_allow_css_functions_and_strip_unsafe_css_values() { + $input_declarations = array( + 'background' => 'var(--wp--preset--color--primary, 10px)', // Simple var(). + 'font-size' => 'clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem)', // Nested clamp(). + 'width' => 'min(150vw, 100px)', + 'min-width' => 'max(150vw, 100px)', + 'max-width' => 'minmax(400px, 50%)', + 'padding' => 'calc(80px * -1)', + 'background-image' => 'url("https://wordpress.org")', + 'line-height' => 'url("https://wordpress.org")', + 'margin' => 'illegalfunction(30px)', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $safecss_filter_attr_allow_css_mock_action = new MockAction(); + + // filter_declaration() is called in get_declarations_string(). + add_filter( 'safecss_filter_attr_allow_css', array( $safecss_filter_attr_allow_css_mock_action, 'filter' ) ); + $css_declarations_string = $css_declarations->get_declarations_string(); + + $this->assertSame( + 9, + $safecss_filter_attr_allow_css_mock_action->get_call_count(), + '"safecss_filter_attr_allow_css" filters were not applied to CSS declaration values.' + ); + + $this->assertSame( + 'background:var(--wp--preset--color--primary, 10px);font-size:clamp(36.00rem, calc(32.00rem + 10.00vw), 40.00rem);width:min(150vw, 100px);min-width:max(150vw, 100px);max-width:minmax(400px, 50%);padding:calc(80px * -1);background-image:url("https://wordpress.org");', + $css_declarations_string, + 'Unsafe values were not removed' + ); + } + + /** + * Tests that CSS declarations are compiled into a CSS declarations block string. + * + * @ticket 56467 + * + * @covers ::get_declarations_string + * + * @dataProvider data_should_compile_css_declarations_to_css_declarations_string + * + * @param string $expected The expected declarations block string. + * @param bool $should_prettify Optional. Whether to pretty the string. Default false. + * @param int $indent_count Optional. The number of tab indents. Default false. + */ + public function test_should_compile_css_declarations_to_css_declarations_string( $expected, $should_prettify = false, $indent_count = 0 ) { + $input_declarations = array( + 'color' => 'red', + 'border-top-left-radius' => '99px', + 'text-decoration' => 'underline', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + + $this->assertSame( + $expected, + $css_declarations->get_declarations_string( $should_prettify, $indent_count ) + ); + } + + /** + * Data provider for test_should_compile_css_declarations_to_css_declarations_string(). + * + * @return array + */ + public function data_should_compile_css_declarations_to_css_declarations_string() { + return array( + 'unprettified, no indent' => array( + 'expected' => 'color:red;border-top-left-radius:99px;text-decoration:underline;', + ), + 'unprettified, one indent' => array( + 'expected' => 'color:red;border-top-left-radius:99px;text-decoration:underline;', + 'should_prettify' => false, + 'indent_count' => 1, + ), + 'prettified, no indent' => array( + 'expected' => 'color: red; border-top-left-radius: 99px; text-decoration: underline;', + 'should_prettify' => true, + ), + 'prettified, one indent' => array( + 'expected' => "\tcolor: red;\n\tborder-top-left-radius: 99px;\n\ttext-decoration: underline;", + 'should_prettify' => true, + 'indent_count' => 1, + ), + 'prettified, two indents' => array( + 'expected' => "\t\tcolor: red;\n\t\tborder-top-left-radius: 99px;\n\t\ttext-decoration: underline;", + 'should_prettify' => true, + 'indent_count' => 2, + ), + ); + } + + /** + * Tests removing a single declaration. + * + * @ticket 56467 + * + * @covers ::remove_declaration + */ + public function test_should_remove_single_declaration() { + $input_declarations = array( + 'color' => 'tomato', + 'margin' => '10em 10em 20em 1px', + 'font-family' => 'Happy Font serif', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + + $this->assertSame( + 'color:tomato;margin:10em 10em 20em 1px;font-family:Happy Font serif;', + $css_declarations->get_declarations_string(), + 'CSS declarations string does not match the values of `$declarations` passed to the constructor.' + ); + + $css_declarations->remove_declaration( 'color' ); + + $this->assertSame( + 'margin:10em 10em 20em 1px;font-family:Happy Font serif;', + $css_declarations->get_declarations_string(), + 'Output after removing "color" declaration via `remove_declaration()` does not match expectations' + ); + } + + /** + * Tests that multiple declarations are removed. + * + * @ticket 56467 + * + * @covers ::remove_declarations + */ + public function test_should_remove_multiple_declarations() { + $input_declarations = array( + 'color' => 'cucumber', + 'margin' => '10em 10em 20em 1px', + 'font-family' => 'Happy Font serif', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + + $this->assertSame( + 'color:cucumber;margin:10em 10em 20em 1px;font-family:Happy Font serif;', + $css_declarations->get_declarations_string(), + 'CSS declarations string does not match the values of `$declarations` passed to the constructor.' + ); + + $css_declarations->remove_declarations( array( 'color', 'margin' ) ); + + $this->assertSame( + 'font-family:Happy Font serif;', + $css_declarations->get_declarations_string(), + 'Output after removing "color" and "margin" declarations via `remove_declarations()` does not match expectations' + ); + } + + /** + * Tests that non-string values are rejected without causing fatal errors. + * + * @ticket 64545 + * + * @covers ::add_declaration + */ + public function test_should_reject_non_string_values() { + $css_declarations = new WP_Style_Engine_CSS_Declarations(); + + // Add valid string value first. + $css_declarations->add_declaration( 'color', 'red' ); + + // Try to add array value - should be silently rejected. + $css_declarations->add_declaration( 'padding-margin', array( 'top' => '10px' ) ); + + // Try to add other non-string values. + $css_declarations->add_declaration( 'font-size', 123 ); + $css_declarations->add_declaration( 'margin', null ); + + // Only the valid string value should be stored. + $this->assertSame( + array( 'color' => 'red' ), + $css_declarations->get_declarations(), + 'Non-string values should be rejected without causing errors.' + ); + } +} diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php new file mode 100644 index 0000000000000..a9de39392ade0 --- /dev/null +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssRule.php @@ -0,0 +1,179 @@ + '10px', + 'font-size' => '2rem', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $css_declarations ); + + $this->assertSame( $selector, $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); + + $expected = "$selector{{$css_declarations->get_declarations_string()}}"; + + $this->assertSame( $expected, $css_rule->get_css(), 'Value returned by get_css() does not match expected declarations string.' ); + } + + /** + * Tests setting and getting a rules group. + * + * @ticket 61099 + * + * @covers ::set_rules_group + * @covers ::get_rules_group + */ + public function test_should_set_rules_group() { + $rule = new WP_Style_Engine_CSS_Rule( '.heres-johnny', array(), '@layer state' ); + + $this->assertSame( '@layer state', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to constructor.' ); + + $rule->set_rules_group( '@layer pony' ); + + $this->assertSame( '@layer pony', $rule->get_rules_group(), 'Return value of get_rules_group() does not match value passed to set_rules_group().' ); + } + + /** + * Tests that declaration properties are deduplicated. + * + * @ticket 56467 + * + * @covers ::add_declarations + * @covers ::get_css + */ + public function test_should_dedupe_properties_in_rules() { + $selector = '.taggart'; + $first_declaration = array( + 'font-size' => '2rem', + ); + $overwrite_first_declaration = array( + 'font-size' => '4px', + ); + $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $first_declaration ); + $css_rule->add_declarations( new WP_Style_Engine_CSS_Declarations( $overwrite_first_declaration ) ); + + $expected = '.taggart{font-size:4px;}'; + + $this->assertSame( $expected, $css_rule->get_css() ); + } + + /** + * Tests that declarations can be added to existing rules. + * + * @ticket 56467 + * + * @covers ::add_declarations + * @covers ::get_css + */ + public function test_should_add_declarations_to_existing_rules() { + // Declarations using a WP_Style_Engine_CSS_Declarations object. + $some_css_declarations = new WP_Style_Engine_CSS_Declarations( array( 'margin-top' => '10px' ) ); + // Declarations using a property => value array. + $some_more_css_declarations = array( 'font-size' => '1rem' ); + $css_rule = new WP_Style_Engine_CSS_Rule( '.hill-street-blues', $some_css_declarations ); + $css_rule->add_declarations( $some_more_css_declarations ); + + $expected = '.hill-street-blues{margin-top:10px;font-size:1rem;}'; + + $this->assertSame( $expected, $css_rule->get_css() ); + } + + /** + * Tests setting a selector to a rule. + * + * @ticket 56467 + * + * @covers ::set_selector + */ + public function test_should_set_selector() { + $selector = '.taggart'; + $css_rule = new WP_Style_Engine_CSS_Rule( $selector ); + + $this->assertSame( $selector, $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to constructor.' ); + + $css_rule->set_selector( '.law-and-order' ); + + $this->assertSame( '.law-and-order', $css_rule->get_selector(), 'Return value of get_selector() does not match value passed to set_selector().' ); + } + + /** + * Tests generating a CSS rule string. + * + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_generate_css_rule_string() { + $selector = '.chips'; + $input_declarations = array( + 'margin-top' => '10px', + 'font-size' => '2rem', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $css_declarations ); + $expected = "$selector{{$css_declarations->get_declarations_string()}}"; + + $this->assertSame( $expected, $css_rule->get_css() ); + } + + /** + * Tests that an empty string will be returned where there are no declarations in a CSS rule. + * + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_return_empty_string_with_no_declarations() { + $selector = '.holmes'; + $input_declarations = array(); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $css_declarations ); + + $this->assertSame( '', $css_rule->get_css() ); + } + + /** + * Tests that CSS rules are prettified. + * + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_prettify_css_rule_output() { + $selector = '.baptiste'; + $input_declarations = array( + 'margin-left' => '0', + 'font-family' => 'Detective Sans', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $input_declarations ); + $css_rule = new WP_Style_Engine_CSS_Rule( $selector, $css_declarations ); + $expected = '.baptiste { + margin-left: 0; + font-family: Detective Sans; +}'; + + $this->assertSameIgnoreEOL( $expected, $css_rule->get_css( true ) ); + } +} diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php b/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php new file mode 100644 index 0000000000000..3797efa6b7c8e --- /dev/null +++ b/tests/phpunit/tests/style-engine/wpStyleEngineCssRulesStore.php @@ -0,0 +1,200 @@ +assertInstanceOf( 'WP_Style_Engine_CSS_Rules_Store', $new_pancakes_store ); + } + + /** + * Tests that a `$store_name` argument is required and no store will be created without one. + * + * @ticket 56467 + * + * @covers ::get_store + */ + public function test_should_not_create_store_without_a_store_name() { + $not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( '' ); + + $this->assertEmpty( $not_a_store, 'get_store() did not return an empty value with empty string as argument.' ); + + $also_not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( 123 ); + + $this->assertEmpty( $also_not_a_store, 'get_store() did not return an empty value with number as argument.' ); + + $definitely_not_a_store = WP_Style_Engine_CSS_Rules_Store::get_store( null ); + + $this->assertEmpty( $definitely_not_a_store, 'get_store() did not return an empty value with `null` as argument.' ); + } + + /** + * Tests returning a previously created store when the same selector key is passed. + * + * @ticket 56467 + * + * @covers ::get_store + */ + public function test_should_return_existing_store() { + $new_fish_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'fish-n-chips' ); + $selector = '.haddock'; + + $new_fish_store->add_rule( $selector ); + + $this->assertSame( $selector, $new_fish_store->add_rule( $selector )->get_selector(), 'Selector string of store rule does not match expected value' ); + + $the_same_fish_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'fish-n-chips' ); + + $this->assertSame( $selector, $the_same_fish_store->add_rule( $selector )->get_selector(), 'Selector string of existing store rule does not match expected value' ); + } + + /** + * Tests returning all previously created stores. + * + * @ticket 56467 + * + * @covers ::get_stores + */ + public function test_should_get_all_existing_stores() { + $burrito_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'burrito' ); + $quesadilla_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'quesadilla' ); + + $this->assertSame( + array( + 'burrito' => $burrito_store, + 'quesadilla' => $quesadilla_store, + ), + WP_Style_Engine_CSS_Rules_Store::get_stores() + ); + } + + /** + * Tests that all previously created stores are deleted. + * + * @ticket 56467 + * + * @covers ::remove_all_stores + */ + public function test_should_remove_all_stores() { + $dolmades_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'dolmades' ); + $tzatziki_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'tzatziki' ); + + $this->assertSame( + array( + 'dolmades' => $dolmades_store, + 'tzatziki' => $tzatziki_store, + ), + WP_Style_Engine_CSS_Rules_Store::get_stores(), + 'Return value of get_stores() does not match expectation' + ); + + WP_Style_Engine_CSS_Rules_Store::remove_all_stores(); + + $this->assertSame( + array(), + WP_Style_Engine_CSS_Rules_Store::get_stores(), + 'Return value of get_stores() is not an empty array after remove_all_stores() called.' + ); + } + + /** + * Tests adding rules to an existing store. + * + * @ticket 56467 + * + * @covers ::add_rule + */ + public function test_should_add_rule_to_existing_store() { + $new_pie_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'meat-pie' ); + $selector = '.wp-block-sauce a:hover'; + $store_rule = $new_pie_store->add_rule( $selector ); + $expected = ''; + + $this->assertSame( $expected, $store_rule->get_css(), 'Return value of get_css() is not a empty string where a rule has no CSS declarations.' ); + + $pie_declarations = array( + 'color' => 'brown', + 'border-color' => 'yellow', + 'border-radius' => '10rem', + ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $pie_declarations ); + $store_rule->add_declarations( $css_declarations ); + $store_rule = $new_pie_store->add_rule( $selector ); + + $expected = "$selector{{$css_declarations->get_declarations_string()}}"; + + $this->assertSame( $expected, $store_rule->get_css(), 'Return value of get_css() does not match expected CSS from existing store rules.' ); + } + + /** + * Tests that all stored rule objects are returned. + * + * @ticket 56467 + * + * @covers ::get_all_rules + */ + public function test_should_get_all_rule_objects_for_a_store() { + $new_pizza_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'pizza-with-mozzarella' ); + $selector = '.wp-block-anchovies a:hover'; + $store_rule = $new_pizza_store->add_rule( $selector ); + $expected = array( + $selector => $store_rule, + ); + + $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations.' ); + + $new_selector = '.wp-block-mushroom a:hover'; + $newer_pizza_declarations = array( + 'padding' => '100px', + ); + $new_store_rule = $new_pizza_store->add_rule( $new_selector ); + $css_declarations = new WP_Style_Engine_CSS_Declarations( $newer_pizza_declarations ); + $new_store_rule->add_declarations( array( $css_declarations ) ); + + $expected = array( + $selector => $store_rule, + $new_selector => $new_store_rule, + ); + + $this->assertSame( $expected, $new_pizza_store->get_all_rules(), 'Return value for get_all_rules() does not match expectations after adding new rules to store.' ); + } + + /** + * Tests adding rules group keys to store. + * + * @ticket 61099 + * + * @covers ::add_rule + */ + public function test_should_store_as_concatenated_rules_groups_and_selector() { + $store_one = WP_Style_Engine_CSS_Rules_Store::get_store( 'one' ); + $store_one_rule = $store_one->add_rule( '.tony', '.one' ); + + $this->assertSame( + '.one .tony', + "{$store_one_rule->get_rules_group()} {$store_one_rule->get_selector()}", + 'add_rule() does not concatenate rules group and selector.' + ); + } +} diff --git a/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php b/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php new file mode 100644 index 0000000000000..070aefe6886f7 --- /dev/null +++ b/tests/phpunit/tests/style-engine/wpStyleEngineProcessor.php @@ -0,0 +1,418 @@ +add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nicer-rule' ); + $a_nicer_css_rule->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nice_processor = new WP_Style_Engine_Processor(); + $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); + + $this->assertSame( + '.a-nice-rule{color:var(--nice-color);background-color:purple;}.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}', + $a_nice_processor->get_css( array( 'prettify' => false ) ) + ); + } + + /** + * Tests adding nested rules with at-rules and returning compiled CSS rules. + * + * @ticket 61099 + * + * @covers ::add_rules + * @covers ::get_css + */ + public function test_should_return_nested_rules_as_compiled_css() { + $a_nice_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nice-rule' ); + $a_nice_css_rule->add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nice_css_rule->set_rules_group( '@media (min-width: 80rem)' ); + + $a_nicer_css_rule = new WP_Style_Engine_CSS_Rule( '.a-nicer-rule' ); + $a_nicer_css_rule->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nicer_css_rule->set_rules_group( '@layer nicety' ); + + $a_nice_processor = new WP_Style_Engine_Processor(); + $a_nice_processor->add_rules( array( $a_nice_css_rule, $a_nicer_css_rule ) ); + + $this->assertSame( + '@media (min-width: 80rem){.a-nice-rule{color:var(--nice-color);background-color:purple;}}@layer nicety{.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}}', + $a_nice_processor->get_css( array( 'prettify' => false ) ) + ); + } + + /** + * Tests compiling CSS rules and formatting them with new lines and indents. + * + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_return_prettified_css_rules() { + $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-wonderful-rule' ); + $a_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-very_wonderful-rule' ); + $a_very_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_more_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-more-wonderful-rule' ); + $a_more_wonderful_css_rule->add_declarations( + array( + 'font-family' => 'Wonderful sans', + 'font-size' => '1em', + 'background-color' => 'orange', + ) + ); + $a_wonderful_processor = new WP_Style_Engine_Processor(); + $a_wonderful_processor->add_rules( array( $a_wonderful_css_rule, $a_very_wonderful_css_rule, $a_more_wonderful_css_rule ) ); + + $expected = '.a-wonderful-rule { + color: var(--wonderful-color); + background-color: orange; +} +.a-very_wonderful-rule { + color: var(--wonderful-color); + background-color: orange; +} +.a-more-wonderful-rule { + font-family: Wonderful sans; + font-size: 1em; + background-color: orange; +} +'; + $this->assertSameIgnoreEOL( + $expected, + $a_wonderful_processor->get_css( array( 'prettify' => true ) ) + ); + } + + /** + * Tests compiling nested CSS rules and formatting them with new lines and indents. + * + * @ticket 61099 + * + * @covers ::get_css + */ + public function test_should_return_prettified_nested_css_rules() { + $a_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-wonderful-rule' ); + $a_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_wonderful_css_rule->set_rules_group( '@media (min-width: 80rem)' ); + + $a_very_wonderful_css_rule = new WP_Style_Engine_CSS_Rule( '.a-very_wonderful-rule' ); + $a_very_wonderful_css_rule->add_declarations( + array( + 'color' => 'var(--wonderful-color)', + 'background-color' => 'orange', + ) + ); + $a_very_wonderful_css_rule->set_rules_group( '@layer wonderfulness' ); + + $a_wonderful_processor = new WP_Style_Engine_Processor(); + $a_wonderful_processor->add_rules( array( $a_wonderful_css_rule, $a_very_wonderful_css_rule ) ); + + $expected = '@media (min-width: 80rem) { + .a-wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +@layer wonderfulness { + .a-very_wonderful-rule { + color: var(--wonderful-color); + background-color: orange; + } +} +'; + $this->assertSame( + $expected, + $a_wonderful_processor->get_css( array( 'prettify' => true ) ) + ); + } + + /** + * Tests adding a store and compiling CSS rules from that store. + * + * @ticket 56467 + * + * @covers ::add_store + */ + public function test_should_return_store_rules_as_css() { + $a_nice_store = WP_Style_Engine_CSS_Rules_Store::get_store( 'nice' ); + $a_nice_store->add_rule( '.a-nice-rule' )->add_declarations( + array( + 'color' => 'var(--nice-color)', + 'background-color' => 'purple', + ) + ); + $a_nice_store->add_rule( '.a-nicer-rule' )->add_declarations( + array( + 'font-family' => 'Nice sans', + 'font-size' => '1em', + 'background-color' => 'purple', + ) + ); + $a_nice_renderer = new WP_Style_Engine_Processor(); + $a_nice_renderer->add_store( $a_nice_store ); + + $this->assertSame( + '.a-nice-rule{color:var(--nice-color);background-color:purple;}.a-nicer-rule{font-family:Nice sans;font-size:1em;background-color:purple;}', + $a_nice_renderer->get_css( array( 'prettify' => false ) ) + ); + } + + /** + * Tests that CSS declarations are merged and deduped in the final CSS rules output. + * + * @ticket 56467 + * + * @covers ::add_rules + * @covers ::get_css + */ + public function test_should_dedupe_and_merge_css_declarations() { + $an_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $an_excellent_processor = new WP_Style_Engine_Processor(); + $an_excellent_rule->add_declarations( + array( + 'color' => 'var(--excellent-color)', + 'border-style' => 'dotted', + ) + ); + $an_excellent_processor->add_rules( $an_excellent_rule ); + + $another_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $another_excellent_rule->add_declarations( + array( + 'color' => 'var(--excellent-color)', + 'border-style' => 'dotted', + 'border-color' => 'brown', + ) + ); + $an_excellent_processor->add_rules( $another_excellent_rule ); + + $this->assertSame( + '.an-excellent-rule{color:var(--excellent-color);border-style:dotted;border-color:brown;}', + $an_excellent_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations with new, deduped and merged declarations.' + ); + + $yet_another_excellent_rule = new WP_Style_Engine_CSS_Rule( '.an-excellent-rule' ); + $yet_another_excellent_rule->add_declarations( + array( + 'color' => 'var(--excellent-color)', + 'border-style' => 'dashed', + 'border-width' => '2px', + ) + ); + $an_excellent_processor->add_rules( $yet_another_excellent_rule ); + + $this->assertSame( + '.an-excellent-rule{color:var(--excellent-color);border-style:dashed;border-color:brown;border-width:2px;}', + $an_excellent_processor->get_css( array( 'prettify' => false ) ), + 'Return value of get_css() does not match expectations with deduped and merged declarations.' + ); + } + + /** + * Tests printing out 'unoptimized' CSS, that is, uncombined selectors and duplicate CSS rules. + * + * This is the default. + * + * @ticket 58811 + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_not_optimize_css_output() { + $a_sweet_rule = new WP_Style_Engine_CSS_Rule( + '.a-sweet-rule', + array( + 'color' => 'var(--sweet-color)', + 'background-color' => 'purple', + ) + ); + + $a_sweeter_rule = new WP_Style_Engine_CSS_Rule( + '#an-even-sweeter-rule > marquee', + array( + 'color' => 'var(--sweet-color)', + 'background-color' => 'purple', + ) + ); + + $the_sweetest_rule = new WP_Style_Engine_CSS_Rule( + '.the-sweetest-rule-of-all a', + array( + 'color' => 'var(--sweet-color)', + 'background-color' => 'purple', + ) + ); + + $a_sweet_processor = new WP_Style_Engine_Processor(); + $a_sweet_processor->add_rules( array( $a_sweet_rule, $a_sweeter_rule, $the_sweetest_rule ) ); + + $this->assertSame( + '.a-sweet-rule{color:var(--sweet-color);background-color:purple;}#an-even-sweeter-rule > marquee{color:var(--sweet-color);background-color:purple;}.the-sweetest-rule-of-all a{color:var(--sweet-color);background-color:purple;}', + $a_sweet_processor->get_css( + array( + 'optimize' => false, + 'prettify' => false, + ) + ) + ); + } + + /** + * Tests that 'optimized' CSS is output, that is, that duplicate CSS rules are combined under their corresponding selectors. + * + * @ticket 58811 + * @ticket 56467 + * + * @covers ::get_css + */ + public function test_should_not_optimize_css_output_by_default() { + $a_sweet_rule = new WP_Style_Engine_CSS_Rule( + '.a-sweet-rule', + array( + 'color' => 'var(--sweet-color)', + 'background-color' => 'purple', + ) + ); + + $a_sweeter_rule = new WP_Style_Engine_CSS_Rule( + '#an-even-sweeter-rule > marquee', + array( + 'color' => 'var(--sweet-color)', + 'background-color' => 'purple', + ) + ); + + $a_sweet_processor = new WP_Style_Engine_Processor(); + $a_sweet_processor->add_rules( array( $a_sweet_rule, $a_sweeter_rule ) ); + + $this->assertSame( + '.a-sweet-rule{color:var(--sweet-color);background-color:purple;}#an-even-sweeter-rule > marquee{color:var(--sweet-color);background-color:purple;}', + $a_sweet_processor->get_css( array( 'prettify' => false ) ) + ); + } + + /** + * Tests that incoming CSS rules are optimized and merged with existing CSS rules. + * + * @ticket 58811 + * @ticket 56467 + * + * @covers ::add_rules + */ + public function test_should_combine_previously_added_css_rules() { + $a_lovely_processor = new WP_Style_Engine_Processor(); + $a_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-lovely-rule', + array( + 'border-color' => 'purple', + ) + ); + $a_lovely_processor->add_rules( $a_lovely_rule ); + $a_lovelier_rule = new WP_Style_Engine_CSS_Rule( + '.a-lovelier-rule', + array( + 'border-color' => 'purple', + ) + ); + $a_lovely_processor->add_rules( $a_lovelier_rule ); + + $this->assertSame( + '.a-lovely-rule,.a-lovelier-rule{border-color:purple;}', + $a_lovely_processor->get_css( + array( + 'prettify' => false, + 'optimize' => true, + ) + ), + 'Return value of get_css() does not match expectations when combining 2 CSS rules' + ); + + $a_most_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-most-lovely-rule', + array( + 'border-color' => 'purple', + ) + ); + $a_lovely_processor->add_rules( $a_most_lovely_rule ); + + $a_perfectly_lovely_rule = new WP_Style_Engine_CSS_Rule( + '.a-perfectly-lovely-rule', + array( + 'border-color' => 'purple', + ) + ); + $a_lovely_processor->add_rules( $a_perfectly_lovely_rule ); + + $this->assertSame( + '.a-lovely-rule,.a-lovelier-rule,.a-most-lovely-rule,.a-perfectly-lovely-rule{border-color:purple;}', + $a_lovely_processor->get_css( + array( + 'prettify' => false, + 'optimize' => true, + ) + ), + 'Return value of get_css() does not match expectations when combining 4 CSS rules' + ); + } +} diff --git a/tests/phpunit/tests/taxonomy.php b/tests/phpunit/tests/taxonomy.php index 6d7cc0660236d..13528c3015c6b 100644 --- a/tests/phpunit/tests/taxonomy.php +++ b/tests/phpunit/tests/taxonomy.php @@ -4,6 +4,18 @@ * @group taxonomy */ class Tests_Taxonomy extends WP_UnitTestCase { + + /** + * Editor user ID. + * + * @var int $editor_id + */ + public static $editor_id; + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$editor_id = $factory->user->create( array( 'role' => 'editor' ) ); + } + public function test_get_post_taxonomies() { $this->assertSame( array( 'category', 'post_tag', 'post_format' ), get_object_taxonomies( 'post' ) ); } @@ -12,19 +24,27 @@ public function test_get_link_taxonomies() { $this->assertSame( array( 'link_category' ), get_object_taxonomies( 'link' ) ); } + public function test_get_block_taxonomies() { + $this->assertSame( array( 'wp_pattern_category' ), get_object_taxonomies( 'wp_block' ) ); + } + /** * @ticket 5417 */ public function test_get_unknown_taxonomies() { // Taxonomies for an unknown object type. - $this->assertSame( array(), get_object_taxonomies( rand_str() ) ); + $this->assertSame( array(), get_object_taxonomies( 'unknown' ) ); $this->assertSame( array(), get_object_taxonomies( '' ) ); $this->assertSame( array(), get_object_taxonomies( 0 ) ); $this->assertSame( array(), get_object_taxonomies( null ) ); } public function test_get_post_taxonomy() { - foreach ( get_object_taxonomies( 'post' ) as $taxonomy ) { + $taxonomies = get_object_taxonomies( 'post' ); + + $this->assertNotEmpty( $taxonomies ); + + foreach ( $taxonomies as $taxonomy ) { $tax = get_taxonomy( $taxonomy ); // Should return an object with the correct taxonomy object type. $this->assertIsObject( $tax ); @@ -106,7 +126,11 @@ public function test_the_taxonomies_term_template() { } public function test_get_link_taxonomy() { - foreach ( get_object_taxonomies( 'link' ) as $taxonomy ) { + $taxonomies = get_object_taxonomies( 'link' ); + + $this->assertNotEmpty( $taxonomies ); + + foreach ( $taxonomies as $taxonomy ) { $tax = get_taxonomy( $taxonomy ); // Should return an object with the correct taxonomy object type. $this->assertIsObject( $tax ); @@ -119,6 +143,7 @@ public function test_taxonomy_exists_known() { $this->assertTrue( taxonomy_exists( 'category' ) ); $this->assertTrue( taxonomy_exists( 'post_tag' ) ); $this->assertTrue( taxonomy_exists( 'link_category' ) ); + $this->assertTrue( taxonomy_exists( 'wp_pattern_category' ) ); } public function test_taxonomy_exists_unknown() { @@ -128,6 +153,41 @@ public function test_taxonomy_exists_unknown() { $this->assertFalse( taxonomy_exists( null ) ); } + /** + * Tests that `taxonomy_exists()` returns `false` when the `$taxonomy` + * argument is not a string. + * + * @ticket 56338 + * + * @covers ::taxonomy_exists + * + * @dataProvider data_taxonomy_exists_should_return_false_with_non_string_taxonomy + * + * @param mixed $taxonomy The non-string taxonomy. + */ + public function test_taxonomy_exists_should_return_false_with_non_string_taxonomy( $taxonomy ) { + $this->assertFalse( taxonomy_exists( $taxonomy ) ); + } + + /** + * Data provider with non-string values. + * + * @return array + */ + public function data_taxonomy_exists_should_return_false_with_non_string_taxonomy() { + return array( + 'array' => array( array() ), + 'object' => array( new stdClass() ), + 'bool (true)' => array( true ), + 'bool (false)' => array( false ), + 'null' => array( null ), + 'integer (0)' => array( 0 ), + 'integer (1)' => array( 1 ), + 'float (0.0)' => array( 0.0 ), + 'float (1.1)' => array( 1.1 ), + ); + } + public function test_is_taxonomy_hierarchical() { $this->assertTrue( is_taxonomy_hierarchical( 'category' ) ); $this->assertFalse( is_taxonomy_hierarchical( 'post_tag' ) ); @@ -144,7 +204,7 @@ public function test_is_taxonomy_hierarchical_unknown() { public function test_register_taxonomy() { // Make up a new taxonomy name, and ensure it's unused. - $tax = rand_str(); + $tax = 'tax_new'; $this->assertFalse( taxonomy_exists( $tax ) ); register_taxonomy( $tax, 'post' ); @@ -158,7 +218,7 @@ public function test_register_taxonomy() { public function test_register_hierarchical_taxonomy() { // Make up a new taxonomy name, and ensure it's unused. - $tax = rand_str(); + $tax = 'tax_new'; $this->assertFalse( taxonomy_exists( $tax ) ); register_taxonomy( $tax, 'post', array( 'hierarchical' => true ) ); @@ -221,6 +281,22 @@ public function test_register_taxonomy_show_in_quick_edit_should_default_to_valu $this->assertFalse( $tax_2->show_in_quick_edit ); } + /** + * @ticket 53212 + */ + public function test_register_taxonomy_fires_registered_actions() { + $taxonomy = 'taxonomy53212'; + $action = new MockAction(); + + add_action( 'registered_taxonomy', array( $action, 'action' ) ); + add_action( "registered_taxonomy_{$taxonomy}", array( $action, 'action' ) ); + + register_taxonomy( $taxonomy, 'post' ); + register_taxonomy( 'random', 'post' ); + + $this->assertSame( 3, $action->get_call_count() ); + } + /** * @ticket 11058 */ @@ -233,7 +309,7 @@ public function test_registering_taxonomies_to_object_types() { // Create a post type to test with. $post_type = 'test_cpt'; $this->assertFalse( get_post_type( $post_type ) ); - $this->assertObjectHasAttribute( 'name', register_post_type( $post_type ) ); + $this->assertObjectHasProperty( 'name', register_post_type( $post_type ) ); // Core taxonomy, core post type. $this->assertTrue( unregister_taxonomy_for_object_type( 'category', 'post' ) ); @@ -267,7 +343,6 @@ public function test_registering_taxonomies_to_object_types() { unset( $GLOBALS['wp_taxonomies'][ $tax ] ); _unregister_post_type( $post_type ); - } /** @@ -630,13 +705,13 @@ public function test_it_should_be_possible_to_register_a_query_var_that_matches_ 'publicly_queryable' => false, ) ); - $t = $this->factory->term->create_and_get( + $t = self::factory()->term->create_and_get( array( 'taxonomy' => 'wptests_tax', ) ); - $p = $this->factory->post->create(); + $p = self::factory()->post->create(); wp_set_object_terms( $p, $t->slug, 'wptests_tax' ); add_filter( 'do_parse_request', array( $this, 'register_query_var' ) ); @@ -947,7 +1022,7 @@ public function test_edit_post_hierarchical_taxonomy() { ) ); - wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) ); + wp_set_current_user( self::$editor_id ); $updated_post_id = edit_post( array( 'post_ID' => $post->ID, @@ -973,7 +1048,7 @@ public function test_edit_post_hierarchical_taxonomy() { */ public function test_default_term_for_custom_taxonomy() { - wp_set_current_user( self::factory()->user->create( array( 'role' => 'editor' ) ) ); + wp_set_current_user( self::$editor_id ); $tax = 'custom-tax'; @@ -1028,9 +1103,6 @@ public function test_default_term_for_custom_taxonomy() { wp_set_object_terms( $post_id, array(), $tax ); $term = wp_get_post_terms( $post_id, $tax ); $this->assertSame( array(), $term ); - - unregister_taxonomy( $tax ); - $this->assertSame( get_option( 'default_term_' . $tax ), false ); } /** diff --git a/tests/phpunit/tests/taxonomy/cleanTaxonomyCache.php b/tests/phpunit/tests/taxonomy/cleanTaxonomyCache.php new file mode 100644 index 0000000000000..0ac9c14a6a8aa --- /dev/null +++ b/tests/phpunit/tests/taxonomy/cleanTaxonomyCache.php @@ -0,0 +1,74 @@ +options WHERE option_name = 'post_tag_children'" === $query ) { + $delete_post_tag_children_called = true; + } + return $query; + } + ); + + clean_taxonomy_cache( 'post_tag' ); + + $this->assertFalse( $delete_post_tag_children_called, 'An attempt to clear the post_tag_children option should not be made.' ); + } + + /** + * Ensure clearing the cache of a hierarchical taxonomy does attempt to delete the hierarchy cache. + * + * @ticket 64090 + */ + public function test_hierarchy_cache_cleared_for_hierarchical_taxonomy() { + // Prime the category cache by inserting terms. + wp_insert_term( 'foo', 'category' ); + + /* + * Determine if delete_option( "{$taxonomy}_children" ) is called. + * + * None of the actions in delete_option() or _get_term_hierarchy() fire for + * non-hierarchical taxonomies, so the query filter needs to be used to determine + * if an attempt to delete the option was made. + */ + $delete_category_children_called = false; + add_filter( + 'query', + static function ( $query ) use ( &$delete_category_children_called ) { + global $wpdb; + if ( "SELECT autoload FROM $wpdb->options WHERE option_name = 'category_children'" === $query ) { + $delete_category_children_called = true; + } + return $query; + } + ); + + clean_taxonomy_cache( 'category' ); + + $this->assertTrue( $delete_category_children_called, 'An attempt to clear the category_children option should be made.' ); + } +} diff --git a/tests/phpunit/tests/template.php b/tests/phpunit/tests/template.php index c807dc469b545..a79702554dc64 100644 --- a/tests/phpunit/tests/template.php +++ b/tests/phpunit/tests/template.php @@ -63,8 +63,43 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { ); } + /** + * @var WP_Scripts|null + */ + protected $original_wp_scripts; + + /** + * @var WP_Styles|null + */ + protected $original_wp_styles; + + /** + * @var array|null + */ + protected $original_theme_features; + + /** + * @var array + */ + const RESTORED_CONFIG_OPTIONS = array( + 'display_errors', + 'error_reporting', + 'log_errors', + 'error_log', + 'default_mimetype', + 'html_errors', + 'error_prepend_string', + 'error_append_string', + ); + + /** + * @var array + */ + protected $original_ini_config; + public function set_up() { parent::set_up(); + register_post_type( 'cpt', array( @@ -80,12 +115,43 @@ public function set_up() { ) ); $this->set_permalink_structure( '/%year%/%monthnum%/%day%/%postname%/' ); + + // Remove hooks which are added by wp_load_classic_theme_block_styles_on_demand() during bootstrapping. + remove_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true', 0 ); + remove_filter( 'should_load_separate_core_block_assets', '__return_true', 0 ); + remove_filter( 'should_load_block_assets_on_demand', '__return_true', 0 ); + remove_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ); + + global $wp_scripts, $wp_styles; + $this->original_wp_scripts = $wp_scripts; + $this->original_wp_styles = $wp_styles; + $wp_scripts = null; + $wp_styles = null; + + foreach ( self::RESTORED_CONFIG_OPTIONS as $option ) { + $this->original_ini_config[ $option ] = ini_get( $option ); + } } public function tear_down() { + global $wp_scripts, $wp_styles; + $wp_scripts = $this->original_wp_scripts; + $wp_styles = $this->original_wp_styles; + + foreach ( $this->original_ini_config as $option => $value ) { + ini_set( $option, $value ); + } + unregister_post_type( 'cpt' ); unregister_taxonomy( 'taxo' ); $this->set_permalink_structure( '' ); + + $registry = WP_Block_Type_Registry::get_instance(); + if ( $registry->is_registered( 'third-party/test' ) ) { + $registry->unregister( 'third-party/test' ); + } + + unset( $GLOBALS['_wp_tests_development_mode'] ); parent::tear_down(); } @@ -177,6 +243,7 @@ public function test_taxonomy_template_hierarchy() { array( 'taxonomy-taxo-foo-😀.php', 'taxonomy-taxo-foo-%f0%9f%98%80.php', + "taxonomy-taxo-{$term->term_id}.php", 'taxonomy-taxo.php', 'taxonomy.php', 'archive.php', @@ -456,6 +523,1633 @@ public function test_embed_template_hierarchy_for_page() { ); } + /** + * Tests that `locate_template()` uses the current theme even after switching the theme. + * + * @ticket 18298 + * + * @covers ::locate_template + */ + public function test_locate_template_uses_current_theme() { + $themes = wp_get_themes(); + + // Look for parent themes with an index.php template. + $relevant_themes = array(); + foreach ( $themes as $theme ) { + if ( $theme->get_stylesheet() !== $theme->get_template() ) { + continue; + } + $php_templates = $theme['Template Files']; + if ( ! isset( $php_templates['index.php'] ) ) { + continue; + } + $relevant_themes[] = $theme; + } + if ( count( $relevant_themes ) < 2 ) { + $this->markTestSkipped( 'Test requires at least two parent themes with an index.php template.' ); + } + + $template_names = array( 'index.php' ); + + $old_theme = $relevant_themes[0]; + $new_theme = $relevant_themes[1]; + + switch_theme( $old_theme->get_stylesheet() ); + $this->assertSame( $old_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in initial theme.' ); + + switch_theme( $new_theme->get_stylesheet() ); + $this->assertSame( $new_theme->get_stylesheet_directory() . '/index.php', locate_template( $template_names ), 'Incorrect index template found in theme after switch.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer in a block theme when no filters are present. + * + * @ticket 43258 + * @ticket 64099 + * + * @covers ::wp_should_output_buffer_template_for_enhancement + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_load_classic_theme_block_styles_on_demand + */ + public function test_wp_start_template_enhancement_output_buffer_without_filters_and_no_override_in_block_theme(): void { + switch_theme( 'block-theme' ); + wp_load_classic_theme_block_styles_on_demand(); + + $level = ob_get_level(); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return false when there are no wp_template_enhancement_output_buffer filters added.' ); + $this->assertFalse( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return false because the output buffer should not be started.' ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' ); + $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() does start a buffer in classic theme. + * + * @ticket 43258 + * @ticket 64099 + * + * @covers ::wp_should_output_buffer_template_for_enhancement + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_load_classic_theme_block_styles_on_demand + */ + public function test_wp_start_template_enhancement_output_buffer_in_classic_theme(): void { + switch_theme( 'default' ); + wp_load_classic_theme_block_styles_on_demand(); + + $level = ob_get_level(); + $this->assertTrue( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return true because wp_load_classic_theme_block_styles_on_demand() adds wp_template_enhancement_output_buffer filters.' ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true because the output buffer should be started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $level + 1, ob_get_level(), 'Expected the initial output buffer level to be incremented by one.' ); + ob_end_clean(); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() does start a buffer when no filters are present but there is an override. + * + * @ticket 43258 + * @covers ::wp_should_output_buffer_template_for_enhancement + * @covers ::wp_start_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_begins_without_filters_but_overridden(): void { + $level = ob_get_level(); + add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_true' ); + $this->assertTrue( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return true when overridden with the wp_should_output_buffer_template_for_enhancement filter.' ); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true because the output buffer should be started due to the override.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); + ob_end_clean(); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() does not start a buffer even when there are filters present due to override. + * + * @ticket 43258 + * + * @covers ::wp_should_output_buffer_template_for_enhancement + * @covers ::wp_start_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_begins_with_filters_but_blocked(): void { + add_filter( + 'wp_template_enhancement_output_buffer', + static function () { + return 'Hey!'; + } + ); + $level = ob_get_level(); + add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' ); + $this->assertFalse( wp_should_output_buffer_template_for_enhancement(), 'Expected wp_should_output_buffer_template_for_enhancement() to return false since wp_should_output_buffer_template_for_enhancement was filtered to be false even though there is a wp_template_enhancement_output_buffer filter added.' ); + $this->assertFalse( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return false because the output buffer should not be started.' ); + $this->assertSame( 0, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to not have fired.' ); + $this->assertSame( $level, ob_get_level(), 'Expected the initial output buffer level to be unchanged.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the expected hooks fire for + * an HTML document and that the response is not incrementally flushable. + * + * @ticket 43258 + * @ticket 64126 + * + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_for_html(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $mock_filter_callback = new MockAction(); + add_filter( + 'wp_template_enhancement_output_buffer', + array( $mock_filter_callback, 'filter' ), + 10, + PHP_INT_MAX + ); + + $mock_action_callback = new MockAction(); + add_filter( + 'wp_finalized_template_enhancement_output_buffer', + array( $mock_action_callback, 'action' ), + 10, + PHP_INT_MAX + ); + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ): string { + $p = WP_HTML_Processor::create_full_parser( $buffer ); + while ( $p->next_tag() ) { + switch ( $p->get_tag() ) { + case 'HTML': + $p->set_attribute( 'lang', 'es' ); + break; + case 'TITLE': + $p->set_modifiable_text( 'Saludo' ); + break; + case 'H1': + if ( $p->next_token() && '#text' === $p->get_token_name() ) { + $p->set_modifiable_text( '¡Hola, mundo!' ); + } + break; + } + } + return $p->get_updated_html(); + } + ); + + $this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' ); + ini_set( 'default_mimetype', 'text/html' ); // Since sending a header won't work. + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' ); + + ?> + + + + Greeting + + assertFalse( + @ob_flush(), // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + 'Expected output buffer to not be incrementally flushable.' + ); + ?> + +

      Hello World!

      + + + assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); + $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); + + ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); + + $this->assertSame( 1, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); + $filter_args = $mock_filter_callback->get_args()[0]; + $this->assertIsArray( $filter_args, 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); + $this->assertCount( 2, $filter_args, 'Expected two args to be supplied to the wp_template_enhancement_output_buffer filter.' ); + $this->assertIsString( $filter_args[0], 'Expected the $filtered_output param to the wp_template_enhancement_output_buffer filter to be a string.' ); + $this->assertIsString( $filter_args[1], 'Expected the $output param to the wp_template_enhancement_output_buffer filter to be a string.' ); + $this->assertSame( $filter_args[1], $filter_args[0], 'Expected the initial $filtered_output to match $output in the wp_template_enhancement_output_buffer filter.' ); + $original_output = $filter_args[0]; + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( 'Greeting', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '

      Hello World!

      ', $original_output, 'Expected original output to contain string.' ); + $this->assertStringContainsString( '', $original_output, 'Expected original output to contain string.' ); + + $processed_output = ob_get_clean(); // Obtain the output via the wrapper output buffer. + $this->assertIsString( $processed_output ); + $this->assertNotEquals( $original_output, $processed_output ); + + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( 'Saludo', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '

      ¡Hola, mundo!

      ', $processed_output, 'Expected processed output to contain string.' ); + $this->assertStringContainsString( '', $processed_output, 'Expected processed output to contain string.' ); + + $this->assertSame( 1, did_action( 'wp_finalized_template_enhancement_output_buffer' ), 'Expected the wp_finalized_template_enhancement_output_buffer action to have fired.' ); + $this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_finalized_template_enhancement_output_buffer action callback to have been called once.' ); + $action_args = $mock_action_callback->get_args()[0]; + $this->assertCount( 1, $action_args, 'Expected the wp_finalized_template_enhancement_output_buffer action to have been passed only one argument.' ); + $this->assertSame( $processed_output, $action_args[0], 'Expected the arg passed to wp_finalized_template_enhancement_output_buffer to be the same as the processed output buffer.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer but ending with cleaning prevents any processing. + * + * @ticket 43258 + * @ticket 64126 + * + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_ended_cleaned(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $mock_filter_callback = new MockAction(); + add_filter( + 'wp_template_enhancement_output_buffer', + array( $mock_filter_callback, 'filter' ) + ); + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ): string { + $p = WP_HTML_Processor::create_full_parser( $buffer ); + if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { + $p->set_modifiable_text( 'Processed' ); + } + return $p->get_updated_html(); + } + ); + + $mock_action_callback = new MockAction(); + add_filter( + 'wp_finalized_template_enhancement_output_buffer', + array( $mock_action_callback, 'action' ), + 10, + PHP_INT_MAX + ); + + $this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' ); + ini_set( 'default_mimetype', 'text/html' ); // Since sending a header won't work. + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented' ); + + ?> + + + + Unprocessed + + +

      Hello World!

      + + + + + + Output Buffer Not Processed + + +

      Template rendering aborted!!!

      + + + assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); + + $this->assertSame( 0, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertStringNotContainsString( 'Unprocessed', $output, 'Expected output buffer to not have string since the template was overridden.' ); + $this->assertStringNotContainsString( 'Processed', $output, 'Expected output buffer to not have string since the filter did not apply.' ); + $this->assertStringContainsString( 'Output Buffer Not Processed', $output, 'Expected output buffer to have string since the output buffer was ended with cleaning.' ); + + $this->assertSame( 0, did_action( 'wp_finalized_template_enhancement_output_buffer' ), 'Expected the wp_finalized_template_enhancement_output_buffer action to not have fired.' ); + $this->assertSame( 0, $mock_action_callback->get_call_count(), 'Expected wp_finalized_template_enhancement_output_buffer action callback to have been called once.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and cleaning allows the template to be replaced. + * + * @ticket 43258 + * @ticket 64126 + * + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_cleaned_and_replaced(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $mock_filter_callback = new MockAction(); + add_filter( + 'wp_template_enhancement_output_buffer', + array( $mock_filter_callback, 'filter' ) + ); + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ): string { + $p = WP_HTML_Processor::create_full_parser( $buffer ); + if ( $p->next_tag( array( 'tag_name' => 'TITLE' ) ) ) { + $p->set_modifiable_text( 'Processed' ); + } + return $p->get_updated_html(); + } + ); + + $mock_action_callback = new MockAction(); + add_filter( + 'wp_finalized_template_enhancement_output_buffer', + array( $mock_action_callback, 'action' ), + 10, + PHP_INT_MAX + ); + + $this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' ); + ini_set( 'default_mimetype', 'application/xhtml+xml' ); // Since sending a header won't work. + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); + + ?> + + + + + Unprocessed + + +

      Hello World!

      + + + '; ?> + + + + + Template Replaced + + +

      Template Replaced

      +

      The original template called ob_clean() which allowed this template to take its place.

      + + + assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); + + $this->assertSame( 1, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to have applied.' ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertStringNotContainsString( 'Unprocessed', $output, 'Expected output buffer to not have string due to template override.' ); + $this->assertStringContainsString( 'Processed', $output, 'Expected output buffer to have string due to filtering.' ); + $this->assertStringContainsString( '

      Template Replaced

      ', $output, 'Expected output buffer to have string due to replaced template.' ); + + $this->assertSame( 1, did_action( 'wp_finalized_template_enhancement_output_buffer' ), 'Expected the wp_finalized_template_enhancement_output_buffer action to have fired.' ); + $this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_finalized_template_enhancement_output_buffer action callback to have been called once.' ); + $action_args = $mock_action_callback->get_args()[0]; + $this->assertCount( 1, $action_args, 'Expected the wp_finalized_template_enhancement_output_buffer action to have been passed only one argument.' ); + $this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_finalized_template_enhancement_output_buffer to be the same as the processed output buffer.' ); + } + + /** + * Tests that wp_start_template_enhancement_output_buffer() starts the expected output buffer and that the output buffer is not processed. + * + * @ticket 43258 + * @ticket 64126 + * + * @covers ::wp_start_template_enhancement_output_buffer + * @covers ::wp_finalize_template_enhancement_output_buffer + */ + public function test_wp_start_template_enhancement_output_buffer_for_json(): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + $mock_filter_callback = new MockAction(); + add_filter( 'wp_template_enhancement_output_buffer', array( $mock_filter_callback, 'filter' ) ); + + $mock_action_callback = new MockAction(); + add_filter( + 'wp_finalized_template_enhancement_output_buffer', + array( $mock_action_callback, 'action' ), + 10, + PHP_INT_MAX + ); + + $initial_ob_level = ob_get_level(); + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + $this->assertSame( 1, did_action( 'wp_template_enhancement_output_buffer_started' ), 'Expected the wp_template_enhancement_output_buffer_started action to have fired.' ); + $this->assertSame( $initial_ob_level + 1, ob_get_level(), 'Expected the output buffer level to have been incremented.' ); + + $this->assertCount( 0, headers_list(), 'Expected no headers to have been sent during unit tests.' ); + ini_set( 'default_mimetype', 'application/json' ); // Since sending a header won't work. + + $json = wp_json_encode( + array( + 'success' => true, + 'data' => array( + 'message' => 'Hello, world!', + 'fish' => '<', // Something that looks like HTML. + ), + ) + ); + echo $json; + + $ob_status = ob_get_status(); + $this->assertSame( 'wp_finalize_template_enhancement_output_buffer', $ob_status['name'], 'Expected name to be WP function.' ); + $this->assertSame( 1, $ob_status['type'], 'Expected type to be user supplied handler.' ); + $this->assertSame( 0, $ob_status['chunk_size'], 'Expected unlimited chunk size.' ); + + ob_end_flush(); // End the buffer started by wp_start_template_enhancement_output_buffer(). + $this->assertSame( $initial_ob_level, ob_get_level(), 'Expected the output buffer to be back at the initial level.' ); + + $this->assertSame( 0, $mock_filter_callback->get_call_count(), 'Expected the wp_template_enhancement_output_buffer filter to not have applied.' ); + + // Obtain the output via the wrapper output buffer. + $output = ob_get_clean(); + $this->assertIsString( $output, 'Expected ob_get_clean() to return a string.' ); + $this->assertSame( $json, $output, 'Expected output to not be processed.' ); + + $this->assertSame( 1, did_action( 'wp_finalized_template_enhancement_output_buffer' ), 'Expected the wp_finalized_template_enhancement_output_buffer action to have fired even though the wp_template_enhancement_output_buffer filter did not apply.' ); + $this->assertSame( 1, $mock_action_callback->get_call_count(), 'Expected wp_finalized_template_enhancement_output_buffer action callback to have been called once.' ); + $action_args = $mock_action_callback->get_args()[0]; + $this->assertCount( 1, $action_args, 'Expected the wp_finalized_template_enhancement_output_buffer action to have been passed only one argument.' ); + $this->assertSame( $output, $action_args[0], 'Expected the arg passed to wp_finalized_template_enhancement_output_buffer to be the same as the processed output buffer.' ); + } + + /** + * Data provider for test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing. + * + * @return array, + * emit_filter_errors: Closure, + * emit_action_errors: Closure, + * expected_processed: bool, + * expected_error_log: string[], + * expected_displayed_errors: string[], + * }> + */ + public function data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing(): array { + $log_and_display_all = array( + 'error_reporting' => E_ALL, + 'display_errors' => true, + 'log_errors' => true, + 'html_errors' => true, + ); + + $tests = array( + 'deprecated' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + ), + ), + 'notice' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + ), + 'warning' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + ), + 'error' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during filter?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'emit_action_errors' => static function () { + @trigger_error( 'ERROR: Can this mistake be rectified during action?', E_USER_ERROR ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during filter? in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: User error triggered: ERROR: Can this mistake be rectified during action? in __FILE__ on line __LINE__', + ), + ), + 'exception' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + throw new Exception( 'I take exception to this filter!' ); + }, + 'emit_action_errors' => static function () { + throw new Exception( 'I take exception to this action!' ); + }, + 'expected_processed' => false, + 'expected_error_log' => array( + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'PHP Warning: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Error: Uncaught "Exception" thrown: I take exception to this filter! in __FILE__ on line __LINE__', + 'Error: Uncaught "Exception" thrown: I take exception to this action! in __FILE__ on line __LINE__', + ), + ), + 'multiple_non_errors' => array( + 'ini_config_options' => $log_and_display_all, + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during filter.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'You are history during action.', E_USER_DEPRECATED ); + trigger_error( 'POSTED: No trespassing during action.', E_USER_NOTICE ); + trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'PHP Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ on line __LINE__', + 'Deprecated: You are history during action. in __FILE__ on line __LINE__', + 'Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ on line __LINE__', + ), + ), + 'deprecated_without_html' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'html_errors' => false, + ) + ), + 'emit_filter_errors' => static function () { + trigger_error( 'You are history during filter.', E_USER_DEPRECATED ); + }, + 'emit_action_errors' => null, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Deprecated: You are history during filter. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Deprecated: You are history during filter. in __FILE__ on line __LINE__', + ), + ), + 'warning_in_eval_with_prepend_and_append' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'error_prepend_string' => '
      PHP Problem!', + 'error_append_string' => '
      ', + ) + ), + 'emit_filter_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante filtro.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'emit_action_errors' => static function () { + eval( "trigger_error( 'AVISO: Piso mojado durante acción.', E_USER_WARNING );" ); // phpcs:ignore Squiz.PHP.Eval.Discouraged -- We're in a test! + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'PHP Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', + ), + 'expected_displayed_errors' => array( + 'Warning: AVISO: Piso mojado durante filtro. in __FILE__ : eval()\'d code on line __LINE__', + 'Warning: AVISO: Piso mojado durante acción. in __FILE__ : eval()\'d code on line __LINE__', + ), + ), + 'notice_with_display_errors_stderr' => array( + 'ini_config_options' => array_merge( + $log_and_display_all, + array( + 'display_errors' => 'stderr', + ) + ), + 'emit_filter_errors' => static function () { + trigger_error( 'POSTED: No trespassing during filter.' ); + }, + 'emit_action_errors' => static function () { + trigger_error( 'POSTED: No trespassing during action.' ); + }, + 'expected_processed' => true, + 'expected_error_log' => array( + 'PHP Notice: POSTED: No trespassing during filter. in __FILE__ on line __LINE__', + 'PHP Notice: POSTED: No trespassing during action. in __FILE__ on line __LINE__', + ), + 'expected_displayed_errors' => array(), + ), + ); + + $tests_error_reporting_warnings_and_above = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['error_reporting'] = E_ALL ^ E_USER_NOTICE ^ E_USER_DEPRECATED; + + $test['expected_error_log'] = array_values( + array_filter( + $test['expected_error_log'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $test['expected_displayed_errors'] = array_values( + array_filter( + $test['expected_displayed_errors'], + static function ( $log_entry ) { + return ! ( str_contains( $log_entry, 'Notice' ) || str_contains( $log_entry, 'Deprecated' ) ); + } + ) + ); + + $tests_error_reporting_warnings_and_above[ "{$name}_with_warnings_and_above_reported" ] = $test; + } + + $tests_without_display_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['expected_displayed_errors'] = array(); + + $tests_without_display_errors[ "{$name}_without_display_errors" ] = $test; + } + + $tests_without_display_or_log_errors = array(); + foreach ( $tests as $name => $test ) { + $test['ini_config_options']['display_errors'] = false; + $test['ini_config_options']['log_errors'] = false; + $test['expected_displayed_errors'] = array(); + $test['expected_error_log'] = array(); + + $tests_without_display_or_log_errors[ "{$name}_without_display_errors_or_log_errors" ] = $test; + } + + return array_merge( $tests, $tests_error_reporting_warnings_and_above, $tests_without_display_errors, $tests_without_display_or_log_errors ); + } + + /** + * Tests that errors are handled as expected when errors are emitted when filtering wp_template_enhancement_output_buffer or doing the wp_finalize_template_enhancement_output_buffer action. + * + * @ticket 43258 + * @ticket 64108 + * + * @covers ::wp_finalize_template_enhancement_output_buffer + * + * @dataProvider data_provider_to_test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing + */ + public function test_wp_finalize_template_enhancement_output_buffer_with_errors_while_processing( array $ini_config_options, ?Closure $emit_filter_errors, ?Closure $emit_action_errors, bool $expected_processed, array $expected_error_log, array $expected_displayed_errors ): void { + // Start a wrapper output buffer so that we can flush the inner buffer. + ob_start(); + + ini_set( 'error_log', $this->temp_filename() ); + foreach ( $ini_config_options as $config => $option ) { + ini_set( $config, $option ); + } + + add_filter( + 'wp_template_enhancement_output_buffer', + static function ( string $buffer ) use ( $emit_filter_errors ): string { + $buffer = str_replace( 'Hello', 'Goodbye', $buffer ); + if ( $emit_filter_errors ) { + $emit_filter_errors(); + } + return $buffer; + } + ); + + if ( $emit_action_errors ) { + add_action( + 'wp_finalized_template_enhancement_output_buffer', + static function () use ( $emit_action_errors ): void { + $emit_action_errors(); + } + ); + } + + $this->assertTrue( wp_start_template_enhancement_output_buffer(), 'Expected wp_start_template_enhancement_output_buffer() to return true indicating the output buffer started.' ); + + ?> + + + + Greeting + + +

      Hello World!

      + + + assertStringContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to have been processed.' ); + } else { + $this->assertStringNotContainsString( 'Goodbye', $processed_output, 'Expected the output buffer to not have been processed.' ); + } + + $actual_error_log = array_values( + array_map( + static function ( string $error_log_entry ): string { + $error_log_entry = preg_replace( + '/^\[.+?] /', + '', + $error_log_entry + ); + $error_log_entry = preg_replace( + '#(?<= in ).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '__FILE__', + $error_log_entry + ); + return preg_replace( + '#(?<= on line )\d+#', + '__LINE__', + $error_log_entry + ); + }, + array_filter( explode( "\n", trim( file_get_contents( ini_get( 'error_log' ) ) ) ) ) + ) + ); + + $this->assertSame( + $expected_error_log, + $actual_error_log, + 'Expected same error log entries. Snapshot: ' . var_export( $actual_error_log, true ) + ); + + $displayed_errors = array_values( + array_map( + static function ( string $displayed_error ): string { + $displayed_error = str_replace( '
      ', '', $displayed_error ); + $displayed_error = preg_replace( + '#( in (?:)?).+?' . preg_quote( basename( __FILE__ ), '#' ) . '(\(\d+\))?#', + '$1__FILE__', + $displayed_error + ); + return preg_replace( + '#( on line (?:)?)\d+#', + '$1__LINE__', + $displayed_error + ); + }, + array_filter( + explode( "\n", trim( $processed_output ) ), + static function ( $line ): bool { + return str_contains( $line, ' in ' ); + } + ) + ) + ); + + $this->assertSame( + $expected_displayed_errors, + $displayed_errors, + 'Expected the displayed errors to be the same. Snapshot: ' . var_export( $displayed_errors, true ) + ); + + if ( count( $expected_displayed_errors ) > 0 ) { + $this->assertStringEndsNotWith( '', rtrim( $processed_output ), 'Expected the output to have the error displayed.' ); + } else { + $this->assertStringEndsWith( '', rtrim( $processed_output ), 'Expected the output to not have the error displayed.' ); + } + } + + /** + * Tests that wp_load_classic_theme_block_styles_on_demand() does not add hooks for block themes. + * + * @ticket 64099 + * @covers ::wp_load_classic_theme_block_styles_on_demand + */ + public function test_wp_load_classic_theme_block_styles_on_demand_in_block_theme(): void { + switch_theme( 'block-theme' ); + + wp_load_classic_theme_block_styles_on_demand(); + + $this->assertFalse( has_filter( 'should_load_separate_core_block_assets' ), 'Expect should_load_separate_core_block_assets filter NOT to be added for block themes.' ); + $this->assertFalse( has_filter( 'should_load_block_assets_on_demand', '__return_true' ), 'Expect should_load_block_assets_on_demand filter NOT to be added for block themes.' ); + $this->assertFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expect wp_template_enhancement_output_buffer_started action NOT to be added for block themes.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_load_classic_theme_block_styles_on_demand(): array { + return array( + 'block_theme' => array( + 'theme' => 'block-theme', + 'set_up' => static function () {}, + 'expected_load_separate' => true, + 'expected_on_demand' => true, + 'expected_buffer_started' => false, + ), + 'classic_theme_with_output_buffer_blocked' => array( + 'theme' => 'default', + 'set_up' => static function () { + add_filter( 'wp_should_output_buffer_template_for_enhancement', '__return_false' ); + }, + 'expected_load_separate' => false, + 'expected_on_demand' => false, + 'expected_buffer_started' => false, + ), + 'classic_theme_with_should_load_separate_core_block_assets_opt_out' => array( + 'theme' => 'default', + 'set_up' => static function () { + add_filter( 'should_load_separate_core_block_assets', '__return_false' ); + }, + 'expected_load_separate' => false, + 'expected_on_demand' => false, + 'expected_buffer_started' => false, + ), + 'classic_theme_with_should_load_block_assets_on_demand_out_out' => array( + 'theme' => 'default', + 'set_up' => static function () { + add_filter( 'should_load_block_assets_on_demand', '__return_false' ); + }, + 'expected_load_separate' => true, + 'expected_on_demand' => false, + 'expected_buffer_started' => false, + ), + 'classic_theme_without_any_opt_out' => array( + 'theme' => 'default', + 'set_up' => static function () {}, + 'expected_load_separate' => true, + 'expected_on_demand' => true, + 'expected_buffer_started' => true, + ), + ); + } + + /** + * Tests that wp_load_classic_theme_block_styles_on_demand() adds the expected hooks (or not). + * + * @ticket 64099 + * @ticket 64150 + * + * @covers ::wp_load_classic_theme_block_styles_on_demand + * + * @dataProvider data_wp_load_classic_theme_block_styles_on_demand + */ + public function test_wp_load_classic_theme_block_styles_on_demand( string $theme, ?Closure $set_up, bool $expected_load_separate, bool $expected_on_demand, bool $expected_buffer_started ) { + $this->assertFalse( wp_should_load_separate_core_block_assets(), 'Expected wp_should_load_separate_core_block_assets() to return false initially.' ); + $this->assertFalse( wp_should_load_block_assets_on_demand(), 'Expected wp_should_load_block_assets_on_demand() to return true' ); + $this->assertFalse( has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expected wp_template_enhancement_output_buffer_started action to be added for classic themes.' ); + + switch_theme( $theme ); + if ( $set_up ) { + $set_up(); + } + + wp_load_classic_theme_block_styles_on_demand(); + _add_default_theme_supports(); + + $this->assertSame( $expected_load_separate, wp_should_load_separate_core_block_assets(), 'Expected wp_should_load_separate_core_block_assets() return value.' ); + $this->assertSame( $expected_on_demand, wp_should_load_block_assets_on_demand(), 'Expected wp_should_load_block_assets_on_demand() return value.' ); + $this->assertSame( $expected_buffer_started, (bool) has_action( 'wp_template_enhancement_output_buffer_started', 'wp_hoist_late_printed_styles' ), 'Expected wp_template_enhancement_output_buffer_started action added status.' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_hoist_late_printed_styles(): array { + $blocks_content = '
      This is only a test!
      '; + + $early_common_styles = array( + 'wp-img-auto-sizes-contain-inline-css', + 'early-css', + 'early-inline-css', + 'wp-emoji-styles-inline-css', + ); + + // Styles enqueued at wp_enqueue_scripts (priority 10). + $common_at_wp_enqueue_scripts = array( + 'normal-css', + 'normal-inline-css', + ); + + $common_late_in_head = array( + // Styles printed at wp_head priority 101. + 'wp-custom-css', + ); + + $common_late_in_body = array( + 'late-css', + 'late-inline-css', + 'core-block-supports-inline-css', + ); + + $common_expected_head_styles = array_merge( + $early_common_styles, + array( + // Core block styles enqueued by wp_common_block_scripts_and_styles(), which runs at wp_enqueue_scripts priority 10, added first. + 'wp-block-library-css', // Inline printed. + 'wp-block-separator-css', // Hoisted. + + // The wp_common_block_scripts_and_styles() function also fires enqueue_block_assets, at which wp_enqueue_classic_theme_styles() runs. + 'classic-theme-styles-css', // Printed at enqueue_block_assets. + + // Third-party block styles. + 'third-party-test-block-css', // Hoisted. + + // Other styles enqueued at enqueue_block_assets, which is fired by wp_common_block_scripts_and_styles(). + 'custom-block-styles-css', // Printed at enqueue_block_assets. + + // Hoisted. Enqueued by wp_enqueue_global_styles() which runs at wp_enqueue_scripts priority 10 and wp_footer priority 1. + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ); + + return array( + 'standard_classic_theme_config_with_min_styles_inlined' => array( + 'set_up' => null, + 'content' => $blocks_content, + 'inline_size_limit' => 0, + 'expected_styles' => array( + 'HEAD' => $common_expected_head_styles, + 'BODY' => array(), + ), + ), + + 'standard_classic_theme_config_with_max_styles_inlined' => array( + 'set_up' => null, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'classic-theme-styles-inline-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), + + 'classic_theme_styles_omitted' => array( + 'set_up' => static function () { + // Note that wp_enqueue_scripts is used instead of enqueue_block_assets because it runs again at the former action. + add_action( + 'wp_enqueue_scripts', + static function () { + wp_dequeue_style( 'classic-theme-styles' ); + }, + 100 + ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), + + 'no_styles_at_enqueued_block_assets' => array( + 'set_up' => static function () { + add_action( + 'wp_enqueue_scripts', + static function () { + wp_dequeue_style( 'classic-theme-styles' ); + wp_dequeue_style( 'custom-block-styles' ); + }, + 100 + ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'third-party-test-block-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), + + 'no_global_styles' => array( + 'set_up' => static function () { + $dequeue = static function () { + wp_dequeue_style( 'global-styles' ); + }; + add_action( 'wp_enqueue_scripts', $dequeue, 1000 ); + add_action( 'wp_footer', $dequeue, 2 ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'classic-theme-styles-inline-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + ), + + 'standard_classic_theme_config_extra_block_library_inline_style_none_inlined' => array( + 'set_up' => static function () { + add_action( + 'enqueue_block_assets', + static function () { + // Extra CSS which prevents empty inline style containing placeholder from being removed. + wp_add_inline_style( 'wp-block-library', '.wp-block-separator{ outline:solid 1px lime; }' ); + } + ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => 0, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-css', + 'wp-block-separator-css', + 'wp-block-library-inline-css-extra', + 'classic-theme-styles-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + 'assert' => function ( string $buffer, string $filtered_buffer ) { + $block_separator_core_style_span = null; + $block_separator_custom_style_span = null; + $processor = new class( $filtered_buffer ) extends WP_HTML_Tag_Processor { + public function get_span(): WP_HTML_Span { + $this->set_bookmark( 'here' ); + return $this->bookmarks['here']; + } + }; + while ( $processor->next_tag() ) { + if ( + $processor->get_tag() === 'LINK' && + $processor->get_attribute( 'rel' ) === 'stylesheet' && + $processor->get_attribute( 'id' ) === 'wp-block-separator-css' + ) { + $block_separator_core_style_span = $processor->get_span(); + } elseif ( + $processor->get_tag() === 'STYLE' && + $processor->get_attribute( 'id' ) === 'wp-block-library-inline-css-extra' && + str_contains( $processor->get_modifiable_text(), '.wp-block-separator{ outline:solid 1px lime; }' ) + ) { + $block_separator_custom_style_span = $processor->get_span(); + } + } + + $this->assertInstanceOf( WP_HTML_Span::class, $block_separator_core_style_span, 'Expected the block separator core style to be present.' ); + $this->assertInstanceOf( WP_HTML_Span::class, $block_separator_custom_style_span, 'Expected the block separator custom style to be present.' ); + $this->assertGreaterThan( $block_separator_core_style_span->start, $block_separator_custom_style_span->start, 'Expected the block separator custom style to appear after the block separator stylesheet.' ); + }, + ), + + 'standard_classic_theme_config_extra_block_library_inline_style_all_inlined' => array( + 'set_up' => static function () { + add_action( + 'enqueue_block_assets', + static function () { + // Extra CSS which prevents empty inline style containing placeholder from being removed. + wp_add_inline_style( 'wp-block-library', '.wp-block-separator{ outline:solid 1px lime; }' ); + } + ); + }, + 'content' => $blocks_content, + 'inline_size_limit' => PHP_INT_MAX, + 'expected_styles' => array( + 'HEAD' => array_merge( + $early_common_styles, + array( + 'wp-block-library-inline-css', + 'wp-block-separator-inline-css', + 'wp-block-library-inline-css-extra', + 'classic-theme-styles-inline-css', + 'third-party-test-block-css', + 'custom-block-styles-css', + 'global-styles-inline-css', + ), + $common_at_wp_enqueue_scripts, + $common_late_in_head, + $common_late_in_body + ), + 'BODY' => array(), + ), + 'assert' => function ( string $buffer, string $filtered_buffer ) { + $block_separator_inline_style_start_tag = '', + ), + array( + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, serif', + ), + ), + ), + ), + ), + ), + true + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'typography' => array( + 'fontFamilies' => array( + 'custom' => array( + array( + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, serif', + ), + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + /** + * @ticket 56467 + */ + public function test_get_element_class_name_button() { + $expected = 'wp-element-button'; + $actual = WP_Theme_JSON::get_element_class_name( 'button' ); + + $this->assertSame( $expected, $actual ); + } + + /** + * @ticket 56467 + */ + public function test_get_element_class_name_invalid() { + $expected = ''; + $actual = WP_Theme_JSON::get_element_class_name( 'unknown-element' ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Testing that dynamic properties in theme.json return the value they reference, + * e.g. array( 'ref' => 'styles.color.background' ) => "#ffffff". + * + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61704 + */ + public function test_get_property_value_valid() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => '#000000', + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body{background-color: #ffffff;color: #000000;}:root :where(.wp-element-button, .wp-block-button__link){background-color: #000000;color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Tests that get_property_value() static method returns an empty string + * if the path is invalid or the value is null. + * + * Also, tests that PHP 8.1 "passing null to non-nullable" deprecation notice + * is not thrown when passing the value to strncmp() in the method. + * + * The notice that we should not see: + * `Deprecated: strncmp(): Passing null to parameter #1 ($string1) of type string is deprecated`. + * + * @dataProvider data_get_property_value_should_return_string_for_invalid_paths_or_null_values + * + * @ticket 56620 + * + * @covers WP_Theme_JSON::get_property_value + * + * @param array $styles An array with style definitions. + * @param array $path Path to the desired properties. + */ + public function test_get_property_value_should_return_string_for_invalid_paths_or_null_values( $styles, $path ) { + $reflection_class = new ReflectionClass( WP_Theme_JSON::class ); + + $get_property_value_method = $reflection_class->getMethod( 'get_property_value' ); + if ( PHP_VERSION_ID < 80100 ) { + $get_property_value_method->setAccessible( true ); + } + $result = $get_property_value_method->invoke( null, $styles, $path ); + + $this->assertSame( '', $result ); + } + + /** + * Data provider for test_get_property_value_should_return_string_for_invalid_paths_or_null_values(). + * + * @return array + */ + public function data_get_property_value_should_return_string_for_invalid_paths_or_null_values() { + return array( + 'empty string' => array( + 'styles' => array(), + 'path' => array( 'non_existent_path' ), + ), + 'null' => array( + 'styles' => array( 'some_null_value' => null ), + 'path' => array( 'some_null_value' ), + ), + ); + } + + /** + * Testing that dynamic properties in theme.json that + * refer to other dynamic properties in a loop + * should be left untouched. + * + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61704 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_loop() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.elements.button.color.background' ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body{background-color: #ffffff;}:root :where(.wp-element-button, .wp-block-button__link){color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Testing that dynamic properties in theme.json that + * refer to other dynamic properties + * should be left unprocessed. + * + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61704 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_recursion() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.color.background' ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'background' => array( 'ref' => 'styles.color.text' ), + 'text' => array( 'ref' => 'styles.color.background' ), + ), + ), + ), + ), + ) + ); + + $expected = 'body{background-color: #ffffff;color: #ffffff;}:root :where(.wp-element-button, .wp-block-button__link){color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Testing that dynamic properties in theme.json that + * refer to themselves should be left unprocessed. + * + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61704 + * @expectedIncorrectUsage get_property_value + */ + public function test_get_property_value_self() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'color' => array( + 'background' => '#ffffff', + 'text' => array( 'ref' => 'styles.color.text' ), + ), + ), + ) + ); + + $expected = 'body{background-color: #ffffff;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61304 + * @ticket 61165 + * @ticket 61704 + */ + public function test_get_styles_for_block_with_padding_aware_alignments() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '10px', + 'right' => '12px', + 'bottom' => '10px', + 'left' => '12px', + ), + ), + ), + 'settings' => array( + 'useRootPaddingAwareAlignments' => true, + ), + ) + ); + + $metadata = array( + 'path' => array( 'styles' ), + 'selector' => 'body', + ); + + $expected = ':where(body) { margin: 0; }.wp-site-blocks { padding-top: var(--wp--style--root--padding-top); padding-bottom: var(--wp--style--root--padding-bottom); }.has-global-padding { padding-right: var(--wp--style--root--padding-right); padding-left: var(--wp--style--root--padding-left); }.has-global-padding > .alignfull { margin-right: calc(var(--wp--style--root--padding-right) * -1); margin-left: calc(var(--wp--style--root--padding-left) * -1); }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) { padding-right: 0; padding-left: 0; }.has-global-padding :where(:not(.alignfull.is-layout-flow) > .has-global-padding:not(.wp-block-block, .alignfull)) > .alignfull { margin-left: 0; margin-right: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}body{--wp--style--root--padding-top: 10px;--wp--style--root--padding-right: 12px;--wp--style--root--padding-bottom: 10px;--wp--style--root--padding-left: 12px;}'; + $root_rules = $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ); + $style_rules = $theme_json->get_styles_for_block( $metadata ); + $this->assertSame( $expected, $root_rules . $style_rules ); + } + + /** + * @ticket 56467 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61704 + */ + public function test_get_styles_for_block_without_padding_aware_alignments() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'padding' => array( + 'top' => '10px', + 'right' => '12px', + 'bottom' => '10px', + 'left' => '12px', + ), + ), + ), + ) + ); + + $metadata = array( + 'path' => array( 'styles' ), + 'selector' => 'body', + ); + + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}body{padding-top: 10px;padding-right: 12px;padding-bottom: 10px;padding-left: 12px;}'; + $root_rules = $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ); + $style_rules = $theme_json->get_styles_for_block( $metadata ); + $this->assertSame( $expected, $root_rules . $style_rules ); + } + + /** + * @ticket 56467 + * @ticket 58550 + * @ticket 61165 + */ + public function test_get_styles_with_content_width() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'layout' => array( + 'contentSize' => '800px', + 'wideSize' => '1000px', + ), + ), + ) + ); + + $metadata = array( + 'path' => array( 'settings' ), + 'selector' => 'body', + ); + + $expected = ':root { --wp--style--global--content-size: 800px;--wp--style--global--wide-size: 1000px; }:where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.is-layout-flex){gap: 0.5em;}:where(.is-layout-grid){gap: 0.5em;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){max-width: var(--wp--style--global--content-size);margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignwide{max-width: var(--wp--style--global--wide-size);}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $this->assertSame( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ) ); + } + + /** + * @ticket 56611 + * @ticket 58548 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61829 + */ + public function test_get_styles_with_appearance_tools() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'appearanceTools' => true, + ), + ) + ); + + $metadata = array( + 'path' => array( 'settings' ), + 'selector' => 'body', + ); + + $expected = ':where(body) { margin: 0; }.wp-site-blocks > .alignleft { float: left; margin-right: 2em; }.wp-site-blocks > .alignright { float: right; margin-left: 2em; }.wp-site-blocks > .aligncenter { justify-content: center; margin-left: auto; margin-right: auto; }:where(.wp-site-blocks) > * { margin-block-start: ; margin-block-end: 0; }:where(.wp-site-blocks) > :first-child { margin-block-start: 0; }:where(.wp-site-blocks) > :last-child { margin-block-end: 0; }:root { --wp--style--block-gap: ; }:root :where(.is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-layout-flow) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-layout-constrained) > *{margin-block-start: 1;margin-block-end: 0;}:root :where(.is-layout-flex){gap: 1;}:root :where(.is-layout-grid){gap: 1;}.is-layout-flow > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-flow > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-flow > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > .alignleft{float: left;margin-inline-start: 0;margin-inline-end: 2em;}.is-layout-constrained > .alignright{float: right;margin-inline-start: 2em;margin-inline-end: 0;}.is-layout-constrained > .aligncenter{margin-left: auto !important;margin-right: auto !important;}.is-layout-constrained > :where(:not(.alignleft):not(.alignright):not(.alignfull)){margin-left: auto !important;margin-right: auto !important;}body .is-layout-flex{display: flex;}.is-layout-flex{flex-wrap: wrap;align-items: center;}.is-layout-flex > :is(*, div){margin: 0;}body .is-layout-grid{display: grid;}.is-layout-grid > :is(*, div){margin: 0;}'; + $this->assertSame( $expected, $theme_json->get_root_layout_rules( WP_Theme_JSON::ROOT_BLOCK_SELECTOR, $metadata ) ); + } + + /** + * @ticket 54487 + */ + public function test_sanitization() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'blockGap' => 'valid value', + ), + 'blocks' => array( + 'core/group' => array( + 'spacing' => array( + 'margin' => 'valid value', + 'display' => 'none', + ), + ), + ), + ), + ) + ); + + $actual = $theme_json->get_raw_data(); + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'spacing' => array( + 'blockGap' => 'valid value', + ), + 'blocks' => array( + 'core/group' => array( + 'spacing' => array( + 'margin' => 'valid value', + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + /** + * @ticket 58462 + */ + public function test_sanitize_for_unregistered_style_variations() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'unregisteredVariation' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ) + ); + + $sanitized_theme_json = $theme_json->get_raw_data(); + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ); + $this->assertSameSetsWithIndex( $expected, $sanitized_theme_json, 'Sanitized theme.json styles does not match' ); + } + + /** + * @ticket 61451 + */ + public function test_unwraps_block_style_variations() { + register_block_style( + array( 'core/paragraph', 'core/group' ), + array( + 'name' => 'myVariation', + 'label' => 'My variation', + ) + ); + + $input = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'variations' => array( + 'myVariation' => array( + 'color' => array( + 'background' => 'topLevel', + 'gradient' => 'topLevel', + ), + 'typography' => array( + 'fontFamily' => 'topLevel', + ), + ), + ), + 'blocks' => array( + 'core/paragraph' => array( + 'variations' => array( + 'myVariation' => array( + 'color' => array( + 'background' => 'blockLevel', + 'text' => 'blockLevel', + ), + 'outline' => array( + 'offset' => 'blockLevel', + ), + ), + ), + ), + ), + ), + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'variations' => array( + 'myVariation' => array( + 'color' => array( + 'background' => 'blockLevel', + 'gradient' => 'topLevel', + 'text' => 'blockLevel', + ), + 'typography' => array( + 'fontFamily' => 'topLevel', + ), + 'outline' => array( + 'offset' => 'blockLevel', + ), + ), + ), + ), + 'core/group' => array( + 'variations' => array( + 'myVariation' => array( + 'color' => array( + 'background' => 'topLevel', + 'gradient' => 'topLevel', + ), + 'typography' => array( + 'fontFamily' => 'topLevel', + ), + ), + ), + ), + ), + ), + ); + $this->assertSameSetsWithIndex( $expected, $input->get_raw_data(), 'Unwrapped block style variations do not match' ); + } + + /** + * @ticket 57583 + * + * @dataProvider data_sanitize_for_block_with_style_variations + * + * @param array $theme_json_variations Theme.json variations to test. + * @param array $expected_sanitized Expected results after sanitizing. + */ + public function test_sanitize_for_block_with_style_variations( $theme_json_variations, $expected_sanitized ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate structure is sanitized. + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertIsArray( $sanitized_theme_json, 'Sanitized theme.json is not an array data type' ); + $this->assertArrayHasKey( 'styles', $sanitized_theme_json, 'Sanitized theme.json does not have an "styles" key' ); + $this->assertSameSetsWithIndex( $expected_sanitized, $sanitized_theme_json['styles'], 'Sanitized theme.json styles does not match' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_for_block_with_style_variations() { + return array( + '1 variation with 1 valid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ), + '1 variation with 2 invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + ), + ), + 'expected_sanitized' => array( + 'blocks' => array( + 'core/quote' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + ), + ), + ), + ); + } + + /** + * Tests that invalid properties are removed from the theme.json inside indexed arrays as settings.typography.fontFamilies. + * + * @ticket 60360 + */ + public function test_sanitize_indexed_arrays() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'badKey2' => 'I am Evil!', + 'settings' => array( + 'badKey3' => 'I am Evil!', + 'typography' => array( + 'badKey4' => 'I am Evil!', + 'fontFamilies' => array( + 'custom' => array( + array( + 'badKey4' => 'I am Evil!', + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, sans-serif', + ), + ), + 'theme' => array( + array( + 'badKey5' => 'I am Evil!', + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => array( + array( + 'badKey6' => 'I am Evil!', + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'badKey7' => 'I am Evil!', + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + array( + 'badKey8' => 'I am Evil!', + 'name' => 'Inter', + 'slug' => 'Inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'badKey9' => 'I am Evil!', + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'badKey10' => 'I am Evil!', + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + ), + ), + ), + ), + ) + ); + + $expected_sanitized = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'typography' => array( + 'fontFamilies' => array( + 'custom' => array( + array( + 'name' => 'Arial', + 'slug' => 'arial', + 'fontFamily' => 'Arial, sans-serif', + ), + ), + 'theme' => array( + array( + 'name' => 'Piazzolla', + 'slug' => 'piazzolla', + 'fontFamily' => 'Piazzolla', + 'fontFace' => array( + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'fontFamily' => 'Piazzolla', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + array( + 'name' => 'Inter', + 'slug' => 'Inter', + 'fontFamily' => 'Inter', + 'fontFace' => array( + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + array( + 'fontFamily' => 'Inter', + 'fontStyle' => 'italic', + 'fontWeight' => '400', + 'src' => 'https://example.com/font.ttf', + ), + ), + ), + ), + ), + ), + ), + ); + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertSameSetsWithIndex( $expected_sanitized, $sanitized_theme_json, 'Sanitized theme.json does not match' ); + } + + /** + * @ticket 57583 + * + * @dataProvider data_sanitize_with_invalid_style_variation + * + * @param array $theme_json_variations The theme.json variations to test. + */ + public function test_sanitize_with_invalid_style_variation( $theme_json_variations ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate structure is sanitized. + $sanitized_theme_json = $theme_json->get_raw_data(); + $this->assertIsArray( $sanitized_theme_json, 'Sanitized theme.json is not an array data type' ); + $this->assertArrayNotHasKey( 'styles', $sanitized_theme_json, 'Sanitized theme.json should not have a "styles" key' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_sanitize_with_invalid_style_variation() { + return array( + 'empty string variation' => array( + array( + 'variations' => '', + ), + ), + 'boolean variation' => array( + array( + 'variations' => false, + ), + ), + ); + } + + /** + * @ticket 57583 + * @ticket 61165 + * + * @dataProvider data_get_styles_for_block_with_style_variations + * + * @param array $theme_json_variations Theme.json variations to test. + * @param string $metadata_variations Style variations to test. + * @param string $expected Expected results for styling. + */ + public function test_get_styles_for_block_with_style_variations( $theme_json_variations, $metadata_variations, $expected ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/quote' => $theme_json_variations, + ), + ), + ) + ); + + // Validate styles are generated properly. + $metadata = array( + 'path' => array( 'styles', 'blocks', 'core/quote' ), + 'selector' => '.wp-block-quote', + 'variations' => $metadata_variations, + ); + $actual_styles = $theme_json->get_styles_for_block( $metadata ); + $this->assertSame( $expected, $actual_styles ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_styles_for_block_with_style_variations() { + $plain = array( + 'metadata' => array( + 'path' => array( 'styles', 'blocks', 'core/quote', 'variations', 'plain' ), + 'selector' => '.is-style-plain.wp-block-quote', + ), + 'styles' => ':root :where(.is-style-plain.wp-block-quote){background-color: hotpink;}', + ); + + return array( + '1 variation with 1 invalid property' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'] ), + 'expected' => $plain['styles'], + ), + '1 variation with 2 invalid properties' => array( + 'theme_json_variations' => array( + 'variations' => array( + 'plain' => array( + 'color' => array( + 'background' => 'hotpink', + ), + 'invalidProperty1' => 'value1', + 'invalidProperty2' => 'value2', + ), + ), + ), + 'metadata_variation' => array( $plain['metadata'] ), + 'expected' => $plain['styles'], + ), + ); + } + + /** + * Tests that block style variation selectors are generated correctly + * for block selectors of various structures. + * + * @ticket 62471 + */ + public function test_get_styles_for_block_with_style_variations_and_custom_selectors() { + register_block_type( + 'test/milk', + array( + 'api_version' => 3, + 'selectors' => array( + 'root' => '.milk', + 'color' => '.wp-block-test-milk .liquid, .wp-block-test-milk:not(.spoiled), .wp-block-test-milk.in-bottle', + ), + ) + ); + + register_block_style( + 'test/milk', + array( + 'name' => 'chocolate', + 'label' => 'Chocolate', + ) + ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'test/milk' => array( + 'color' => array( + 'background' => 'white', + ), + 'variations' => array( + 'chocolate' => array( + 'color' => array( + 'background' => '#35281E', + ), + ), + ), + ), + ), + ), + ) + ); + + $metadata = array( + 'name' => 'test/milk', + 'path' => array( 'styles', 'blocks', 'test/milk' ), + 'selector' => '.wp-block-test-milk', + 'selectors' => array( + 'color' => '.wp-block-test-milk .liquid, .wp-block-test-milk:not(.spoiled), .wp-block-test-milk.in-bottle', + ), + 'variations' => array( + 'chocolate' => array( + 'path' => array( 'styles', 'blocks', 'test/milk', 'variations', 'chocolate' ), + 'selector' => '.is-style-chocolate.wp-block-test-milk', + ), + ), + ); + + $actual_styles = $theme_json->get_styles_for_block( $metadata ); + $default_styles = ':root :where(.wp-block-test-milk .liquid, .wp-block-test-milk:not(.spoiled), .wp-block-test-milk.in-bottle){background-color: white;}'; + $variation_styles = ':root :where(.is-style-chocolate.wp-block-test-milk .liquid,.is-style-chocolate.wp-block-test-milk:not(.spoiled),.is-style-chocolate.wp-block-test-milk.in-bottle){background-color: #35281E;}'; + $expected = $default_styles . $variation_styles; + + unregister_block_style( 'test/milk', 'chocolate' ); + unregister_block_type( 'test/milk' ); + + $this->assertSame( $expected, $actual_styles ); + } + + /** + * Tests that block style variations with blockGap generate proper layout styles. + * + * @ticket 64533 + */ + public function test_get_styles_for_block_with_style_variations_and_block_gap() { + register_block_style( + 'core/group', + array( + 'name' => 'withGap', + 'label' => 'With Gap', + ) + ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'spacing' => array( + 'blockGap' => true, + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'withGap' => array( + 'color' => array( + 'background' => 'tomato', + ), + 'spacing' => array( + 'blockGap' => '5rem', + ), + ), + ), + ), + ), + ), + ) + ); + + $metadata = array( + 'name' => 'core/group', + 'path' => array( 'styles', 'blocks', 'core/group' ), + 'selector' => '.wp-block-group', + 'css' => '.wp-block-group', + 'variations' => array( + array( + 'path' => array( 'styles', 'blocks', 'core/group', 'variations', 'withGap' ), + 'selector' => '.is-style-withGap.wp-block-group', + ), + ), + ); + + $actual_styles = $theme_json->get_styles_for_block( $metadata ); + + unregister_block_style( 'core/group', 'withGap' ); + + $expected = ':root :where(.is-style-withGap.wp-block-group){background-color: tomato;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flow) > :first-child{margin-block-start: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flow) > :last-child{margin-block-end: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flow) > *{margin-block-start: 5rem;margin-block-end: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-constrained) > :first-child{margin-block-start: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-constrained) > :last-child{margin-block-end: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-constrained) > *{margin-block-start: 5rem;margin-block-end: 0;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-flex){gap: 5rem;}:root :where(.is-style-withGap.wp-block-group.wp-block-group-is-layout-grid){gap: 5rem;}'; + $this->assertSame( $expected, $actual_styles ); + } + + public function test_block_style_variations() { + wp_set_current_user( static::$administrator_id ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'background' => 'blue', + ), + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'purple', + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON::remove_insecure_properties( $expected ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + + public function test_block_style_variations_with_invalid_properties() { + wp_set_current_user( static::$administrator_id ); + + $partially_invalid_variation = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'background' => 'blue', + ), + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'purple', + ), + 'invalid' => array( + 'value' => 'should be stripped', + ), + ), + ), + ), + ), + ), + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'background' => 'blue', + ), + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'purple', + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON::remove_insecure_properties( $partially_invalid_variation ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + + /** + * Test ensures that inner block type styles and their element styles are + * preserved for block style variations when removing insecure properties. + * + * @ticket 62372 + */ + public function test_block_style_variations_with_inner_blocks_and_elements() { + wp_set_current_user( static::$administrator_id ); + register_block_style( + array( 'core/group' ), + array( + 'name' => 'custom-group', + 'label' => 'Custom Group', + ) + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'color' => array( + 'background' => 'blue', + ), + 'variations' => array( + 'custom-group' => array( + 'color' => array( + 'background' => 'purple', + ), + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'green', + ), + ), + ), + ), + ), + 'core/heading' => array( + 'typography' => array( + 'fontSize' => '24px', + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ':hover' => array( + 'color' => array( + 'text' => 'orange', + ), + ), + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON::remove_insecure_properties( $expected ); + + // The sanitization processes blocks in a specific order which might differ to the theme.json input. + $this->assertEqualsCanonicalizing( + $expected, + $actual, + 'Block style variations data does not match when inner blocks or element styles present' + ); + } + + /** + * Test ensures that inner block type styles and their element styles for block + * style variations have all unsafe values removed. + * + * @ticket 62372 + */ + public function test_block_style_variations_with_invalid_inner_block_or_element_styles() { + wp_set_current_user( static::$administrator_id ); + register_block_style( + array( 'core/group' ), + array( + 'name' => 'custom-group', + 'label' => 'Custom Group', + ) + ); + + $input = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'custom-group' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'typography' => array( + 'fontSize' => 'alert(1)', // Should be removed. + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + 'css' => 'unsafe-value', // Should be removed. + ), + ), + 'custom' => 'unsafe-value', // Should be removed. + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + 'javascript' => 'alert(1)', // Should be removed. + ), + ), + ), + ), + ), + ), + ), + ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'variations' => array( + 'custom-group' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'red', + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'blue', + ), + ), + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ), + ), + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON::remove_insecure_properties( $input ); + + // The sanitization processes blocks in a specific order which might differ to the theme.json input. + $this->assertEqualsCanonicalizing( + $expected, + $actual, + 'Insecure properties were not removed from block style variation inner block types or elements' + ); + } + + /** + * Tests generating the spacing presets array based on the spacing scale provided. + * + * @ticket 56467 + * + * @dataProvider data_set_spacing_sizes + */ + public function test_set_spacing_sizes( $spacing_scale, $expected_output ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => $spacing_scale, + ), + ), + ), + 'default' + ); + + $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); + } + + /** + * Data provider for spacing scale tests. + * + * @ticket 56467 + * + * @return array + */ + public function data_set_spacing_sizes() { + return array( + 'only one value when single step in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '4rem', + ), + ), + ), + 'one step above medium when two steps in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 2, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '5.5rem', + ), + ), + ), + 'one step above medium and one below when three steps in spacing scale' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 3, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '5.5rem', + ), + ), + ), + 'extra step added above medium when an even number of steps > 2 specified' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 4, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '4rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '5.5rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '7rem', + ), + ), + ), + 'extra steps above medium if bottom end will go below zero' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 2.5, + 'steps' => 5, + 'mediumStep' => 5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '2.5rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '7.5rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '10rem', + ), + array( + 'name' => '2X-Large', + 'slug' => '80', + 'size' => '12.5rem', + ), + ), + ), + 'multiplier correctly calculated above and below medium' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'X-Small', + 'slug' => '30', + 'size' => '0.67rem', + ), + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '1rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '2.25rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '3.38rem', + ), + ), + ), + 'increment < 1 combined showing * operator acting as divisor above and below medium' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 0.25, + 'steps' => 5, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => 'X-Small', + 'slug' => '30', + 'size' => '0.09rem', + ), + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '0.38rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '6rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '24rem', + ), + ), + ), + 't-shirt sizing used if more than 7 steps in scale' => array( + 'spacing_scale' => array( + 'operator' => '*', + 'increment' => 1.5, + 'steps' => 8, + 'mediumStep' => 1.5, + 'unit' => 'rem', + ), + 'expected_output' => array( + array( + 'name' => '2X-Small', + 'slug' => '20', + 'size' => '0.44rem', + ), + array( + 'name' => 'X-Small', + 'slug' => '30', + 'size' => '0.67rem', + ), + array( + 'name' => 'Small', + 'slug' => '40', + 'size' => '1rem', + ), + array( + 'name' => 'Medium', + 'slug' => '50', + 'size' => '1.5rem', + ), + array( + 'name' => 'Large', + 'slug' => '60', + 'size' => '2.25rem', + ), + array( + 'name' => 'X-Large', + 'slug' => '70', + 'size' => '3.38rem', + ), + array( + 'name' => '2X-Large', + 'slug' => '80', + 'size' => '5.06rem', + ), + array( + 'name' => '3X-Large', + 'slug' => '90', + 'size' => '7.59rem', + ), + ), + ), + ); + } + + /** + * Tests generating the spacing presets array based on the spacing scale provided. + * + * @ticket 56467 + * + * @dataProvider data_set_spacing_sizes_when_invalid + * + * @param array $spacing_scale Example spacing scale definitions from the data provider. + * @param array $expected_output Expected output from data provider. + */ + public function test_set_spacing_sizes_when_invalid( $spacing_scale, $expected_output ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'spacing' => array( + 'spacingScale' => $spacing_scale, + ), + ), + ), + 'default' + ); + + $this->assertSame( $expected_output, _wp_array_get( $theme_json->get_raw_data(), array( 'settings', 'spacing', 'spacingSizes', 'default' ) ) ); + } + + /** + * Data provider for spacing scale tests. + * + * @ticket 56467 + * + * @return array + */ + public function data_set_spacing_sizes_when_invalid() { + return array( + 'missing operator value' => array( + 'spacing_scale' => array( + 'operator' => '', + 'increment' => 1.5, + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array(), + ), + 'non numeric increment' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 'add two to previous value', + 'steps' => 1, + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array(), + ), + 'non numeric steps' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 'spiral staircase preferred', + 'mediumStep' => 4, + 'unit' => 'rem', + ), + 'expected_output' => array(), + ), + 'non numeric medium step' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 'That which is just right', + 'unit' => 'rem', + ), + 'expected_output' => array(), + ), + 'missing unit value' => array( + 'spacing_scale' => array( + 'operator' => '+', + 'increment' => 1.5, + 'steps' => 5, + 'mediumStep' => 4, + ), + 'expected_output' => array(), + ), + ); + } + + /** + * Tests the core separator block output based on various provided settings. + * + * @ticket 56903 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * + * @dataProvider data_update_separator_declarations + * + * @param array $separator_block_settings Example separator block settings from the data provider. + * @param array $expected_output Expected output from data provider. + */ + public function test_update_separator_declarations( $separator_block_settings, $expected_output ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/separator' => $separator_block_settings, + ), + ), + ), + 'default' + ); + + $separator_node = array( + 'path' => array( 'styles', 'blocks', 'core/separator' ), + 'selector' => '.wp-block-separator', + ); + + $this->assertSame( $expected_output, $theme_json->get_styles_for_block( $separator_node ) ); + } + + /** + * Data provider for separator declaration tests. + * + * @return array + */ + public function data_update_separator_declarations() { + return array( + // If only background is defined, test that includes border-color to the style so it is applied on the front end. + 'only background' => array( + array( + 'color' => array( + 'background' => 'blue', + ), + ), + 'expected_output' => ':root :where(.wp-block-separator){background-color: blue;color: blue;}', + ), + // If background and text are defined, do not include border-color, as text color is enough. + 'background and text, no border-color' => array( + array( + 'color' => array( + 'background' => 'blue', + 'text' => 'red', + ), + ), + 'expected_output' => ':root :where(.wp-block-separator){background-color: blue;color: red;}', + ), + // If only text is defined, do not include border-color, as by itself is enough. + 'only text' => array( + array( + 'color' => array( + 'text' => 'red', + ), + ), + 'expected_output' => ':root :where(.wp-block-separator){color: red;}', + ), + // If background, text, and border-color are defined, include everything, CSS specificity will decide which to apply. + 'background, text, and border-color' => array( + array( + 'color' => array( + 'background' => 'blue', + 'text' => 'red', + ), + 'border' => array( + 'color' => 'pink', + ), + ), + 'expected_output' => ':root :where(.wp-block-separator){background-color: blue;border-color: pink;color: red;}', + ), + // If background and border color are defined, include everything, CSS specificity will decide which to apply. + 'background, and border-color' => array( + array( + 'color' => array( + 'background' => 'blue', + ), + 'border' => array( + 'color' => 'pink', + ), + ), + 'expected_output' => ':root :where(.wp-block-separator){background-color: blue;border-color: pink;}', + ), + ); + } + + /** + * @ticket 57559 + */ + public function test_shadow_preset_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'shadow' => array( + 'presets' => array( + array( + 'slug' => 'natural', + 'shadow' => '5px 5px 5px 0 black', + ), + array( + 'slug' => 'sharp', + 'shadow' => '5px 5px black', + ), + ), + ), + ), + ) + ); + + $expected_styles = ':root{--wp--preset--shadow--natural: 5px 5px 5px 0 black;--wp--preset--shadow--sharp: 5px 5px black;}'; + $this->assertSame( $expected_styles, $theme_json->get_stylesheet(), 'Styles returned from "::get_stylesheet()" does not match expectations' ); + $this->assertSame( $expected_styles, $theme_json->get_stylesheet( array( 'variables' ) ), 'Styles returned from "::get_stylesheet()" when requiring "variables" type does not match expectations' ); + } + + /** + * @ticket 57559 + * @ticket 58550 + * @ticket 60936 + * @ticket 61165 + * @ticket 61630 + */ + public function test_get_shadow_styles_for_blocks() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'shadow' => array( + 'presets' => array( + array( + 'slug' => 'natural', + 'shadow' => '5px 5px 0 0 black', + ), + ), + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'shadow' => 'var(--wp--preset--shadow--natural)', + ), + ), + 'elements' => array( + 'button' => array( + 'shadow' => 'var:preset|shadow|natural', + ), + 'link' => array( + 'shadow' => array( 'ref' => 'styles.elements.button.shadow' ), + ), + ), + ), + ) + ); + + $variable_styles = ':root{--wp--preset--shadow--natural: 5px 5px 0 0 black;}'; + $element_styles = 'a:where(:not(.wp-element-button)){box-shadow: var(--wp--preset--shadow--natural);}:root :where(.wp-element-button, .wp-block-button__link){box-shadow: var(--wp--preset--shadow--natural);}:root :where(p){box-shadow: var(--wp--preset--shadow--natural);}'; + $expected_styles = $variable_styles . $element_styles; + $this->assertSame( $expected_styles, $theme_json->get_stylesheet( array( 'styles', 'presets', 'variables' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Tests that theme background image styles are correctly generated, + * and that default background size of "cover" isn't + * applied (it's only applied to blocks). + * + * @ticket 61123 + * @ticket 61165 + * @ticket 61720 + * @ticket 61704 + * @ticket 61858 + */ + public function test_get_top_level_background_image_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/image.png', + ), + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', + ), + ), + ) + ); + + $body_node = array( + 'path' => array( 'styles' ), + 'selector' => 'body', + ); + + $expected_styles = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-attachment: fixed;}"; + $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_stylesheet()" with top-level background styles type do not match expectations' ); + + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => "url('http://example.org/image.png')", + 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', + ), + ), + ) + ); + + $expected_styles = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/image.png');background-position: center center;background-repeat: no-repeat;background-size: contain;background-attachment: fixed;}"; + $this->assertSame( $expected_styles, $theme_json->get_styles_for_block( $body_node ), 'Styles returned from "::get_stylesheet()" with top-level background image as string type do not match expectations' ); + } + + /** + * Block-level global background image styles. + * + * @ticket 61588 + * @ticket 61720 + * @ticket 61858 + */ + public function test_get_block_background_image_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => "url('http://example.org/group.png')", + 'backgroundRepeat' => 'no-repeat', + 'backgroundPosition' => 'center center', + 'backgroundAttachment' => 'fixed', + ), + ), + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/quote.png', + 'id' => 321, + ), + 'backgroundSize' => 'contain', + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/verse.png', + 'id' => 123, + ), + ), + ), + ), + ), + ) + ); + + $group_node = array( + 'name' => 'core/group', + 'path' => array( 'styles', 'blocks', 'core/group' ), + 'selector' => '.wp-block-group', + 'selectors' => array( + 'root' => '.wp-block-group', + ), + ); + + $group_styles = ":root :where(.wp-block-group){background-image: url('http://example.org/group.png');background-position: center center;background-repeat: no-repeat;background-attachment: fixed;}"; + $this->assertSame( $group_styles, $theme_json->get_styles_for_block( $group_node ), 'Styles returned from "::get_styles_for_block()" with core/group background styles as string type do not match expectations.' ); + + $quote_node = array( + 'name' => 'core/quote', + 'path' => array( 'styles', 'blocks', 'core/quote' ), + 'selector' => '.wp-block-quote', + 'selectors' => array( + 'root' => '.wp-block-quote', + ), + ); + + $quote_styles = ":root :where(.wp-block-quote){background-image: url('http://example.org/quote.png');background-position: 50% 50%;background-size: contain;}"; + $this->assertSame( $quote_styles, $theme_json->get_styles_for_block( $quote_node ), 'Styles returned from "::get_styles_for_block()" with core/quote default background styles do not match expectations.' ); + + $verse_node = array( + 'name' => 'core/verse', + 'path' => array( 'styles', 'blocks', 'core/verse' ), + 'selector' => '.wp-block-verse', + 'selectors' => array( + 'root' => '.wp-block-verse', + ), + ); + + $verse_styles = ":root :where(.wp-block-verse){background-image: url('http://example.org/verse.png');background-size: cover;}"; + $this->assertSame( $verse_styles, $theme_json->get_styles_for_block( $verse_node ), 'Styles returned from "::get_styles_for_block()" with default core/verse background styles as string type do not match expectations.' ); + } + + /** + * Testing background dynamic properties in theme.json. + * + * @ticket 61858 + */ + public function test_get_resolved_background_image_styles() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'http://example.org/top.png', + ), + 'backgroundSize' => 'contain', + 'backgroundRepeat' => 'repeat', + 'backgroundPosition' => '10% 20%', + 'backgroundAttachment' => 'scroll', + ), + 'blocks' => array( + 'core/group' => array( + 'background' => array( + 'backgroundImage' => array( + 'id' => 123, + 'url' => 'http://example.org/group.png', + ), + ), + ), + 'core/post-content' => array( + 'background' => array( + 'backgroundImage' => array( + 'ref' => 'styles.background.backgroundImage', + ), + 'backgroundSize' => array( + 'ref' => 'styles.background.backgroundSize', + ), + 'backgroundRepeat' => array( + 'ref' => 'styles.background.backgroundRepeat', + ), + 'backgroundPosition' => array( + 'ref' => 'styles.background.backgroundPosition', + ), + 'backgroundAttachment' => array( + 'ref' => 'styles.background.backgroundAttachment', + ), + ), + ), + ), + ), + ) + ); + + $expected = "html{min-height: calc(100% - var(--wp-admin--admin-bar--height, 0px));}body{background-image: url('http://example.org/top.png');background-position: 10% 20%;background-repeat: repeat;background-size: contain;background-attachment: scroll;}:root :where(.wp-block-group){background-image: url('http://example.org/group.png');background-size: cover;}:root :where(.wp-block-post-content){background-image: url('http://example.org/top.png');background-position: 10% 20%;background-repeat: repeat;background-size: contain;background-attachment: scroll;}"; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + } + + /** + * Tests that base custom CSS is generated correctly. + * + * @ticket 61395 + */ + public function test_get_stylesheet_handles_base_custom_css() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'css' => 'body {color:purple;}', + ), + ) + ); + + $custom_css = 'body {color:purple;}'; + $this->assertSame( $custom_css, $theme_json->get_stylesheet( array( 'custom-css' ) ) ); + } + + /** + * Tests that block custom CSS is generated correctly. + * + * @ticket 61395 + */ + public function test_get_styles_for_block_handles_block_custom_css() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'css' => 'color:red;', + ), + ), + ), + ) + ); + + $paragraph_node = array( + 'name' => 'core/paragraph', + 'path' => array( 'styles', 'blocks', 'core/paragraph' ), + 'selector' => 'p', + 'selectors' => array( + 'root' => 'p', + ), + ); + + $custom_css = ':root :where(p){color:red;}'; + $this->assertSame( $custom_css, $theme_json->get_styles_for_block( $paragraph_node ) ); + } + + /** + * Tests that custom CSS is kept for users with correct capabilities and removed for others. + * + * @ticket 57536 + * + * @dataProvider data_custom_css_for_user_caps + * + * @param string $user_property The property name for current user. + * @param array $expected Expected results. + */ + public function test_custom_css_for_user_caps( $user_property, array $expected ) { + wp_set_current_user( static::${$user_property} ); + + $actual = WP_Theme_JSON::remove_insecure_properties( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'css' => 'body { color:purple; }', + 'blocks' => array( + 'core/separator' => array( + 'color' => array( + 'background' => 'blue', + ), + ), + ), + ), + ) + ); + + $this->assertSameSetsWithIndex( $expected, $actual ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_custom_css_for_user_caps() { + return array( + 'allows custom css for users with caps' => array( + 'user_property' => 'administrator_id', + 'expected' => array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'css' => 'body { color:purple; }', + 'blocks' => array( + 'core/separator' => array( + 'color' => array( + 'background' => 'blue', + ), + ), + ), + ), + ), + ), + 'removes custom css for users without caps' => array( + 'user_property' => 'user_id', + 'expected' => array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/separator' => array( + 'color' => array( + 'background' => 'blue', + ), + ), + ), + ), + ), + ), + ); + } + + /** + * @ticket 61165 + * @ticket 61769 + * + * @dataProvider data_process_blocks_custom_css + * + * @param array $input An array containing the selector and css to test. + * @param string $expected Expected results. + */ + public function test_process_blocks_custom_css( $input, $expected ) { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array(), + ) + ); + $reflection = new ReflectionMethod( $theme_json, 'process_blocks_custom_css' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + $this->assertSame( $expected, $reflection->invoke( $theme_json, $input['css'], $input['selector'] ) ); + } + + /** + * Data provider. + * + * @return array[] + */ + public function data_process_blocks_custom_css() { + return array( + // Simple CSS without any nested selectors. + 'empty css' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => '', + ), + 'expected' => '', + ), + 'no nested selectors' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto;', + ), + 'expected' => ':root :where(.foo){color: red; margin: auto;}', + ), + // CSS with nested selectors. + 'with nested selector' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;}', + ), + 'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo.one){color: blue;}:root :where(.foo .two){color: green;}', + ), + 'no root styles' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => '&::before{color: red;}', + ), + 'expected' => ':root :where(.foo)::before{color: red;}', + ), + // CSS with pseudo elements. + 'with pseudo elements' => array( + 'input' => array( + 'selector' => '.foo', + 'css' => 'color: red; margin: auto; &::before{color: blue;} & ::before{color: green;} &.one::before{color: yellow;} & .two::before{color: purple;}', + ), + 'expected' => ':root :where(.foo){color: red; margin: auto;}:root :where(.foo)::before{color: blue;}:root :where(.foo) ::before{color: green;}:root :where(.foo.one)::before{color: yellow;}:root :where(.foo .two)::before{color: purple;}', + ), + // CSS with multiple root selectors. + 'with multiple root selectors' => array( + 'input' => array( + 'selector' => '.foo, .bar', + 'css' => 'color: red; margin: auto; &.one{color: blue;} & .two{color: green;} &::before{color: yellow;} & ::before{color: purple;} &.three::before{color: orange;} & .four::before{color: skyblue;}', + ), + 'expected' => ':root :where(.foo, .bar){color: red; margin: auto;}:root :where(.foo.one, .bar.one){color: blue;}:root :where(.foo .two, .bar .two){color: green;}:root :where(.foo, .bar)::before{color: yellow;}:root :where(.foo, .bar) ::before{color: purple;}:root :where(.foo.three, .bar.three)::before{color: orange;}:root :where(.foo .four, .bar .four)::before{color: skyblue;}', + ), + ); + } + + public function test_internal_syntax_is_converted_to_css_variables() { + $result = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'color' => array( + 'background' => 'var:preset|color|primary', + 'text' => 'var(--wp--preset--color--secondary)', + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'background' => 'var:preset|color|pri', + 'text' => 'var(--wp--preset--color--sec)', + ), + ), + ), + 'blocks' => array( + 'core/post-terms' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--small)' ), + 'color' => array( 'background' => 'var:preset|color|secondary' ), + ), + 'core/navigation' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'background' => 'var:preset|color|p', + 'text' => 'var(--wp--preset--color--s)', + ), + ), + ), + ), + 'core/quote' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--d)' ), + 'color' => array( 'background' => 'var:preset|color|d' ), + 'variations' => array( + 'plain' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--s)' ), + 'color' => array( 'background' => 'var:preset|color|s' ), + ), + ), + ), + ), + ), + ) + ); + $styles = $result->get_raw_data()['styles']; + + $this->assertSame( 'var(--wp--preset--color--primary)', $styles['color']['background'], 'Top level: Assert the originally correct values are still correct.' ); + $this->assertSame( 'var(--wp--preset--color--secondary)', $styles['color']['text'], 'Top level: Assert the originally correct values are still correct.' ); + + $this->assertSame( 'var(--wp--preset--color--pri)', $styles['elements']['link']['color']['background'], 'Element top level: Assert the originally correct values are still correct.' ); + $this->assertSame( 'var(--wp--preset--color--sec)', $styles['elements']['link']['color']['text'], 'Element top level: Assert the originally correct values are still correct.' ); + + $this->assertSame( 'var(--wp--preset--font-size--small)', $styles['blocks']['core/post-terms']['typography']['fontSize'], 'Top block level: Assert the originally correct values are still correct.' ); + $this->assertSame( 'var(--wp--preset--color--secondary)', $styles['blocks']['core/post-terms']['color']['background'], 'Top block level: Assert the internal variables are convert to CSS custom variables.' ); + + $this->assertSame( 'var(--wp--preset--color--p)', $styles['blocks']['core/navigation']['elements']['link']['color']['background'], 'Elements block level: Assert the originally correct values are still correct.' ); + $this->assertSame( 'var(--wp--preset--color--s)', $styles['blocks']['core/navigation']['elements']['link']['color']['text'], 'Elements block level: Assert the originally correct values are still correct.' ); + + $this->assertSame( 'var(--wp--preset--font-size--s)', $styles['blocks']['core/quote']['variations']['plain']['typography']['fontSize'], 'Style variations: Assert the originally correct values are still correct.' ); + $this->assertSame( 'var(--wp--preset--color--s)', $styles['blocks']['core/quote']['variations']['plain']['color']['background'], 'Style variations: Assert the internal variables are convert to CSS custom variables.' ); + } + + /** + * Tests that the theme.json file is correctly parsed and the variables are resolved. + * + * @ticket 58588 + * @ticket 60613 + * + * @covers WP_Theme_JSON::resolve_variables + * @covers WP_Theme_JSON::convert_variables_to_value + */ + public function test_resolve_variables() { + $primary_color = '#9DFF20'; + $secondary_color = '#9DFF21'; + $contrast_color = '#000'; + $raw_color_value = '#efefef'; + $large_font = '18px'; + $small_font = '12px'; + $spacing = 'clamp(1.5rem, 5vw, 2rem)'; + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'color' => $primary_color, + 'name' => 'Primary', + 'slug' => 'primary', + ), + array( + 'color' => $secondary_color, + 'name' => 'Secondary', + 'slug' => 'secondary', + ), + array( + 'color' => $contrast_color, + 'name' => 'Contrast', + 'slug' => 'contrast', + ), + ), + ), + ), + 'typography' => array( + 'fontSizes' => array( + array( + 'size' => $small_font, + 'name' => 'Font size small', + 'slug' => 'small', + ), + array( + 'size' => $large_font, + 'name' => 'Font size large', + 'slug' => 'large', + ), + ), + ), + 'spacing' => array( + 'spacingSizes' => array( + array( + 'size' => $spacing, + 'name' => '100', + 'slug' => '100', + ), + ), + ), + ), + 'styles' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--primary)', + 'text' => $raw_color_value, + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'text' => 'var(--wp--preset--color--contrast)', + ), + 'typography' => array( + 'fontSize' => 'var(--wp--preset--font-size--small)', + ), + ), + ), + 'blocks' => array( + 'core/post-terms' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--small)' ), + 'color' => array( 'background' => $raw_color_value ), + ), + 'core/more' => array( + 'typography' => array( 'fontSize' => 'var(--undefined--font-size--small)' ), + 'color' => array( 'background' => 'linear-gradient(90deg, var(--wp--preset--color--primary) 0%, var(--wp--preset--color--secondary) 35%, var(--wp--undefined--color--secondary) 100%)' ), + ), + 'core/comment-content' => array( + 'typography' => array( 'fontSize' => 'calc(var(--wp--preset--font-size--small, 12px) + 20px)' ), + 'color' => array( + 'text' => 'var(--wp--preset--color--primary, red)', + 'background' => 'var(--wp--preset--color--primary, var(--wp--preset--font-size--secondary))', + 'link' => 'var(--undefined--color--primary, var(--wp--preset--font-size--secondary))', + ), + ), + 'core/comments' => array( + 'color' => array( + 'text' => 'var(--undefined--color--primary, var(--wp--preset--font-size--small))', + 'background' => 'var(--wp--preset--color--primary, var(--undefined--color--primary))', + ), + ), + 'core/navigation' => array( + 'elements' => array( + 'link' => array( + 'color' => array( + 'background' => 'var(--wp--preset--color--primary)', + 'text' => 'var(--wp--preset--color--secondary)', + ), + 'typography' => array( + 'fontSize' => 'var(--wp--preset--font-size--large)', + ), + ), + ), + ), + 'core/quote' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--large)' ), + 'color' => array( 'background' => 'var(--wp--preset--color--primary)' ), + 'variations' => array( + 'plain' => array( + 'typography' => array( 'fontSize' => 'var(--wp--preset--font-size--small)' ), + 'color' => array( 'background' => 'var(--wp--preset--color--secondary)' ), + ), + ), + ), + 'core/post-template' => array( + 'spacing' => array( + 'blockGap' => null, + ), + ), + 'core/columns' => array( + 'spacing' => array( + 'blockGap' => 'var(--wp--preset--spacing--100)', + ), + ), + ), + ), + ) + ); + + $styles = $theme_json::resolve_variables( $theme_json )->get_raw_data()['styles']; + + $this->assertSame( $primary_color, $styles['color']['background'], 'Top level: Assert values are converted' ); + $this->assertSame( $raw_color_value, $styles['color']['text'], 'Top level: Assert raw values stay intact' ); + + $this->assertSame( $contrast_color, $styles['elements']['button']['color']['text'], 'Elements: color' ); + $this->assertSame( $small_font, $styles['elements']['button']['typography']['fontSize'], 'Elements: font-size' ); + + $this->assertSame( $large_font, $styles['blocks']['core/quote']['typography']['fontSize'], 'Blocks: font-size' ); + $this->assertSame( $primary_color, $styles['blocks']['core/quote']['color']['background'], 'Blocks: color' ); + $this->assertSame( $raw_color_value, $styles['blocks']['core/post-terms']['color']['background'], 'Blocks: Raw color value stays intact' ); + $this->assertSame( $small_font, $styles['blocks']['core/post-terms']['typography']['fontSize'], 'Block core/post-terms: font-size' ); + $this->assertSame( + "linear-gradient(90deg, $primary_color 0%, $secondary_color 35%, var(--wp--undefined--color--secondary) 100%)", + $styles['blocks']['core/more']['color']['background'], + 'Blocks: multiple colors and undefined color' + ); + $this->assertSame( 'var(--undefined--font-size--small)', $styles['blocks']['core/more']['typography']['fontSize'], 'Blocks: undefined font-size ' ); + $this->assertSame( "calc($small_font + 20px)", $styles['blocks']['core/comment-content']['typography']['fontSize'], 'Blocks: font-size in random place' ); + $this->assertSame( $primary_color, $styles['blocks']['core/comment-content']['color']['text'], 'Blocks: text color with fallback' ); + $this->assertSame( $primary_color, $styles['blocks']['core/comment-content']['color']['background'], 'Blocks: background color with var as fallback' ); + $this->assertSame( $primary_color, $styles['blocks']['core/navigation']['elements']['link']['color']['background'], 'Block element: background color' ); + $this->assertSame( $secondary_color, $styles['blocks']['core/navigation']['elements']['link']['color']['text'], 'Block element: text color' ); + $this->assertSame( $large_font, $styles['blocks']['core/navigation']['elements']['link']['typography']['fontSize'], 'Block element: font-size' ); + + $this->assertSame( + "var(--undefined--color--primary, $small_font)", + $styles['blocks']['core/comments']['color']['text'], + 'Blocks: text color with undefined var and fallback' + ); + $this->assertSame( + $primary_color, + $styles['blocks']['core/comments']['color']['background'], + 'Blocks: background color with variable and undefined fallback' + ); + + $this->assertSame( $small_font, $styles['blocks']['core/quote']['variations']['plain']['typography']['fontSize'], 'Block variations: font-size' ); + $this->assertSame( $secondary_color, $styles['blocks']['core/quote']['variations']['plain']['color']['background'], 'Block variations: color' ); + /* + * As with wp_get_global_styles(), WP_Theme_JSON::resolve_variables may be called with merged data from + * WP_Theme_JSON_Resolver. WP_Theme_JSON_Resolver::get_block_data() sets blockGap for supported blocks to `null` if the value is not defined. + */ + $this->assertNull( + $styles['blocks']['core/post-template']['spacing']['blockGap'], + 'Blocks: Post Template spacing.blockGap should be null' + ); + $this->assertSame( + $spacing, + $styles['blocks']['core/columns']['spacing']['blockGap'], + 'Blocks: Columns spacing.blockGap should match' + ); + } + + /** + * Tests the correct application of a block style variation's selector to + * a block's selector. + * + * @ticket 60453 + * + * @dataProvider data_get_block_style_variation_selector + * + * @param string $selector CSS selector. + * @param string $expected Expected block style variation CSS selector. + */ + public function test_get_block_style_variation_selector( $selector, $expected ) { + $theme_json = new ReflectionClass( 'WP_Theme_JSON' ); + + $func = $theme_json->getMethod( 'get_block_style_variation_selector' ); + if ( PHP_VERSION_ID < 80100 ) { + $func->setAccessible( true ); + } + + $actual = $func->invoke( null, 'custom', $selector ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Data provider for generating block style variation selectors. + * + * @return array[] + */ + public function data_get_block_style_variation_selector() { + return array( + 'empty block selector' => array( + 'selector' => '', + 'expected' => '.is-style-custom', + ), + 'class selector' => array( + 'selector' => '.wp-block', + 'expected' => '.wp-block.is-style-custom', + ), + 'id selector' => array( + 'selector' => '#wp-block', + 'expected' => '#wp-block.is-style-custom', + ), + 'element tag selector' => array( + 'selector' => 'p', + 'expected' => 'p.is-style-custom', + ), + 'attribute selector' => array( + 'selector' => '[style*="color"]', + 'expected' => '[style*="color"].is-style-custom', + ), + 'descendant selector' => array( + 'selector' => '.wp-block .inner', + 'expected' => '.wp-block.is-style-custom .inner', + ), + 'comma separated selector' => array( + 'selector' => '.wp-block .inner, .wp-block .alternative', + 'expected' => '.wp-block.is-style-custom .inner, .wp-block.is-style-custom .alternative', + ), + 'pseudo selector' => array( + 'selector' => 'div:first-child', + 'expected' => 'div.is-style-custom:first-child', + ), + ':is selector' => array( + 'selector' => '.wp-block:is(.outer .inner:first-child)', + 'expected' => '.wp-block.is-style-custom:is(.outer .inner:first-child)', + ), + ':not selector' => array( + 'selector' => '.wp-block:not(.outer .inner:first-child)', + 'expected' => '.wp-block.is-style-custom:not(.outer .inner:first-child)', + ), + ':has selector' => array( + 'selector' => '.wp-block:has(.outer .inner:first-child)', + 'expected' => '.wp-block.is-style-custom:has(.outer .inner:first-child)', + ), + ':where selector' => array( + 'selector' => '.wp-block:where(.outer .inner:first-child)', + 'expected' => '.wp-block.is-style-custom:where(.outer .inner:first-child)', + ), + 'wrapping :where selector' => array( + 'selector' => ':where(.outer .inner:first-child)', + 'expected' => ':where(.outer.is-style-custom .inner:first-child)', + ), + 'complex' => array( + 'selector' => '.wp:where(.something):is(.test:not(.nothing p)):has(div[style]) .content, .wp:where(.nothing):not(.test:is(.something div)):has(span[style]) .inner', + 'expected' => '.wp.is-style-custom:where(.something):is(.test:not(.nothing p)):has(div[style]) .content, .wp.is-style-custom:where(.nothing):not(.test:is(.something div)):has(span[style]) .inner', + ), + ); + } + + /** + * Tests the correct scoping of selectors for a style node. + * + * @ticket 61119 + */ + public function test_scope_style_node_selectors() { + $theme_json = new ReflectionClass( 'WP_Theme_JSON' ); + + $func = $theme_json->getMethod( 'scope_style_node_selectors' ); + if ( PHP_VERSION_ID < 80100 ) { + $func->setAccessible( true ); + } + + $node = array( + 'name' => 'core/image', + 'path' => array( 'styles', 'blocks', 'core/image' ), + 'selector' => '.wp-block-image', + 'selectors' => array( + 'root' => '.wp-block-image', + 'border' => '.wp-block-image img, .wp-block-image .wp-block-image__crop-area, .wp-block-image .components-placeholder', + 'typography' => array( + 'textDecoration' => '.wp-block-image caption', + ), + 'filter' => array( + 'duotone' => '.wp-block-image img, .wp-block-image .components-placeholder', + ), + ), + ); + + $actual = $func->invoke( null, '.custom-scope', $node ); + $expected = array( + 'name' => 'core/image', + 'path' => array( 'styles', 'blocks', 'core/image' ), + 'selector' => '.custom-scope .wp-block-image', + 'selectors' => array( + 'root' => '.custom-scope .wp-block-image', + 'border' => '.custom-scope .wp-block-image img, .custom-scope .wp-block-image .wp-block-image__crop-area, .custom-scope .wp-block-image .components-placeholder', + 'typography' => array( + 'textDecoration' => '.custom-scope .wp-block-image caption', + ), + 'filter' => array( + 'duotone' => '.custom-scope .wp-block-image img, .custom-scope .wp-block-image .components-placeholder', + ), + ), + ); + + $this->assertSame( $expected, $actual ); + } + + /** + * Block style variations styles aren't generated by default. This test covers + * the `get_block_nodes` does not include variations by default, preventing + * the inclusion of their styles. + * + * @ticket 61443 + */ + public function test_opt_out_of_block_style_variations_by_default() { + $theme_json = new ReflectionClass( 'WP_Theme_JSON' ); + + $func = $theme_json->getMethod( 'get_block_nodes' ); + if ( PHP_VERSION_ID < 80100 ) { + $func->setAccessible( true ); + } + + $theme_json = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'red', + ), + ), + ), + ), + ), + ), + ); + $selectors = array(); + + $block_nodes = $func->invoke( null, $theme_json, $selectors ); + $button_variations = $block_nodes[0]['variations'] ?? array(); - $this->assertEqualSetsWithIndex( $expected, $actual ); + $this->assertSame( array(), $button_variations ); } /** - * @ticket 53175 + * Block style variations styles aren't generated by default. This test ensures + * variations are included by `get_block_nodes` when requested. + * + * @ticket 61443 */ - public function test_merge_incoming_data_empty_presets() { - $theme_json = new WP_Theme_JSON( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - ), - ), - 'spacing' => array( - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'fontSizes' => array( - array( - 'slug' => 'size', - 'value' => 'size', + public function test_opt_in_to_block_style_variations() { + $theme_json = new ReflectionClass( 'WP_Theme_JSON' ); + + $func = $theme_json->getMethod( 'get_block_nodes' ); + if ( PHP_VERSION_ID < 80100 ) { + $func->setAccessible( true ); + } + + $theme_json = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'red', + ), ), ), ), ), - ) + ), ); + $selectors = array(); + $options = array( 'include_block_style_variations' => true ); - $theme_json->merge( - new WP_Theme_JSON( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array(), - 'gradients' => array(), - 'palette' => array(), - ), - 'spacing' => array( - 'units' => array(), - ), - 'typography' => array( - 'fontSizes' => array(), - ), - ), - ) - ) - ); + $block_nodes = $func->invoke( null, $theme_json, $selectors, $options ); + $button_variations = $block_nodes[0]['variations'] ?? array(); - $actual = $theme_json->get_raw_data(); $expected = array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'duotone' => array(), - 'gradients' => array( - 'theme' => array(), - ), - 'palette' => array( - 'theme' => array(), - ), - ), - 'spacing' => array( - 'units' => array(), - ), - 'typography' => array( - 'fontSizes' => array( - 'theme' => array(), - ), - ), + array( + 'path' => array( 'styles', 'blocks', 'core/button', 'variations', 'outline' ), + 'selector' => '.wp-block-button.is-style-outline .wp-block-button__link', ), ); - $this->assertEqualSetsWithIndex( $expected, $actual ); + $this->assertSame( $expected, $button_variations ); } /** - * @ticket 53175 + * Tests that block-level settings inherit global default settings when not explicitly set. + * + * When a block doesn't have its own default presets setting, it should inherit + * the global setting from the theme. This affects whether default presets + * are filtered out during merging. + * + * @ticket 64195 */ - public function test_merge_incoming_data_null_presets() { - $theme_json = new WP_Theme_JSON( + public function test_merge_incoming_data_block_level_inherits_global_default_setting() { + $defaults = new WP_Theme_JSON( array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( - 'color' => array( - 'duotone' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), - 'palette' => array( - array( - 'slug' => 'red', - 'color' => 'red', - ), - ), - ), - 'spacing' => array( - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'fontSizes' => array( + 'color' => array( + 'defaultDuotone' => true, + 'duotone' => array( array( - 'slug' => 'size', - 'value' => 'size', + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Default Dark grayscale', ), ), ), ), - ) - ); - - $theme_json->merge( - new WP_Theme_JSON( - array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - ), - 'spacing' => array( - 'customMargin' => false, - ), - 'typography' => array( - 'customLineHeight' => false, - ), - ), - ) - ) + ), + 'default' ); - - $actual = $theme_json->get_raw_data(); - $expected = array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array( - 'color' => array( - 'custom' => false, - 'duotone' => array( - array( - 'slug' => 'value', - 'colors' => array( 'red', 'green' ), - ), - ), - 'gradients' => array( - 'theme' => array( - array( - 'slug' => 'gradient', - 'gradient' => 'gradient', - ), - ), + $theme = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( + 'defaultDuotone' => false, ), - 'palette' => array( - 'theme' => array( - array( - 'slug' => 'red', - 'color' => 'red', + 'blocks' => array( + 'core/image' => array( + 'color' => array( + // No defaultDuotone setting - should inherit global (false) set by theme. + 'duotone' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Theme Dark grayscale', + ), + ), ), ), - ), - ), - 'spacing' => array( - 'customMargin' => false, - 'units' => array( 'px', 'em' ), - ), - 'typography' => array( - 'customLineHeight' => false, - 'fontSizes' => array( - 'theme' => array( - array( - 'slug' => 'size', - 'value' => 'size', + 'core/cover' => array( + 'color' => array( + 'defaultDuotone' => true, // Explicitly enabled at block level + 'duotone' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Cover Dark grayscale', + ), + ), ), ), ), ), - ), - ); - - $this->assertEqualSetsWithIndex( $expected, $actual ); - } - - /** - * @ticket 52991 - */ - public function test_get_from_editor_settings() { - $input = array( - 'disableCustomColors' => true, - 'disableCustomGradients' => true, - 'disableCustomFontSizes' => true, - 'enableCustomLineHeight' => true, - 'enableCustomUnits' => true, - 'colors' => array( - array( - 'slug' => 'color-slug', - 'name' => 'Color Name', - 'color' => 'colorvalue', - ), - ), - 'gradients' => array( - array( - 'slug' => 'gradient-slug', - 'name' => 'Gradient Name', - 'gradient' => 'gradientvalue', - ), - ), - 'fontSizes' => array( - array( - 'slug' => 'size-slug', - 'name' => 'Size Name', - 'size' => 'sizevalue', - ), - ), + ) ); $expected = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( - 'color' => array( - 'custom' => false, - 'customGradient' => false, - 'gradients' => array( - array( - 'slug' => 'gradient-slug', - 'name' => 'Gradient Name', - 'gradient' => 'gradientvalue', - ), - ), - 'palette' => array( - array( - 'slug' => 'color-slug', - 'name' => 'Color Name', - 'color' => 'colorvalue', + 'color' => array( + 'defaultDuotone' => false, + 'duotone' => array( + 'default' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Default Dark grayscale', + ), ), ), ), - 'spacing' => array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), - ), - 'typography' => array( - 'customFontSize' => false, - 'customLineHeight' => true, - 'fontSizes' => array( - array( - 'slug' => 'size-slug', - 'name' => 'Size Name', - 'size' => 'sizevalue', + 'blocks' => array( + 'core/image' => array( + 'color' => array( + 'duotone' => array( + 'theme' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Theme Dark grayscale', + ), + ), + ), + ), + ), + 'core/cover' => array( + 'color' => array( + 'defaultDuotone' => true, + 'duotone' => array( + 'theme' => array( + // Should be filtered out because block-level defaults are enabled + // and slug matches default + ), + ), ), ), ), ), ); - $actual = WP_Theme_JSON::get_from_editor_settings( $input ); + $defaults->merge( $theme ); + $actual = $defaults->get_raw_data(); $this->assertEqualSetsWithIndex( $expected, $actual ); } /** - * @ticket 52991 + * Tests that presets with unique slugs are preserved during merging. + * + * When merging theme presets, any preset with a slug that doesn't match + * a default preset should always be preserved, regardless of default + * preset settings. Only presets with matching slugs should be filtered out + * when defaults are enabled. + * + * @ticket 64195 */ - public function test_get_editor_settings_no_theme_support() { - $input = array( - '__unstableEnableFullSiteEditingBlocks' => false, - 'disableCustomColors' => false, - 'disableCustomFontSizes' => false, - 'disableCustomGradients' => false, - 'enableCustomLineHeight' => false, - 'enableCustomUnits' => false, - 'imageSizes' => array( - array( - 'slug' => 'thumbnail', - 'name' => 'Thumbnail', - ), - array( - 'slug' => 'medium', - 'name' => 'Medium', - ), - array( - 'slug' => 'large', - 'name' => 'Large', - ), - array( - 'slug' => 'full', - 'name' => 'Full Size', + public function test_merge_incoming_data_unique_slugs_always_preserved() { + $defaults = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( + 'defaultDuotone' => true, // Defaults enabled + 'duotone' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Default Dark grayscale', + ), + ), + ), ), ), - 'isRTL' => false, - 'maxUploadFileSize' => 123, + 'default' + ); + $theme = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'color' => array( + 'defaultDuotone' => true, // Block-level defaults enabled + 'duotone' => array( + array( + 'slug' => 'custom-unique', + 'colors' => array( '#ff0000', '#00ff00' ), + 'name' => 'Custom Unique', + ), + array( + 'slug' => 'dark-grayscale', // Matches default slug + 'colors' => array( '#111111', '#888888' ), + 'name' => 'Theme Dark grayscale', + ), + ), + ), + ), + ), + ), + ) ); $expected = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA, 'settings' => array( - 'color' => array( - 'custom' => true, - 'customGradient' => true, - ), - 'spacing' => array( - 'units' => false, + 'color' => array( + 'defaultDuotone' => true, + 'duotone' => array( + 'default' => array( + array( + 'slug' => 'dark-grayscale', + 'colors' => array( '#000000', '#7f7f7f' ), + 'name' => 'Default Dark grayscale', + ), + ), + ), ), - 'typography' => array( - 'customFontSize' => true, - 'customLineHeight' => false, + 'blocks' => array( + 'core/image' => array( + 'color' => array( + 'defaultDuotone' => true, + 'duotone' => array( + 'theme' => array( + array( + 'slug' => 'custom-unique', // Should always be preserved + 'colors' => array( '#ff0000', '#00ff00' ), + 'name' => 'Custom Unique', + ), + // 'dark-grayscale' should be filtered out due to slug match + ), + ), + ), + ), ), ), ); - $actual = WP_Theme_JSON::get_from_editor_settings( $input ); + $defaults->merge( $theme ); + $actual = $defaults->get_raw_data(); $this->assertEqualSetsWithIndex( $expected, $actual ); } /** - * @ticket 52991 + * Test that block pseudo selectors are processed correctly. */ - public function test_get_editor_settings_blank() { - $expected = array( - 'version' => WP_Theme_JSON::LATEST_SCHEMA, - 'settings' => array(), + public function test_block_pseudo_selectors_are_processed() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'text' => 'white', + 'background' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'blue', + 'background' => 'white', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'red', + 'background' => 'yellow', + ), + ), + ), + ), + ), + ) ); - $actual = WP_Theme_JSON::get_from_editor_settings( array() ); - $this->assertSameSetsWithIndex( $expected, $actual ); + $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link:focus){background-color: yellow;color: red;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); } /** - * @ticket 52991 + * Test that block pseudo selectors are processed correctly within variations. */ - public function test_get_editor_settings_custom_units_can_be_disabled() { - add_theme_support( 'custom-units', array() ); - $input = get_default_block_editor_settings(); + public function test_block_variation_pseudo_selectors_are_processed() { + register_block_style( + 'core/button', + array( + 'name' => 'outline', + 'label' => 'Outline', + ) + ); - $expected = array( - 'units' => array( array() ), - 'customPadding' => false, + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'text' => 'white', + 'background' => 'blue', + ), + 'variations' => array( + 'outline' => array( + 'color' => array( + 'text' => 'currentColor', + 'background' => 'transparent', + ), + 'border' => array( + 'color' => 'currentColor', + 'width' => '1px', + 'style' => 'solid', + ), + ':hover' => array( + 'color' => array( + 'text' => 'white', + 'background' => 'red', + ), + ), + ':focus' => array( + 'color' => array( + 'text' => 'black', + 'background' => 'yellow', + ), + ), + ), + ), + ), + ), + ), + ) ); - $actual = WP_Theme_JSON::get_from_editor_settings( $input ); + $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link){background-color: transparent;border-color: currentColor;border-width: 1px;border-style: solid;color: currentColor;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:hover){background-color: red;color: white;}:root :where(.wp-block-button.is-style-outline .wp-block-button__link:focus){background-color: yellow;color: black;}'; + $actual = $theme_json->get_stylesheet( + array( 'styles' ), + null, + array( + 'skip_root_layout_styles' => true, + 'include_block_style_variations' => true, + ) + ); + + unregister_block_style( 'core/button', 'outline' ); - $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] ); + $this->assertSame( $expected, $actual ); } /** - * @ticket 52991 + * Test that non-whitelisted pseudo selectors are ignored for blocks. */ - public function test_get_editor_settings_custom_units_can_be_enabled() { - add_theme_support( 'custom-units' ); - $input = get_default_block_editor_settings(); + public function test_block_pseudo_selectors_ignores_non_whitelisted() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'text' => 'white', + 'background' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'blue', + 'background' => 'white', + ), + ), + ':levitate' => array( + 'color' => array( + 'text' => 'yellow', + 'background' => 'black', + ), + ), + ), + ), + ), + ) + ); - $expected = array( - 'units' => array( 'px', 'em', 'rem', 'vh', 'vw', '%' ), - 'customPadding' => false, + $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + $this->assertStringNotContainsString( '.wp-block-button .wp-block-button__link:levitate{', $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * Test that blocks without pseudo selector support ignore pseudo selectors. + */ + public function test_blocks_without_pseudo_support_ignore_pseudo_selectors() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'text' => 'black', + ), + ':hover' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ), + ) ); - $actual = WP_Theme_JSON::get_from_editor_settings( $input ); + $expected = ':root :where(p){color: black;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); + $this->assertStringNotContainsString( 'p:hover{', $theme_json->get_stylesheet( array( 'styles' ) ) ); + } + + /** + * Test that block pseudo selectors work with elements within blocks. + */ + public function test_block_pseudo_selectors_with_elements() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'blocks' => array( + 'core/button' => array( + 'color' => array( + 'text' => 'white', + 'background' => 'blue', + ), + ':hover' => array( + 'color' => array( + 'text' => 'blue', + 'background' => 'white', + ), + ), + 'elements' => array( + 'button' => array( + 'color' => array( + 'text' => 'green', + ), + ':hover' => array( + 'color' => array( + 'text' => 'orange', + ), + ), + ), + ), + ), + ), + ), + ) + ); - $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] ); + $expected = ':root :where(.wp-block-button .wp-block-button__link){background-color: blue;color: white;}:root :where(.wp-block-button .wp-block-button__link:hover){background-color: white;color: blue;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button,.wp-block-button .wp-block-button__link .wp-block-button__link){color: green;}:root :where(.wp-block-button .wp-block-button__link .wp-element-button:hover,.wp-block-button .wp-block-button__link .wp-block-button__link:hover){color: orange;}'; + $this->assertSame( $expected, $theme_json->get_stylesheet( array( 'styles' ), null, array( 'skip_root_layout_styles' => true ) ) ); } /** - * @ticket 52991 + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 */ - public function test_get_editor_settings_custom_units_can_be_filtered() { - add_theme_support( 'custom-units', 'rem', 'em' ); - $input = get_default_block_editor_settings(); + public function test_sanitize_preserves_boolean_values_when_schema_expects_boolean() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'lightbox' => array( + 'enabled' => true, + 'allowEditing' => false, + ), + ), + ) + ); - $expected = array( - 'units' => array( 'rem', 'em' ), - 'customPadding' => false, + $settings = $theme_json->get_settings(); + $this->assertTrue( $settings['lightbox']['enabled'], 'Enabled should be true' ); + $this->assertFalse( $settings['lightbox']['allowEditing'], 'Allow editing should be false' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_removes_non_boolean_values_when_schema_expects_boolean() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'lightbox' => array( + 'enabled' => 'not-a-boolean', + 'allowEditing' => 123, + ), + ), + ) ); - $actual = WP_Theme_JSON::get_from_editor_settings( $input ); + $settings = $theme_json->get_settings(); + $this->assertArrayNotHasKey( 'enabled', $settings['lightbox'] ?? array(), 'Enabled should be removed' ); + $this->assertArrayNotHasKey( 'allowEditing', $settings['lightbox'] ?? array(), 'Allow editing should be removed' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_preserves_boolean_values_in_block_settings() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'lightbox' => array( + 'enabled' => true, + 'allowEditing' => false, + ), + ), + ), + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertTrue( $settings['blocks']['core/image']['lightbox']['enabled'], 'Enabled should be true' ); + $this->assertFalse( $settings['blocks']['core/image']['lightbox']['allowEditing'], 'Allow editing should be false' ); + } + + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_removes_non_boolean_values_in_block_settings() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'blocks' => array( + 'core/image' => array( + 'lightbox' => array( + 'enabled' => 'string-value', + 'allowEditing' => array( 'not', 'a', 'boolean' ), + ), + ), + ), + ), + ) + ); - $this->assertSameSetsWithIndex( $expected, $actual['settings']['spacing'] ); + $settings = $theme_json->get_settings(); + $lightbox = $settings['blocks']['core/image']['lightbox'] ?? array(); + $this->assertArrayNotHasKey( 'enabled', $lightbox, 'Enabled should be removed' ); + $this->assertArrayNotHasKey( 'allowEditing', $lightbox, 'Allow editing should be removed' ); } + /** + * @covers WP_Theme_JSON::sanitize + * @covers WP_Theme_JSON::remove_keys_not_in_schema + * + * @ticket 64280 + */ + public function test_sanitize_preserves_null_schema_behavior() { + // Test that settings with null in schema (no type validation) still accept any type. + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'appearanceTools' => 'string-value', // null in schema, should accept any type. + 'custom' => array( 'nested' => 'value' ), // null in schema, should accept any type. + ), + ) + ); + + $settings = $theme_json->get_settings(); + $this->assertSame( 'string-value', $settings['appearanceTools'], 'Appearance tools should be string value' ); + $this->assertSame( array( 'nested' => 'value' ), $settings['custom'], 'Custom should be array value' ); + } } diff --git a/tests/phpunit/tests/theme/wpThemeJsonResolver.php b/tests/phpunit/tests/theme/wpThemeJsonResolver.php index 7e4bd37d54d0a..ce5609a396f18 100644 --- a/tests/phpunit/tests/theme/wpThemeJsonResolver.php +++ b/tests/phpunit/tests/theme/wpThemeJsonResolver.php @@ -12,6 +12,89 @@ */ class Tests_Theme_wpThemeJsonResolver extends WP_UnitTestCase { + /** + * Administrator ID. + * + * @var int + */ + protected static $administrator_id; + + /** + * WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var ReflectionProperty + */ + private static $property_blocks_cache; + + /** + * Original value of the WP_Theme_JSON_Resolver::$blocks_cache property. + * + * @var array + */ + private static $property_blocks_cache_orig_value; + + /** + * WP_Theme_JSON_Resolver::$core property. + * + * @var ReflectionProperty + */ + private static $property_core; + + /** + * Original value of the WP_Theme_JSON_Resolver::$core property. + * + * @var WP_Theme_JSON + */ + private static $property_core_orig_value; + + /** + * Theme root directory. + * + * @var string|null + */ + private $theme_root; + + /** + * Original theme directory. + * + * @var array|null + */ + private $orig_theme_dir; + + /** + * @var array|null + */ + private $queries; + + public static function set_up_before_class() { + parent::set_up_before_class(); + + self::$administrator_id = self::factory()->user->create( + array( + 'role' => 'administrator', + 'user_email' => 'administrator@example.com', + ) + ); + + static::$property_blocks_cache = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'blocks_cache' ); + if ( PHP_VERSION_ID < 80100 ) { + static::$property_blocks_cache->setAccessible( true ); + } + static::$property_blocks_cache_orig_value = static::$property_blocks_cache->getValue(); + + static::$property_core = new ReflectionProperty( WP_Theme_JSON_Resolver::class, 'core' ); + if ( PHP_VERSION_ID < 80100 ) { + static::$property_core->setAccessible( true ); + } + static::$property_core_orig_value = static::$property_core->getValue(); + } + + public static function tear_down_after_class() { + static::$property_blocks_cache->setValue( null, static::$property_blocks_cache_orig_value ); + static::$property_core->setValue( null, static::$property_core_orig_value ); + parent::tear_down_after_class(); + } + public function set_up() { parent::set_up(); $this->theme_root = realpath( DIR_TESTDATA . '/themedir1' ); @@ -24,6 +107,8 @@ public function set_up() { add_filter( 'theme_root', array( $this, 'filter_set_theme_root' ) ); add_filter( 'stylesheet_root', array( $this, 'filter_set_theme_root' ) ); add_filter( 'template_root', array( $this, 'filter_set_theme_root' ) ); + add_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) ); + $this->queries = array(); // Clear caches. wp_clean_themes_cache(); unset( $GLOBALS['wp_themes'] ); @@ -33,9 +118,24 @@ public function tear_down() { $GLOBALS['wp_theme_directories'] = $this->orig_theme_dir; wp_clean_themes_cache(); unset( $GLOBALS['wp_themes'] ); + remove_filter( 'theme_file_uri', array( $this, 'filter_theme_file_uri' ) ); + + // Reset data between tests. + wp_clean_theme_json_cache(); parent::tear_down(); } + /* + * This filter callback normalizes the return value from `get_theme_file_uri` + * to guard against changes in test environments. + * The test suite otherwise returns full system dir path, e.g., + * /var/www/tests/phpunit/includes/../data/themedir1/block-theme/assets/sugarloaf-mountain.jpg + */ + public function filter_theme_file_uri( $file ) { + $file_name = substr( strrchr( $file, '/' ), 1 ); + return 'https://example.org/wp-content/themes/example-theme/assets/' . $file_name; + } + public function filter_set_theme_root() { return $this->theme_root; } @@ -46,22 +146,27 @@ public function filter_set_locale_to_polish() { /** * @ticket 52991 + * @ticket 54336 + * @ticket 56611 */ public function test_translations_are_applied() { add_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); load_textdomain( 'block-theme', realpath( DIR_TESTDATA . '/languages/themes/block-theme-pl_PL.mo' ) ); switch_theme( 'block-theme' ); - - $actual = WP_Theme_JSON_Resolver::get_theme_data(); + $theme_data = WP_Theme_JSON_Resolver::get_theme_data(); + $style_variations = WP_Theme_JSON_Resolver::get_style_variations(); unload_textdomain( 'block-theme' ); remove_filter( 'locale', array( $this, 'filter_set_locale_to_polish' ) ); - $this->assertSame( wp_get_theme()->get( 'TextDomain' ), 'block-theme' ); + $this->assertSame( 'block-theme', wp_get_theme()->get( 'TextDomain' ) ); + $this->assertSame( 'Motyw blokowy', $theme_data->get_data()['title'] ); $this->assertSame( array( 'color' => array( + 'custom' => false, + 'customGradient' => false, 'palette' => array( 'theme' => array( array( @@ -85,11 +190,20 @@ public function test_translations_are_applied() { ), ), ), - 'custom' => false, - 'customGradient' => false, + 'duotone' => array( + 'theme' => array( + array( + 'colors' => array( '#333333', '#aaaaaa' ), + 'slug' => 'custom-duotone', + 'name' => 'Custom Duotone', + ), + ), + ), ), 'typography' => array( - 'fontSizes' => array( + 'customFontSize' => false, + 'lineHeight' => true, + 'fontSizes' => array( 'theme' => array( array( 'name' => 'Custom', @@ -98,14 +212,27 @@ public function test_translations_are_applied() { ), ), ), - 'customFontSize' => false, - 'customLineHeight' => true, ), 'spacing' => array( - 'units' => array( - 'rem', + 'units' => array( 'rem' ), + 'padding' => true, + 'blockGap' => true, + ), + 'shadow' => array( + 'presets' => array( + 'theme' => array( + array( + 'name' => 'Natural', + 'slug' => 'natural', + 'shadow' => '2px 2px 3px #000', + ), + array( + 'name' => 'Test', + 'slug' => 'test', + 'shadow' => '2px 2px 3px #000', + ), + ), ), - 'customPadding' => true, ), 'blocks' => array( 'core/paragraph' => array( @@ -123,24 +250,1238 @@ public function test_translations_are_applied() { ), ), ), - $actual->get_settings() + $theme_data->get_settings() + ); + + $custom_templates = $theme_data->get_custom_templates(); + $this->assertArrayHasKey( 'page-home', $custom_templates ); + $this->assertSame( + array( + 'title' => 'Szablon strony głównej', + 'postTypes' => array( 'page' ), + ), + $custom_templates['page-home'] + ); + + $this->assertSameSets( + array( + 'small-header' => array( + 'title' => 'Mały nagłówek', + 'area' => 'header', + ), + ), + $theme_data->get_template_parts() ); + + $this->assertSame( + 'Wariant motywu blokowego', + $style_variations[2]['title'] + ); + } + + private function get_registered_block_names( $hard_reset = false ) { + static $expected_block_names; + + if ( ! $hard_reset && ! empty( $expected_block_names ) ) { + return $expected_block_names; + } + + $expected_block_names = array(); + $resolver = WP_Block_Type_Registry::get_instance(); + $blocks = $resolver->get_all_registered(); + foreach ( array_keys( $blocks ) as $block_name ) { + $expected_block_names[ $block_name ] = true; + } + + return $expected_block_names; } /** - * @ticket 52991 + * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or + * does not match the all registered blocks. + * + * Though this is a non-public method, it is vital to other functionality. + * Therefore, tests are provided to validate it functions as expected. + * + * @dataProvider data_has_same_registered_blocks_when_all_blocks_not_cached + * @ticket 56467 + * + * @param string $origin The origin to test. + */ + public function test_has_same_registered_blocks_when_all_blocks_not_cached( $origin, array $cache = array() ) { + $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' ); + if ( PHP_VERSION_ID < 80100 ) { + $has_same_registered_blocks->setAccessible( true ); + } + $expected_cache = $this->get_registered_block_names(); + + // Set up the blocks cache for the origin. + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache[ $origin ] = $cache; + static::$property_blocks_cache->setValue( null, $blocks_cache ); + + $this->assertFalse( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return false when same blocks are not cached' ); + $blocks_cache = static::$property_blocks_cache->getValue(); + $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_has_same_registered_blocks_when_all_blocks_not_cached() { + return array( + 'origin: core; cache: empty' => array( + 'origin' => 'core', + ), + 'origin: blocks; cache: empty' => array( + 'origin' => 'blocks', + ), + 'origin: theme; cache: empty' => array( + 'origin' => 'theme', + ), + 'origin: user; cache: empty' => array( + 'origin' => 'user', + ), + 'origin: core; cache: not empty' => array( + 'origin' => 'core', + 'cache' => array( + 'core/block' => true, + ), + ), + 'origin: blocks; cache: not empty' => array( + 'origin' => 'blocks', + 'cache' => array( + 'core/block' => true, + 'core/comments' => true, + ), + ), + 'origin: theme; cache: not empty' => array( + 'origin' => 'theme', + 'cache' => array( + 'core/cover' => true, + ), + ), + 'origin: user; cache: not empty' => array( + 'origin' => 'user', + 'cache' => array( + 'core/gallery' => true, + ), + ), + ); + } + + /** + * Tests when WP_Theme_JSON_Resolver::$blocks_cache is empty or + * does not match the all registered blocks. + * + * Though this is a non-public method, it is vital to other functionality. + * Therefore, tests are provided to validate it functions as expected. + * + * @dataProvider data_has_same_registered_blocks_when_all_blocks_are_cached + * @ticket 56467 + * + * @param string $origin The origin to test. + */ + public function test_has_same_registered_blocks_when_all_blocks_are_cached( $origin ) { + $has_same_registered_blocks = new ReflectionMethod( WP_Theme_JSON_Resolver::class, 'has_same_registered_blocks' ); + if ( PHP_VERSION_ID < 80100 ) { + $has_same_registered_blocks->setAccessible( true ); + } + $expected_cache = $this->get_registered_block_names(); + + // Set up the cache with all registered blocks. + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache[ $origin ] = $this->get_registered_block_names(); + static::$property_blocks_cache->setValue( null, $blocks_cache ); + + $this->assertTrue( $has_same_registered_blocks->invoke( null, $origin ), 'WP_Theme_JSON_Resolver::has_same_registered_blocks() should return true when using the cache' ); + $this->assertSameSets( $expected_cache, $blocks_cache[ $origin ], 'WP_Theme_JSON_Resolver::$blocks_cache should contain all expected block names for the given origin' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_has_same_registered_blocks_when_all_blocks_are_cached() { + return array( + 'core' => array( 'core' ), + 'blocks' => array( 'blocks' ), + 'theme' => array( 'theme' ), + 'user' => array( 'user' ), + ); + } + + /** + * @dataProvider data_get_core_data + * @covers WP_Theme_JSON_Resolver::get_core_data + * @ticket 56467 + */ + public function test_get_core_data( $should_fire_filter, $core_is_cached, $blocks_are_cached ) { + wp_clean_theme_json_cache(); + + // If should cache core, then fire the method to cache it before running the tests. + if ( $core_is_cached ) { + WP_Theme_JSON_Resolver::get_core_data(); + } + + // If should cache registered blocks, then set them up before running the tests. + if ( $blocks_are_cached ) { + $blocks_cache = static::$property_blocks_cache->getValue(); + $blocks_cache['core'] = $this->get_registered_block_names(); + static::$property_blocks_cache->setValue( null, $blocks_cache ); + } + + $expected_filter_count = did_filter( 'wp_theme_json_data_default' ); + $actual = WP_Theme_JSON_Resolver::get_core_data(); + if ( $should_fire_filter ) { + ++$expected_filter_count; + } + + $this->assertSame( $expected_filter_count, did_filter( 'wp_theme_json_data_default' ), 'The filter "wp_theme_json_data_default" should fire the given number of times' ); + $this->assertInstanceOf( WP_Theme_JSON::class, $actual, 'WP_Theme_JSON_Resolver::get_core_data() should return instance of WP_Theme_JSON' ); + $this->assertSame( static::$property_core->getValue(), $actual, 'WP_Theme_JSON_Resolver::$core property should be the same object as returned from WP_Theme_JSON_Resolver::get_core_data()' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_get_core_data() { + return array( + 'When both caches are empty' => array( + 'should_fire_filter' => true, + 'core_is_cached' => false, + 'blocks_are_cached' => false, + ), + 'When the blocks_cache is not empty and matches' => array( + 'should_fire_filter' => true, + 'core_is_cached' => false, + 'blocks_are_cached' => true, + ), + 'When blocks_cache is empty but core cache is not' => array( + 'should_fire_filter' => true, + 'core_is_cached' => true, + 'blocks_are_cached' => false, + ), + 'When both caches are not empty' => array( + 'should_fire_filter' => true, + 'core_is_cached' => true, + 'blocks_are_cached' => false, + ), + ); + } + + /** + * @ticket 54336 + * @ticket 60118 + * + * @covers ::add_theme_support + */ + public function test_add_theme_supports_are_loaded_for_themes_without_theme_json() { + switch_theme( 'default' ); + $color_palette = array( + array( + 'name' => 'Primary', + 'slug' => 'primary', + 'color' => '#F00', + ), + array( + 'name' => 'Secondary', + 'slug' => 'secondary', + 'color' => '#0F0', + ), + array( + 'name' => 'Tertiary', + 'slug' => 'tertiary', + 'color' => '#00F', + ), + ); + add_theme_support( 'editor-color-palette', $color_palette ); + add_theme_support( 'custom-line-height' ); + add_theme_support( 'appearance-tools' ); + + $settings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings(); + + remove_theme_support( 'custom-line-height' ); + remove_theme_support( 'editor-color-palette' ); + remove_theme_support( 'appearance-tools' ); + + $this->assertFalse( wp_theme_has_theme_json() ); + $this->assertTrue( $settings['typography']['lineHeight'] ); + $this->assertSame( $color_palette, $settings['color']['palette']['theme'] ); + $this->assertTrue( $settings['border']['color'], 'Support for "appearance-tools" was not added.' ); + } + + /** + * Tests that classic themes still get core default settings such as color palette and duotone. + * + * @ticket 60136 + */ + public function test_core_default_settings_are_loaded_for_themes_without_theme_json() { + switch_theme( 'default' ); + + $settings = WP_Theme_JSON_Resolver::get_merged_data( 'theme' )->get_settings(); + + $this->assertFalse( wp_theme_has_theme_json() ); + $this->assertTrue( $settings['color']['defaultPalette'] ); + $this->assertTrue( $settings['color']['defaultDuotone'] ); + $this->assertTrue( $settings['color']['defaultGradients'] ); + } + + /** + * @ticket 54336 + * @ticket 56611 + */ + public function test_merges_child_theme_json_into_parent_theme_json() { + switch_theme( 'block-theme-child' ); + + $actual_settings = WP_Theme_JSON_Resolver::get_theme_data()->get_settings(); + $expected_settings = array( + 'color' => array( + 'custom' => false, + 'customGradient' => false, + 'duotone' => array( + 'theme' => array( + array( + 'colors' => array( '#333333', '#aaaaaa' ), + 'name' => 'Custom Duotone', + 'slug' => 'custom-duotone', + ), + ), + ), + 'gradients' => array( + 'theme' => array( + array( + 'name' => 'Custom gradient', + 'gradient' => 'linear-gradient(135deg,rgba(0,0,0) 0%,rgb(0,0,0) 100%)', + 'slug' => 'custom-gradient', + ), + ), + ), + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f3f4f6', + ), + array( + 'slug' => 'primary', + 'name' => 'Primary', + 'color' => '#3858e9', + ), + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#111827', + ), + ), + ), + 'link' => true, + ), + 'typography' => array( + 'customFontSize' => false, + 'lineHeight' => true, + 'fontSizes' => array( + 'theme' => array( + array( + 'name' => 'Custom', + 'slug' => 'custom', + 'size' => '100px', + ), + ), + ), + ), + 'shadow' => array( + 'presets' => array( + 'theme' => array( + array( + 'name' => 'Natural', + 'slug' => 'natural', + 'shadow' => '2px 2px 3px #000', + ), + array( + 'name' => 'Test', + 'slug' => 'test', + 'shadow' => '2px 2px 3px #000', + ), + ), + ), + ), + 'spacing' => array( + 'blockGap' => true, + 'units' => array( 'rem' ), + 'padding' => true, + ), + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f5f7f9', + ), + ), + ), + ), + ), + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'light', + 'name' => 'Light', + 'color' => '#f3f4f6', + ), + ), + ), + ), + ), + ), + ); + wp_recursive_ksort( $actual_settings ); + wp_recursive_ksort( $expected_settings ); + + // Should merge settings. + $this->assertSame( + $expected_settings, + $actual_settings + ); + + $this->assertSame( + array( + 'page-home' => array( + 'title' => 'Homepage', + 'postTypes' => array( 'page' ), + ), + 'custom-single-post-template' => array( + 'title' => 'Custom Single Post template', + 'postTypes' => array( 'post' ), + ), + ), + WP_Theme_JSON_Resolver::get_theme_data()->get_custom_templates() + ); + } + + /** + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_does_not_use_uncached_queries() { + // Switch to a theme that does have support. + switch_theme( 'block-theme' ); + wp_set_current_user( self::$administrator_id ); + $theme = wp_get_theme(); + WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $global_styles_query_count = 0; + add_filter( + 'query', + static function ( $query ) use ( &$global_styles_query_count ) { + if ( preg_match( '#post_type = \'wp_global_styles\'#', $query ) ) { + $global_styles_query_count++; + } + return $query; + } + ); + for ( $i = 0; $i < 3; $i++ ) { + WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + wp_clean_theme_json_cache(); + } + $this->assertSame( 0, $global_styles_query_count, 'Unexpected SQL queries detected for the wp_global_style post type prior to creation.' ); + + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $this->assertEmpty( $user_cpt, 'User CPT is expected to be empty.' ); + + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertNotEmpty( $user_cpt, 'User CPT is expected not to be empty.' ); + + $global_styles_query_count = 0; + for ( $i = 0; $i < 3; $i++ ) { + $new_user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + wp_clean_theme_json_cache(); + $this->assertSameSets( $user_cpt, $new_user_cpt, "User CPTs do not match on run {$i}." ); + } + $this->assertSame( 1, $global_styles_query_count, 'Unexpected SQL queries detected for the wp_global_style post type after creation.' ); + } + + /** + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_does_not_use_uncached_queries_for_logged_out_users() { + // Switch to a theme that does have support. + switch_theme( 'block-theme' ); + $theme = wp_get_theme(); + WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $query_count = get_num_queries(); + for ( $i = 0; $i < 3; $i++ ) { + WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + wp_clean_theme_json_cache(); + } + $query_count = get_num_queries() - $query_count; + $this->assertSame( 0, $query_count, 'Unexpected SQL queries detected for the wp_global_style post type prior to creation.' ); + + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $this->assertEmpty( $user_cpt, 'User CPT is expected to be empty.' ); + } + + /** + * @ticket 56945 + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_runs_for_classic_themes() { + // The 'default' theme does not support theme.json (classic theme). + switch_theme( 'default' ); + wp_set_current_user( self::$administrator_id ); + $theme = wp_get_theme(); + + // Classic themes should now be able to access user global styles data. + // When should_create_post is true, it should create a post. + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertIsArray( $user_cpt, 'User CPT should be an array for classic themes.' ); + $this->assertArrayHasKey( 'ID', $user_cpt, 'User CPT should have an ID for classic themes.' ); + + // Clean up the created post. + if ( isset( $user_cpt['ID'] ) ) { + wp_delete_post( $user_cpt['ID'], true ); + } + } + + /** + * @ticket 55392 + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_does_exist() { + // Switch to a theme that does have support. + switch_theme( 'block-theme' ); + $theme = wp_get_theme(); + $post1 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertIsArray( $post1 ); + $this->assertArrayHasKey( 'ID', $post1 ); + wp_delete_post( $post1['ID'], true ); + $post2 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertIsArray( $post2 ); + $this->assertArrayHasKey( 'ID', $post2 ); + } + + /** + * @ticket 55392 + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles */ - public function test_switching_themes_recalculates_data() { - // By default, the theme for unit tests is "default", - // which doesn't have theme.json support. - $default = WP_Theme_JSON_Resolver::theme_has_support(); + public function test_get_user_data_from_wp_global_styles_create_post() { + // Switch to a theme that does have support. + switch_theme( 'block-theme' ); + $theme = wp_get_theme( 'testing' ); + $post1 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $this->assertIsArray( $post1 ); + $this->assertSameSets( array(), $post1 ); + $post2 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme ); + $this->assertIsArray( $post2 ); + $this->assertSameSets( array(), $post2 ); + $post3 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true ); + $this->assertIsArray( $post3 ); + $this->assertArrayHasKey( 'ID', $post3 ); + } + /** + * @ticket 55392 + * @covers WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles + */ + public function test_get_user_data_from_wp_global_styles_filter_state() { // Switch to a theme that does have support. switch_theme( 'block-theme' ); - $has_theme_json_support = WP_Theme_JSON_Resolver::theme_has_support(); + $theme = wp_get_theme( 'foo' ); + $post1 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, true, array( 'publish' ) ); + $this->assertIsArray( $post1 ); + $this->assertArrayHasKey( 'ID', $post1 ); + $post2 = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( $theme, false, array( 'draft' ) ); + $this->assertIsArray( $post2 ); + $this->assertSameSets( array(), $post2 ); + } + + /** + * @ticket 56835 + * @covers WP_Theme_JSON_Resolver::get_theme_data + */ + public function test_get_theme_data_theme_supports_overrides_theme_json() { + switch_theme( 'default' ); + + // Test that get_theme_data() returns a WP_Theme_JSON object. + $theme_json_resolver = new WP_Theme_JSON_Resolver(); + $theme_json_resolver->get_merged_data(); + $theme_data = $theme_json_resolver->get_theme_data(); + $this->assertInstanceOf( 'WP_Theme_JSON', $theme_data, 'Theme data should be an instance of WP_Theme_JSON.' ); + + // Test that wp_theme_json_data_theme filter has been called. + $this->assertGreaterThan( 0, did_filter( 'wp_theme_json_data_default' ), 'The filter "wp_theme_json_data_default" should fire.' ); + + // Test that data from theme.json is backfilled from existing theme supports. + $previous_settings = $theme_data->get_settings(); + $previous_line_height = $previous_settings['typography']['lineHeight']; + $this->assertFalse( $previous_line_height, 'lineHeight setting from theme.json should be false.' ); + + add_theme_support( 'custom-line-height' ); + $current_settings = $theme_json_resolver->get_theme_data()->get_settings(); + $line_height = $current_settings['typography']['lineHeight']; + $this->assertTrue( $line_height, 'lineHeight setting after add_theme_support() should be true.' ); + remove_theme_support( 'custom-line-height' ); + } + + /** + * @ticket 56945 + * @covers WP_Theme_JSON_Resolver::get_theme_data + */ + public function test_get_theme_data_does_not_parse_theme_json_if_not_present() { + // The 'default' theme does not support theme.json. + switch_theme( 'default' ); + + $theme_json_resolver = new WP_Theme_JSON_Resolver(); + + // Force-unset $i18n_schema property to "unload" translation schema. + $property = new ReflectionProperty( $theme_json_resolver, 'i18n_schema' ); + if ( PHP_VERSION_ID < 80100 ) { + $property->setAccessible( true ); + } + $property->setValue( null, null ); + + // A completely empty theme.json data set still has the 'version' key when parsed. + $empty_theme_json = array( 'version' => WP_Theme_JSON::LATEST_SCHEMA ); + + // Call using 'with_supports' set to false, so that the method only considers theme.json. + $theme_data = $theme_json_resolver->get_theme_data( array(), array( 'with_supports' => false ) ); + $this->assertInstanceOf( 'WP_Theme_JSON', $theme_data, 'Theme data should be an instance of WP_Theme_JSON.' ); + $this->assertSame( $empty_theme_json, $theme_data->get_raw_data(), 'Theme data should be empty without theme support.' ); + $this->assertNull( $property->getValue(), 'Theme i18n schema should not have been loaded without theme support.' ); + } + + /** + * Tests that get_merged_data returns the data merged up to the proper origin. + * + * @ticket 57545 + * + * @covers WP_Theme_JSON_Resolver::get_merged_data + * + * @dataProvider data_get_merged_data_returns_origin + * + * @param string $origin What origin to get data from. + * @param bool $core_palette Whether the core palette is present. + * @param string $core_palette_text Message. + * @param string $block_styles Whether the block styles are present. + * @param string $block_styles_text Message. + * @param bool $theme_palette Whether the theme palette is present. + * @param string $theme_palette_text Message. + * @param bool $user_palette Whether the user palette is present. + * @param string $user_palette_text Message. + */ + public function test_get_merged_data_returns_origin( $origin, $core_palette, $core_palette_text, $block_styles, $block_styles_text, $theme_palette, $theme_palette_text, $user_palette, $user_palette_text ) { + // Make sure there is data from the blocks origin. + register_block_type( + 'my/block-with-styles', + array( + 'api_version' => 3, + 'attributes' => array( + 'borderColor' => array( + 'type' => 'string', + ), + 'style' => array( + 'type' => 'object', + ), + ), + 'supports' => array( + '__experimentalStyle' => array( + 'typography' => array( + 'fontSize' => '42rem', + ), + ), + ), + ) + ); + + // Make sure there is data from the theme origin. + switch_theme( 'block-theme' ); + + // Make sure there is data from the user origin. + wp_set_current_user( self::$administrator_id ); + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + $config = json_decode( $user_cpt['post_content'], true ); + $config['settings']['color']['palette']['custom'] = array( + array( + 'color' => 'hotpink', + 'name' => 'My color', + 'slug' => 'my-color', + ), + ); + $user_cpt['post_content'] = wp_json_encode( $config ); + wp_update_post( $user_cpt, true, false ); + + $theme_json = WP_Theme_JSON_Resolver::get_merged_data( $origin ); + $settings = $theme_json->get_settings(); + $styles = $theme_json->get_styles_block_nodes(); + $styles = array_filter( + $styles, + static function ( $element ) { + return isset( $element['name'] ) && 'my/block-with-styles' === $element['name']; + } + ); + unregister_block_type( 'my/block-with-styles' ); + + $this->assertSame( $core_palette, isset( $settings['color']['palette']['default'] ), $core_palette_text ); + $this->assertSame( $block_styles, count( $styles ) === 1, $block_styles_text ); + $this->assertSame( $theme_palette, isset( $settings['color']['palette']['theme'] ), $theme_palette_text ); + $this->assertSame( $user_palette, isset( $settings['color']['palette']['custom'] ), $user_palette_text ); + } + + /** + * Tests that get_merged_data returns the data merged up to the proper origin + * and that the core values have the proper data. + * + * @ticket 57824 + * + * @covers WP_Theme_JSON_Resolver::get_merged_data + */ + public function test_get_merged_data_returns_origin_proper() { + // Make sure the theme has a theme.json + // though it doesn't have any data for styles.spacing.padding. + switch_theme( 'block-theme' ); + + // Make sure the user defined some data for styles.spacing.padding. + wp_set_current_user( self::$administrator_id ); + $user_cpt = WP_Theme_JSON_Resolver::get_user_data_from_wp_global_styles( wp_get_theme(), true ); + $config = json_decode( $user_cpt['post_content'], true ); + $config['styles']['spacing']['padding'] = array( + 'top' => '23px', + 'left' => '23px', + 'bottom' => '23px', + 'right' => '23px', + ); + $user_cpt['post_content'] = wp_json_encode( $config ); + wp_update_post( $user_cpt, true, false ); + + // Query data from the user origin and then for the theme origin. + $theme_json_user = WP_Theme_JSON_Resolver::get_merged_data( 'custom' ); + $padding_user = $theme_json_user->get_raw_data()['styles']['spacing']['padding']; + $theme_json_theme = WP_Theme_JSON_Resolver::get_merged_data( 'theme' ); + $padding_theme = $theme_json_theme->get_raw_data()['styles']['spacing']['padding']; + + $this->assertSame( '23px', $padding_user['top'] ); + $this->assertSame( '23px', $padding_user['right'] ); + $this->assertSame( '23px', $padding_user['bottom'] ); + $this->assertSame( '23px', $padding_user['left'] ); + $this->assertSame( '0px', $padding_theme['top'] ); + $this->assertSame( '0px', $padding_theme['right'] ); + $this->assertSame( '0px', $padding_theme['bottom'] ); + $this->assertSame( '0px', $padding_theme['left'] ); + } + + /** + * Data provider for test_get_merged_data_returns_origin. + * + * @return array[] + */ + public function data_get_merged_data_returns_origin() { + return array( + 'origin_default' => array( + 'origin' => 'default', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => false, + 'block_styles_text' => 'Block styles should not be present', + 'theme_palette' => false, + 'theme_palette_text' => 'Theme palette should not be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_blocks' => array( + 'origin' => 'blocks', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => false, + 'theme_palette_text' => 'Theme palette should not be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_theme' => array( + 'origin' => 'theme', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => true, + 'theme_palette_text' => 'Theme palette must be present', + 'user_palette' => false, + 'user_palette_text' => 'User palette should not be present', + ), + 'origin_custom' => array( + 'origin' => 'custom', + 'core_palette' => true, + 'core_palette_text' => 'Core palette must be present', + 'block_styles' => true, + 'block_styles_text' => 'Block styles must be present', + 'theme_palette' => true, + 'theme_palette_text' => 'Theme palette must be present', + 'user_palette' => true, + 'user_palette_text' => 'User palette must be present', + ), + ); + } + + /** + * Tests that `get_style_variations` returns all the appropriate variations, + * including parent variations if the theme is a child, and that the child + * variation overwrites the parent variation of the same name. + * + * Note: This covers both theme and block style variations. + * + * @ticket 57545 + * @ticket 61312 + * + * @covers WP_Theme_JSON_Resolver::get_style_variations + * + * @dataProvider data_get_style_variations + * + * @param string $theme Name of the theme to use. + * @param string $scope Scope to filter variations by e.g. theme vs block. + * @param array $expected_variations Collection of expected variations. + */ + public function test_get_style_variations( $theme, $scope, $expected_variations ) { + switch_theme( $theme ); + wp_set_current_user( self::$administrator_id ); + + $actual_variations = WP_Theme_JSON_Resolver::get_style_variations( $scope ); + + wp_recursive_ksort( $actual_variations ); + wp_recursive_ksort( $expected_variations ); + + $this->assertSame( $expected_variations, $actual_variations ); + } + + /** + * Data provider for test_get_style_variations + * + * @return array + */ + public function data_get_style_variations() { + return array( + // @ticket 57545 + 'theme_style_variations' => array( + 'theme' => 'block-theme-child', + 'scope' => 'theme', + 'expected_variations' => array( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'variation-a', + 'settings' => array( + 'blocks' => array( + 'core/paragraph' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), + ), + ), + ), + ), + ), + ), + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'variation-b', + 'settings' => array( + 'blocks' => array( + 'core/post-title' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'dark', + 'name' => 'Dark', + 'color' => '#010101', + ), + ), + ), + ), + ), + ), + ), + ), + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'title' => 'Block theme variation', + 'settings' => array( + 'color' => array( + 'palette' => array( + 'theme' => array( + array( + 'slug' => 'foreground', + 'name' => 'Foreground', + 'color' => '#3F67C6', + ), + ), + ), + ), + ), + 'styles' => array( + 'blocks' => array( + 'core/post-title' => array( + 'typography' => array( + 'fontWeight' => '700', + ), + ), + ), + ), + ), + ), + ), + 'block_style_variations' => array( + 'theme' => 'block-theme-child-with-block-style-variations', + 'scope' => 'block', + 'expected_variations' => array( + array( + 'blockTypes' => array( 'core/group', 'core/columns', 'core/media-text' ), + 'version' => 3, + 'title' => 'block-style-variation-a', + 'styles' => array( + 'color' => array( + 'background' => 'darkcyan', + 'text' => 'aliceblue', + ), + ), + ), + array( + 'blockTypes' => array( 'core/group', 'core/columns' ), + 'version' => 3, + 'title' => 'block-style-variation-b', + 'styles' => array( + 'color' => array( + 'background' => 'midnightblue', + 'text' => 'lightblue', + ), + ), + ), + // @ticket 61440 + array( + 'blockTypes' => array( 'core/group', 'core/columns' ), + 'version' => 3, + 'slug' => 'WithSlug', + 'title' => 'With Slug', + 'styles' => array( + 'color' => array( + 'background' => 'aliceblue', + 'text' => 'midnightblue', + ), + ), + ), + ), + ), + ); + } + + /** + * @ticket 60815 + */ + public function test_theme_shadow_presets_do_not_override_default_shadow_presets() { + switch_theme( 'block-theme' ); + + $theme_json_resolver = new WP_Theme_JSON_Resolver(); + $theme_json = $theme_json_resolver->get_merged_data(); + $actual_settings = $theme_json->get_settings()['shadow']['presets']; + + $expected_settings = array( + 'default' => array( + array( + 'name' => 'Natural', + 'shadow' => '6px 6px 9px rgba(0, 0, 0, 0.2)', + 'slug' => 'natural', + ), + array( + 'name' => 'Deep', + 'shadow' => '12px 12px 50px rgba(0, 0, 0, 0.4)', + 'slug' => 'deep', + ), + array( + 'name' => 'Sharp', + 'shadow' => '6px 6px 0px rgba(0, 0, 0, 0.2)', + 'slug' => 'sharp', + ), + array( + 'name' => 'Outlined', + 'shadow' => '6px 6px 0px -3px rgb(255, 255, 255), 6px 6px rgb(0, 0, 0)', + 'slug' => 'outlined', + ), + array( + 'name' => 'Crisp', + 'shadow' => '6px 6px 0px rgb(0, 0, 0)', + 'slug' => 'crisp', + ), + ), + 'theme' => array( + array( + 'name' => 'Test', + 'shadow' => '2px 2px 3px #000', + 'slug' => 'test', + ), + ), + ); + + wp_recursive_ksort( $actual_settings ); + wp_recursive_ksort( $expected_settings ); + + $this->assertSame( + $expected_settings, + $actual_settings + ); + } + + /** + * @ticket 60815 + */ + public function test_shadow_default_presets_value_for_block_and_classic_themes() { + $theme_json_resolver = new WP_Theme_JSON_Resolver(); + $theme_json = $theme_json_resolver->get_merged_data(); + + $default_presets_for_classic = $theme_json->get_settings()['shadow']['defaultPresets']; + $this->assertFalse( $default_presets_for_classic ); + + switch_theme( 'block-theme' ); + $theme_json_resolver = new WP_Theme_JSON_Resolver(); + $theme_json = $theme_json_resolver->get_merged_data(); + + $default_presets_for_block = $theme_json->get_settings()['shadow']['defaultPresets']; + $this->assertTrue( $default_presets_for_block ); + } + + /** + * Tests that relative paths are resolved and merged into the theme.json data. + * + * @covers WP_Theme_JSON_Resolver::resolve_theme_file_uris + * @ticket 61273 + * @ticket 61588 + */ + public function test_resolve_theme_file_uris() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/image.png', + ), + ), + 'blocks' => array( + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/quote.png', + ), + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/verse.png', + ), + ), + ), + ), + ), + ) + ); + + $expected_data = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.org/wp-content/themes/example-theme/assets/image.png', + ), + ), + 'blocks' => array( + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.org/wp-content/themes/example-theme/assets/quote.png', + ), + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'https://example.org/wp-content/themes/example-theme/assets/verse.png', + ), + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Resolver::resolve_theme_file_uris( $theme_json ); + + $this->assertSame( $expected_data, $actual->get_raw_data() ); + } + + /** + * Tests that them uris are resolved and bundled with other metadata in an array. + * + * @covers WP_Theme_JSON_Resolver::get_resolved_theme_uris + * @ticket 61273 + * @ticket 61588 + */ + public function test_get_resolved_theme_uris() { + $theme_json = new WP_Theme_JSON( + array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'styles' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/image.png', + ), + ), + 'blocks' => array( + 'core/quote' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/quote.jpg', + ), + ), + ), + 'core/verse' => array( + 'background' => array( + 'backgroundImage' => array( + 'url' => 'file:./assets/verse.gif', + ), + ), + ), + ), + ), + ) + ); + + $expected_data = array( + array( + 'name' => 'file:./assets/image.png', + 'href' => 'https://example.org/wp-content/themes/example-theme/assets/image.png', + 'target' => 'styles.background.backgroundImage.url', + 'type' => 'image/png', + ), + array( + 'name' => 'file:./assets/quote.jpg', + 'href' => 'https://example.org/wp-content/themes/example-theme/assets/quote.jpg', + 'target' => 'styles.blocks.core/quote.background.backgroundImage.url', + 'type' => 'image/jpeg', + ), + array( + 'name' => 'file:./assets/verse.gif', + 'href' => 'https://example.org/wp-content/themes/example-theme/assets/verse.gif', + 'target' => 'styles.blocks.core/verse.background.backgroundImage.url', + 'type' => 'image/gif', + ), + ); + + $actual = WP_Theme_JSON_Resolver::get_resolved_theme_uris( $theme_json ); - $this->assertFalse( $default ); - $this->assertTrue( $has_theme_json_support ); + $this->assertSame( $expected_data, $actual ); } + /** + * Tests that block style variations data gets merged in the following + * priority order, from highest priority to lowest. + * + * - `styles.blocks.blockType.variations` from theme.json + * - `styles.variations` from theme.json + * - variations from block style variation files under `/styles` + * - variations from `WP_Block_Styles_Registry` + * + * @ticket 61451 + */ + public function test_block_style_variation_merge_order() { + switch_theme( 'block-theme-child-with-block-style-variations' ); + + /* + * Register style for a block that isn't included in the block style variation's partial + * theme.json's blockTypes. The name must match though so we can ensure the partial's + * styles do not get applied to this block. + */ + register_block_style( + 'core/heading', + array( + 'name' => 'block-style-variation-b', + 'label' => 'Heading only variation', + ) + ); + + // Register variation for a block that will be partially overridden at all levels. + register_block_style( + 'core/media-text', + array( + 'name' => 'block-style-variation-a', + 'label' => 'Block Style Variation A', + 'style_data' => array( + 'color' => array( + 'background' => 'pink', + 'gradient' => 'var(--custom)', + ), + ), + ) + ); + + $data = WP_Theme_JSON_Resolver::get_theme_data()->get_raw_data(); + $block_styles = $data['styles']['blocks'] ?? array(); + $actual = array_intersect_key( + $block_styles, + array_flip( array( 'core/button', 'core/media-text', 'core/heading' ) ) + ); + $expected = array( + 'core/button' => array( + 'variations' => array( + 'outline' => array( + 'color' => array( + 'background' => 'red', + 'text' => 'white', + ), + ), + ), + ), + 'core/media-text' => array( + 'variations' => array( + 'block-style-variation-a' => array( + 'color' => array( + 'background' => 'blue', + 'gradient' => 'var(--custom)', + 'text' => 'aliceblue', + ), + 'typography' => array( + 'fontSize' => '1.5em', + 'lineHeight' => '1.4em', + ), + ), + ), + ), + 'core/heading' => array( + 'variations' => array( + 'block-style-variation-b' => array( + 'typography' => array( + 'fontSize' => '3em', + ), + ), + ), + ), + ); + + unregister_block_style( 'core/heading', 'block-style-variation-b' ); + unregister_block_style( 'core/media-text', 'block-style-variation-a' ); + + $this->assertSameSetsWithIndex( $expected, $actual, 'Merged variation styles do not match.' ); + } } diff --git a/tests/phpunit/tests/theme/wpThemeJsonSchema.php b/tests/phpunit/tests/theme/wpThemeJsonSchema.php new file mode 100644 index 0000000000000..aa6f78eb03439 --- /dev/null +++ b/tests/phpunit/tests/theme/wpThemeJsonSchema.php @@ -0,0 +1,289 @@ + 1, + 'settings' => array( + 'color' => array( + 'palette' => array( + array( + 'name' => 'Pale Pink', + 'slug' => 'pale-pink', + 'color' => '#f78da7', + ), + array( + 'name' => 'Vivid Red', + 'slug' => 'vivid-red', + 'color' => '#cf2e2e', + ), + ), + 'custom' => false, + 'link' => true, + ), + 'border' => array( + 'color' => false, + 'customRadius' => false, + 'style' => false, + 'width' => false, + ), + 'typography' => array( + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + 'fontStyle' => false, + 'fontWeight' => false, + 'letterSpacing' => false, + 'textDecoration' => false, + 'textTransform' => false, + ), + 'blocks' => array( + 'core/group' => array( + 'border' => array( + 'color' => true, + 'customRadius' => true, + 'style' => true, + 'width' => true, + ), + 'typography' => array( + 'fontStyle' => true, + 'fontWeight' => true, + 'letterSpacing' => true, + 'textDecoration' => true, + 'textTransform' => true, + ), + ), + ), + ), + 'styles' => array( + 'color' => array( + 'background' => 'purple', + ), + 'blocks' => array( + 'core/group' => array( + 'color' => array( + 'background' => 'red', + ), + 'spacing' => array( + 'padding' => array( + 'top' => '10px', + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ), + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Schema::migrate( $theme_json_v1 ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'color' => array( + 'palette' => array( + array( + 'name' => 'Pale Pink', + 'slug' => 'pale-pink', + 'color' => '#f78da7', + ), + array( + 'name' => 'Vivid Red', + 'slug' => 'vivid-red', + 'color' => '#cf2e2e', + ), + ), + 'custom' => false, + 'link' => true, + ), + 'border' => array( + 'color' => false, + 'radius' => false, + 'style' => false, + 'width' => false, + ), + 'typography' => array( + 'defaultFontSizes' => false, + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + 'fontStyle' => false, + 'fontWeight' => false, + 'letterSpacing' => false, + 'textDecoration' => false, + 'textTransform' => false, + ), + 'blocks' => array( + 'core/group' => array( + 'border' => array( + 'color' => true, + 'radius' => true, + 'style' => true, + 'width' => true, + ), + 'typography' => array( + 'fontStyle' => true, + 'fontWeight' => true, + 'letterSpacing' => true, + 'textDecoration' => true, + 'textTransform' => true, + ), + ), + ), + ), + 'styles' => array( + 'color' => array( + 'background' => 'purple', + ), + 'blocks' => array( + 'core/group' => array( + 'color' => array( + 'background' => 'red', + ), + 'spacing' => array( + 'padding' => array( + 'top' => '10px', + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'yellow', + ), + ), + ), + ), + ), + 'elements' => array( + 'link' => array( + 'color' => array( + 'text' => 'red', + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } + + public function test_migrate_v2_to_latest() { + $theme_json_v2 = array( + 'version' => 2, + 'settings' => array( + 'typography' => array( + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + ), + 'spacing' => array( + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), + ), + ); + + $actual = WP_Theme_JSON_Schema::migrate( $theme_json_v2 ); + + $expected = array( + 'version' => WP_Theme_JSON::LATEST_SCHEMA, + 'settings' => array( + 'typography' => array( + 'defaultFontSizes' => false, + 'fontSizes' => array( + array( + 'name' => 'Small', + 'slug' => 'small', + 'size' => 12, + ), + array( + 'name' => 'Normal', + 'slug' => 'normal', + 'size' => 16, + ), + ), + ), + 'spacing' => array( + 'defaultSpacingSizes' => false, + 'spacingSizes' => array( + array( + 'name' => 'Small', + 'slug' => 20, + 'size' => '20px', + ), + array( + 'name' => 'Large', + 'slug' => 80, + 'size' => '80px', + ), + ), + ), + ), + ); + + $this->assertEqualSetsWithIndex( $expected, $actual ); + } +} diff --git a/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php b/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php new file mode 100644 index 0000000000000..f477683eafd06 --- /dev/null +++ b/tests/phpunit/tests/unicode/wpCheckInvalidUtf8.php @@ -0,0 +1,110 @@ +assertSame( + $bytes, + wp_check_invalid_utf8( $bytes ), + 'Should have returned the unchanged string for valid UTF-8 input when not stripping invalid bytes.' + ); + + $this->assertSame( + $bytes, + wp_check_invalid_utf8( $bytes, true ), + 'Should have returned the unchanged string for valid UTF-8 input when stripping invalid bytes.' + ); + } else { + $this->assertSame( + '', + wp_check_invalid_utf8( $bytes ), + 'Should have rejected invalid input, returning an empty string when not stripping invalid bytes.' + ); + + $this->assertSame( + $scrubbed, + wp_check_invalid_utf8( $bytes, true ), + 'Failed to properly scrub the invalid spans of UTF-8 from the input string.' + ); + } + } + + /** + * Data provider. + * + * @throws Exception + * + * @return Generator + */ + public static function data_utf8_test_data() { + $test_file = fopen( __DIR__ . '/../../data/unicode/utf8tests/utf8tests.txt', 'r' ); + $line_number = 0; + $last_description = ''; + + while ( false !== ( $line = fgets( $test_file ) ) ) { + ++$line_number; + + if ( empty( trim( $line ) ) ) { + continue; + } + + if ( str_starts_with( $line, '#' ) ) { + $last_description = trim( substr( $line, 1 ) ); + continue; + } + + $test_parts = explode( ':', $line ); + if ( count( $test_parts ) < 3 ) { + throw new Exception( 'Wrong test data: check utf8tests.txt' ); + } + + list( $reference, $classification, $test_data ) = $test_parts; + + $reference = trim( $reference ); + $classification = trim( $classification ); + $test_data = trim( $test_data ); + + switch ( $classification ) { + case 'valid': + yield "{$reference} {$last_description}" => array( $test_data, null ); + break; + + case 'valid hex': + case 'invalid hex': + if ( 'invalid hex' === $classification && count( $test_parts ) < 5 ) { + throw new Exception( "Test data missing expected “scrubbed” value: check utf8tests.txt:{$line_number}" ); + } + + $bytes = hex2bin( str_replace( ' ', '', $test_data ) ); + $scrubbed = 'invalid hex' === $classification + ? hex2bin( str_replace( ' ', '', trim( $test_parts[4] ) ) ) + : null; + + yield "{$reference} {$last_description}" => array( $bytes, $scrubbed ); + break; + + default: + throw new Exception( "Test input file contains unrecognized input classification '{$classification}' (see utf8tests.txt): {$line}" ); + } + } + } +} diff --git a/tests/phpunit/tests/unicode/wpHasNoncharacters.php b/tests/phpunit/tests/unicode/wpHasNoncharacters.php new file mode 100644 index 0000000000000..880f89c4f8e45 --- /dev/null +++ b/tests/phpunit/tests/unicode/wpHasNoncharacters.php @@ -0,0 +1,190 @@ +assertTrue( + wp_has_noncharacters( $noncharacter ), + 'Failed to detect entire string as noncharacter.' + ); + + $this->assertTrue( + wp_has_noncharacters( "{$noncharacter} and more." ), + 'Failed to detect noncharacter prefix.' + ); + + $this->assertTrue( + wp_has_noncharacters( "Some text and then a {$noncharacter} and more." ), + 'Failed to detect medial noncharacter.' + ); + + $this->assertTrue( + wp_has_noncharacters( "Some text and a {$noncharacter}." ), + 'Failed to detect noncharacter suffix.' + ); + } + + /** + * Ensures that a noncharacter inside a string will be properly detected + * using the fallback function when Unicode PCRE support is missing. + * + * @ticket 63863 + * + * @dataProvider data_noncharacters + * + * @param string $noncharacter Noncharacter as a UTF-8 string. + */ + public function test_fallback_detects_non_characters( string $noncharacter ) { + $this->assertTrue( + _wp_has_noncharacters_fallback( $noncharacter ), + 'Failed to detect entire string as noncharacter.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "{$noncharacter} and more." ), + 'Failed to detect noncharacter prefix.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "Some text and then a {$noncharacter} and more." ), + 'Failed to detect medial noncharacter.' + ); + + $this->assertTrue( + _wp_has_noncharacters_fallback( "Some text and a {$noncharacter}." ), + 'Failed to detect noncharacter suffix.' + ); + } + + /** + * Ensures that Unicode characters are not falsely detect as noncharacters. + * + * @ticket 63863 + */ + public function test_avoids_false_positives() { + // Get all the noncharacters in one long string, each surrounded on both sides by null bytes. + $noncharacters = implode( + "\x00", + array_map( + static function ( $c ) { + return "\x00{$c}"; + }, + array_column( array_values( iterator_to_array( self::data_noncharacters() ) ), 0 ) + ) + ) . "\x00"; + + $this->assertFalse( + wp_has_noncharacters( "\x00" ), + 'Falsely detected noncharacter in U+0000' + ); + + for ( $code_point = 1; $code_point <= 0x10FFFF; $code_point++ ) { + // Surrogate halves are invalid UTF-8. + if ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) { + continue; + } + + $char = mb_chr( $code_point ); + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + + if ( str_contains( $noncharacters, $char ) ) { + $this->assertTrue( + wp_has_noncharacters( $char ), + "Failed to detect noncharacter as test verification for U+{$hex_char}" + ); + } else { + $this->assertFalse( + wp_has_noncharacters( $char ), + "Falsely detected noncharacter in U+{$hex_char}." + ); + } + } + } + + /** + * Ensures that Unicode characters are not falsely detect as noncharacters + * using the fallback function when Unicode PCRE support is missing. + * + * @ticket 63863 + */ + public function test_fallback_avoids_false_positives() { + // Get all the noncharacters in one long string, each surrounded on both sides by null bytes. + $noncharacters = implode( + "\x00", + array_map( + static function ( $c ) { + return "\x00{$c}"; + }, + array_column( array_values( iterator_to_array( self::data_noncharacters() ) ), 0 ) + ) + ) . "\x00"; + + $this->assertFalse( + _wp_has_noncharacters_fallback( "\x00" ), + 'Falsely detected noncharacter in U+0000' + ); + + for ( $code_point = 1; $code_point <= 0x10FFFF; $code_point++ ) { + // Surrogate halves are invalid UTF-8. + if ( $code_point >= 0xD800 && $code_point <= 0xDFFF ) { + continue; + } + + $char = mb_chr( $code_point ); + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + + if ( str_contains( $noncharacters, $char ) ) { + $this->assertTrue( + _wp_has_noncharacters_fallback( $char ), + "Failed to detect noncharacter as test verification for U+{$hex_char}" + ); + } else { + $this->assertFalse( + _wp_has_noncharacters_fallback( $char ), + "Falsely detected noncharacter in U+{$hex_char}." + ); + } + } + } + + /** + * Data provider + * + * @return array[] + */ + public static function data_noncharacters() { + for ( $code_point = 0xFDD0; $code_point <= 0xFDEF; $code_point++ ) { + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + } + + yield 'U+FFFE' => array( "\u{FFFE}" ); + yield 'U+FFFF' => array( "\u{FFFF}" ); + + for ( $plane = 0x10000; $plane <= 0x10FFFF; $plane += 0x10000 ) { + $code_point = $plane + 0xFFFE; + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + + $code_point = $plane + 0xFFFF; + $hex_char = strtoupper( str_pad( dechex( $code_point ), 4, '0', STR_PAD_LEFT ) ); + yield "U+{$hex_char}" => array( mb_chr( $code_point ) ); + } + } +} diff --git a/tests/phpunit/tests/unicode/wpIsValidUtf8.php b/tests/phpunit/tests/unicode/wpIsValidUtf8.php new file mode 100644 index 0000000000000..386ff8cf2d6ee --- /dev/null +++ b/tests/phpunit/tests/unicode/wpIsValidUtf8.php @@ -0,0 +1,104 @@ +assertSame( + $is_valid, + wp_is_valid_utf8( $bytes ), + $is_valid + ? 'Should have identified the input as a valid UTF-8 string.' + : 'Should have reject the invalid UTF-8 string.' + ); + } + + /** + * Verifies that WordPress can properly detect valid and invalid UTF-8; + * forces testing with the fallback mechanism in pure PHP code. + * + * @ticket 38044 + * + * @dataProvider data_utf8_test_data + * + * @param string $bytes Bytes as a PHP string. + */ + public function test_fallback_properly_validates_utf8( string $bytes ) { + $is_valid = mb_check_encoding( $bytes, 'UTF-8' ); + + $this->assertSame( + $is_valid, + _wp_is_valid_utf8_fallback( $bytes ), + $is_valid + ? 'Should have identified the input as a valid UTF-8 string.' + : 'Should have reject the invalid UTF-8 string.' + ); + } + + /** + * Data provider. + * + * @throws Exception + * + * @return Generator + */ + public static function data_utf8_test_data() { + $test_file = fopen( __DIR__ . '/../../data/unicode/utf8tests/utf8tests.txt', 'r' ); + $last_description = ''; + + while ( false !== ( $line = fgets( $test_file ) ) ) { + if ( empty( trim( $line ) ) ) { + continue; + } + + if ( str_starts_with( $line, '#' ) ) { + $last_description = trim( substr( $line, 1 ) ); + continue; + } + + $test_parts = explode( ':', $line ); + if ( count( $test_parts ) < 3 ) { + throw new Exception( 'Wrong test data: check utf8tests.txt' ); + } + + list( $reference, $classification, $test_data ) = $test_parts; + + $reference = trim( $reference ); + $classification = trim( $classification ); + $test_data = trim( $test_data ); + + switch ( $classification ) { + case 'valid': + yield "{$reference} {$last_description}" => array( $test_data ); + break; + + case 'valid hex': + case 'invalid hex': + $bytes = hex2bin( str_replace( ' ', '', $test_data ) ); + yield "{$reference} {$last_description}" => array( $bytes ); + break; + + default: + throw new Exception( "Test input file contains unrecognized input classification '{$classification}' (see utf8tests.txt): {$line}" ); + } + } + } +} diff --git a/tests/phpunit/tests/unicode/wpScrubUtf8.php b/tests/phpunit/tests/unicode/wpScrubUtf8.php new file mode 100644 index 0000000000000..cd4f003bd6e4c --- /dev/null +++ b/tests/phpunit/tests/unicode/wpScrubUtf8.php @@ -0,0 +1,125 @@ +assertSame( + $bytes, + wp_scrub_utf8( $bytes ), + 'Should have returned the unchanged string for valid UTF-8 input.' + ); + } else { + $this->assertSame( + bin2hex( $scrubbed ), + bin2hex( wp_scrub_utf8( $bytes ) ), + 'Failed to properly scrub the invalid spans of UTF-8 from the input string.' + ); + } + } + + /** + * Verifies that WordPress’ fallback code can properly detect valid UTF-8 + * while replacing invalid byte sequences. + * + * @ticket 63837 + * + * @dataProvider data_utf8_test_data + * + * @param string $bytes Bytes as a PHP string. + * @param string|null $scrubbed Expected checked value, if string isn’t valid UTF-8. + */ + public function test_fallback_properly_scrubs_utf8( string $bytes, ?string $scrubbed = null ) { + if ( null === $scrubbed ) { + $this->assertSame( + $bytes, + _wp_scrub_utf8_fallback( $bytes ), + 'Should have returned the unchanged string for valid UTF-8 input.' + ); + } else { + $this->assertSame( + bin2hex( $scrubbed ), + bin2hex( _wp_scrub_utf8_fallback( $bytes ) ), + 'Failed to properly scrub the invalid spans of UTF-8 from the input string.' + ); + } + } + + /** + * Data provider. + * + * @throws Exception + * + * @return Generator + */ + public static function data_utf8_test_data() { + $test_file = fopen( __DIR__ . '/../../data/unicode/utf8tests/utf8tests.txt', 'r' ); + $line_number = 0; + $last_description = ''; + + while ( false !== ( $line = fgets( $test_file ) ) ) { + ++$line_number; + + if ( empty( trim( $line ) ) ) { + continue; + } + + if ( str_starts_with( $line, '#' ) ) { + $last_description = trim( substr( $line, 1 ) ); + continue; + } + + $test_parts = explode( ':', $line ); + if ( count( $test_parts ) < 3 ) { + throw new Exception( 'Wrong test data: check utf8tests.txt' ); + } + + list( $reference, $classification, $test_data ) = $test_parts; + + $reference = trim( $reference ); + $classification = trim( $classification ); + $test_data = trim( $test_data ); + + switch ( $classification ) { + case 'valid': + yield "{$reference} {$last_description}" => array( $test_data, null ); + break; + + case 'valid hex': + case 'invalid hex': + if ( 'invalid hex' === $classification && count( $test_parts ) < 5 ) { + throw new Exception( "Test data missing expected “scrubbed” value: check utf8tests.txt:{$line_number}" ); + } + + $bytes = hex2bin( str_replace( ' ', '', $test_data ) ); + $scrubbed = 'invalid hex' === $classification + ? hex2bin( str_replace( ' ', '', trim( $test_parts[4] ) ) ) + : null; + + yield "{$reference} {$last_description}" => array( $bytes, $scrubbed ); + break; + + default: + throw new Exception( "Test input file contains unrecognized input classification '{$classification}' (see utf8tests.txt): {$line}" ); + } + } + } +} diff --git a/tests/phpunit/tests/upload.php b/tests/phpunit/tests/upload.php index 4640439313db1..46fcea7099097 100644 --- a/tests/phpunit/tests/upload.php +++ b/tests/phpunit/tests/upload.php @@ -105,5 +105,4 @@ public function test_upload_dir_empty() { $this->assertSame( $subdir, $info['subdir'] ); $this->assertFalse( $info['error'] ); } - } diff --git a/tests/phpunit/tests/url.php b/tests/phpunit/tests/url.php index ff360fdea460e..3734891eb19ac 100644 --- a/tests/phpunit/tests/url.php +++ b/tests/phpunit/tests/url.php @@ -7,6 +7,17 @@ */ class Tests_URL extends WP_UnitTestCase { + /** + * Author user ID. + * + * @var int $author_id + */ + public static $author_id; + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$author_id = $factory->user->create( array( 'role' => 'author' ) ); + } + public function set_up() { parent::set_up(); $GLOBALS['pagenow'] = ''; @@ -14,6 +25,8 @@ public function set_up() { /** * @dataProvider data_is_ssl + * + * @covers ::is_ssl */ public function test_is_ssl( $value, $expected ) { $_SERVER['HTTPS'] = $value; @@ -47,6 +60,9 @@ public function data_is_ssl() { ); } + /** + * @covers ::is_ssl + */ public function test_is_ssl_by_port() { unset( $_SERVER['HTTPS'] ); $_SERVER['SERVER_PORT'] = '443'; @@ -55,6 +71,9 @@ public function test_is_ssl_by_port() { $this->assertTrue( $is_ssl ); } + /** + * @covers ::is_ssl + */ public function test_is_ssl_with_no_value() { unset( $_SERVER['HTTPS'] ); @@ -67,6 +86,8 @@ public function test_is_ssl_with_no_value() { * * @param string $url Test URL. * @param string $expected Expected result. + * + * @covers ::admin_url */ public function test_admin_url( $url, $expected ) { $siteurl_http = get_option( 'siteurl' ); @@ -135,6 +156,8 @@ public function data_admin_urls() { * * @param string $url Test URL. * @param string $expected Expected result. + * + * @covers ::home_url */ public function test_home_url( $url, $expected ) { $homeurl_http = get_option( 'home' ); @@ -198,6 +221,9 @@ public function data_home_urls() { ); } + /** + * @covers ::home_url + */ public function test_home_url_from_admin() { // Pretend to be in the site admin. set_current_screen( 'dashboard' ); @@ -243,6 +269,9 @@ public function test_home_url_from_admin() { update_option( 'home', set_url_scheme( $home, 'http' ) ); } + /** + * @covers ::network_home_url + */ public function test_network_home_url_from_admin() { // Pretend to be in the site admin. set_current_screen( 'dashboard' ); @@ -264,6 +293,9 @@ public function test_network_home_url_from_admin() { $this->assertSame( $home_https, network_home_url() ); } + /** + * @covers ::set_url_scheme + */ public function test_set_url_scheme() { $links = array( 'http://wordpress.org/', @@ -318,12 +350,15 @@ public function test_set_url_scheme() { $this->assertSame( $http_links[ $i ], set_url_scheme( $link, 'login' ) ); $this->assertSame( $http_links[ $i ], set_url_scheme( $link, 'rpc' ) ); - $i++; + ++$i; } force_ssl_admin( $forced_admin ); } + /** + * @covers ::get_adjacent_post + */ public function test_get_adjacent_post() { $now = time(); $post_id = self::factory()->post->create( array( 'post_date' => gmdate( 'Y-m-d H:i:s', $now - 1 ) ) ); @@ -358,9 +393,11 @@ public function test_get_adjacent_post() { * Test get_adjacent_post returns the next private post when the author is the currently logged in user. * * @ticket 30287 + * + * @covers ::get_adjacent_post */ public function test_get_adjacent_post_should_return_private_posts_belonging_to_the_current_user() { - $u = self::factory()->user->create( array( 'role' => 'author' ) ); + $u = self::$author_id; $old_uid = get_current_user_id(); wp_set_current_user( $u ); @@ -395,9 +432,11 @@ public function test_get_adjacent_post_should_return_private_posts_belonging_to_ /** * @ticket 30287 + * + * @covers ::get_adjacent_post */ public function test_get_adjacent_post_should_return_private_posts_belonging_to_other_users_if_the_current_user_can_read_private_posts() { - $u1 = self::factory()->user->create( array( 'role' => 'author' ) ); + $u1 = self::$author_id; $u2 = self::factory()->user->create( array( 'role' => 'administrator' ) ); $old_uid = get_current_user_id(); wp_set_current_user( $u2 ); @@ -433,9 +472,11 @@ public function test_get_adjacent_post_should_return_private_posts_belonging_to_ /** * @ticket 30287 + * + * @covers ::get_adjacent_post */ public function test_get_adjacent_post_should_not_return_private_posts_belonging_to_other_users_if_the_current_user_cannot_read_private_posts() { - $u1 = self::factory()->user->create( array( 'role' => 'author' ) ); + $u1 = self::$author_id; $u2 = self::factory()->user->create( array( 'role' => 'author' ) ); $old_uid = get_current_user_id(); wp_set_current_user( $u2 ); @@ -479,6 +520,17 @@ public function test_get_adjacent_post_should_not_return_private_posts_belonging * Test that *_url functions handle paths with ".." * * @ticket 19032 + * + * @covers ::site_url + * @covers ::home_url + * @covers ::admin_url + * @covers ::network_admin_url + * @covers ::user_admin_url + * @covers ::includes_url + * @covers ::network_site_url + * @covers ::network_home_url + * @covers ::content_url + * @covers ::plugins_url */ public function test_url_functions_for_dots_in_paths() { $functions = array( @@ -517,4 +569,62 @@ public function test_url_functions_for_dots_in_paths() { ); } } + + /** + * Test get_adjacent_post with posts having identical post_date. + * + * @ticket 8107 + * @covers ::get_adjacent_post + */ + public function test_get_adjacent_post_with_identical_dates() { + $identical_date = gmdate( 'Y-m-d H:i:s', time() ); + + // Create 3 posts with identical dates but different IDs. + $post_ids = array(); + for ( $i = 1; $i <= 3; $i++ ) { + $post_ids[] = self::factory()->post->create( + array( + 'post_title' => "Identical Post $i", + 'post_date' => $identical_date, + ) + ); + } + + // Test from the middle post (2nd post). + $GLOBALS['post'] = get_post( $post_ids[1] ); + + // Previous post should be the 1st post (lower ID, same date). + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertSame( $post_ids[0], $previous->ID ); + + // Next post should be the 3rd post (higher ID, same date). + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertSame( $post_ids[2], $next->ID ); + + // Test from the first post. + $GLOBALS['post'] = get_post( $post_ids[0] ); + + // Previous should be empty (no earlier posts). + $previous = get_adjacent_post( false, '', true ); + $this->assertSame( '', $previous ); + + // Next should be the 2nd post. + $next = get_adjacent_post( false, '', false ); + $this->assertInstanceOf( 'WP_Post', $next ); + $this->assertSame( $post_ids[1], $next->ID ); + + // Test from the last post. + $GLOBALS['post'] = get_post( $post_ids[2] ); + + // Previous should be the 2nd post. + $previous = get_adjacent_post( false, '', true ); + $this->assertInstanceOf( 'WP_Post', $previous ); + $this->assertSame( $post_ids[1], $previous->ID ); + + // Next should be empty (no later posts). + $next = get_adjacent_post( false, '', false ); + $this->assertSame( '', $next ); + } } diff --git a/tests/phpunit/tests/url/getPrivacyPolicyUrl.php b/tests/phpunit/tests/url/getPrivacyPolicyUrl.php index 7c06cf4fda3f0..0aab744a2c49f 100644 --- a/tests/phpunit/tests/url/getPrivacyPolicyUrl.php +++ b/tests/phpunit/tests/url/getPrivacyPolicyUrl.php @@ -1,20 +1,15 @@ assertSame( $privacy_policy_url, get_privacy_policy_url() ); } - /** - * The function should return an empty string when `wp_page_for_privacy_policy` is _not_ set. - */ - public function test_get_privacy_policy_url_should_return_empty_when_privacy_policy_page_not_set() { - $this->assertSame( '', get_privacy_policy_url() ); - } - /** * The function should return an empty string for an invalid `wp_page_for_privacy_policy` value. */ diff --git a/tests/phpunit/tests/user.php b/tests/phpunit/tests/user.php index caa9a5e24d398..3770816ec4502 100644 --- a/tests/phpunit/tests/user.php +++ b/tests/phpunit/tests/user.php @@ -49,7 +49,7 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$user_ids[] = self::$admin_id; self::$editor_id = $factory->user->create( array( - 'user_email' => 'test@test.com', + 'user_email' => 'test@example.com', 'role' => 'editor', ) ); @@ -63,9 +63,37 @@ public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { public function set_up() { parent::set_up(); + add_action( 'set_auth_cookie', array( $this, 'action_set_auth_cookie' ), 10, 6 ); + add_action( 'set_logged_in_cookie', array( $this, 'action_set_logged_in_cookie' ), 10 ); + add_action( 'clear_auth_cookie', array( $this, 'action_clear_auth_cookie' ) ); + + $_COOKIE = array(); + $this->author = clone self::$_author; } + final public function action_set_auth_cookie( + string $cookie, + int $expire, + int $expiration, + int $user_id, + string $scheme, + string $token + ): void { + $_COOKIE[ SECURE_AUTH_COOKIE ] = $cookie; + $_COOKIE[ AUTH_COOKIE ] = $cookie; + } + + final public function action_set_logged_in_cookie( string $cookie ): void { + $_COOKIE[ LOGGED_IN_COOKIE ] = $cookie; + } + + final public function action_clear_auth_cookie(): void { + unset( $_COOKIE[ LOGGED_IN_COOKIE ] ); + unset( $_COOKIE[ SECURE_AUTH_COOKIE ] ); + unset( $_COOKIE[ AUTH_COOKIE ] ); + } + public function test_get_users_of_blog() { // Add one of each user role. $nusers = array( @@ -104,7 +132,7 @@ public function test_user_option() { $this->assertSame( $val, get_user_option( $key, self::$author_id ) ); // Change and get again. - $val2 = rand_str(); + $val2 = 'baz'; update_user_option( self::$author_id, $key, $val2 ); $this->assertSame( $val2, get_user_option( $key, self::$author_id ) ); } @@ -135,12 +163,11 @@ public function test_usermeta() { // Delete by key AND value. update_user_meta( self::$author_id, $key, $val ); // Incorrect key: key still exists. - delete_user_meta( self::$author_id, $key, rand_str() ); + delete_user_meta( self::$author_id, $key, 'foo' ); $this->assertSame( $val, get_user_meta( self::$author_id, $key, true ) ); // Correct key: deleted. delete_user_meta( self::$author_id, $key, $val ); $this->assertSame( '', get_user_meta( self::$author_id, $key, true ) ); - } /** @@ -149,9 +176,9 @@ public function test_usermeta() { public function test_usermeta_array() { // Some values to set. $vals = array( - rand_str() => 'val-' . rand_str(), - rand_str() => 'val-' . rand_str(), - rand_str() => 'val-' . rand_str(), + 'key0' => 'val0', + 'key1' => 'val1', + 'key2' => 'val2', ); // There is already some stuff in the array. @@ -475,8 +502,8 @@ public function test_global_authordata() { $post = array( 'post_author' => self::$author_id, 'post_status' => 'publish', - 'post_content' => rand_str(), - 'post_title' => rand_str(), + 'post_content' => 'content', + 'post_title' => 'title', 'post_type' => 'post', ); @@ -546,6 +573,8 @@ public function test_user_get_data_by_ID_should_alias_to_id() { /** * @ticket 21431 + * + * @covers ::count_many_users_posts */ public function test_count_many_users_posts() { $user_id_b = self::factory()->user->create( array( 'role' => 'author' ) ); @@ -560,21 +589,245 @@ public function test_count_many_users_posts() { wp_set_current_user( self::$author_id ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false ); - $this->assertEquals( 1, $counts[ self::$author_id ] ); - $this->assertEquals( 1, $counts[ $user_id_b ] ); + $this->assertSame( '1', $counts[ self::$author_id ] ); + $this->assertSame( '1', $counts[ $user_id_b ] ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true ); - $this->assertEquals( 1, $counts[ self::$author_id ] ); - $this->assertEquals( 1, $counts[ $user_id_b ] ); + $this->assertSame( '1', $counts[ self::$author_id ] ); + $this->assertSame( '1', $counts[ $user_id_b ] ); wp_set_current_user( $user_id_b ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', false ); - $this->assertEquals( 1, $counts[ self::$author_id ] ); - $this->assertEquals( 2, $counts[ $user_id_b ] ); + $this->assertSame( '1', $counts[ self::$author_id ] ); + $this->assertSame( '2', $counts[ $user_id_b ] ); $counts = count_many_users_posts( array( self::$author_id, $user_id_b ), 'post', true ); - $this->assertEquals( 1, $counts[ self::$author_id ] ); - $this->assertEquals( 1, $counts[ $user_id_b ] ); + $this->assertSame( '1', $counts[ self::$author_id ] ); + $this->assertSame( '1', $counts[ $user_id_b ] ); + } + + /** + * Ensure the second and subsequent calls to count_many_users_posts() are cached. + * + * @ticket 63045 + * + * @covers ::count_many_users_posts + */ + public function test_count_many_users_posts_is_cached() { + $user_1 = self::$user_ids[0]; + $user_2 = self::$user_ids[1]; + + // Create posts for both users. + self::factory()->post->create( array( 'post_author' => $user_1 ) ); + self::factory()->post->create( array( 'post_author' => $user_2 ) ); + + // Warm the cache. + $count1 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false ); + + // Ensure cache is hit for second call. + $start_queries = get_num_queries(); + $count2 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false ); + $end_queries = get_num_queries(); + $this->assertSame( 0, $end_queries - $start_queries, 'No database queries expected for second call to count_many_users_posts()' ); + $this->assertSameSetsWithIndex( $count1, $count2, 'Expected same results from both calls to count_many_users_posts()' ); + } + + /** + * Ensure equivalent arguments hit the same cache in count_many_users_posts(). + * + * @ticket 63045 + * + * @covers ::count_many_users_posts + * + * @dataProvider data_count_many_users_posts_cached_for_equivalent_arguments + * + * @param array $first_args First set of arguments to pass to count_many_users_posts(). + * @param array $second_args Second set of arguments to pass to count_many_users_posts(). + */ + public function test_count_many_users_posts_cached_for_equivalent_arguments( $first_args, $second_args ) { + // Replace placeholder user IDs with real ones. + $first_args[0] = array_map( + static function ( $user ) { + return self::$user_ids[ $user ]; + }, + $first_args[0] + ); + $second_args[0] = array_map( + static function ( $user ) { + return self::$user_ids[ $user ]; + }, + $second_args[0] + ); + + // Warm the cache with the first set of arguments. + $count1 = count_many_users_posts( ...$first_args ); + + // Ensure the cache is hit for the second set of equivalent arguments. + $start_queries = get_num_queries(); + $count2 = count_many_users_posts( ...$second_args ); + $end_queries = get_num_queries(); + $this->assertSame( 0, $end_queries - $start_queries, 'No database queries expected for second call to count_many_users_posts() with equivalent arguments' ); + $this->assertSameSetsWithIndex( $count1, $count2, 'Expected same results from both calls to count_many_users_posts()' ); + } + + /** + * Data provider for test_count_many_users_posts_cached_for_equivalent_arguments(). + * + * @return array[] Data provider. + */ + public function data_count_many_users_posts_cached_for_equivalent_arguments(): array { + return array( + 'single post string vs array' => array( + array( array( 0 ), 'post' ), + array( array( 0 ), array( 'post' ) ), + ), + 'duplicate post type in array' => array( + array( array( 0 ), array( 'post', 'post' ) ), + array( array( 0 ), array( 'post' ) ), + ), + 'different post type order' => array( + array( array( 0 ), array( 'post', 'page' ) ), + array( array( 0 ), array( 'page', 'post' ) ), + ), + 'duplicate user IDs in array' => array( + array( array( 0, 1, 1 ), 'post' ), + array( array( 0, 1 ), 'post' ), + ), + 'different user order' => array( + array( array( 0, 1 ), 'post' ), + array( array( 1, 0 ), 'post' ), + ), + 'integer vs string user IDs' => array( + array( array( 0, 1 ), 'post' ), + array( array( '0', '1' ), 'post' ), + ), + ); + } + + /** + * Test cache invalidation for count_many_users_posts(). + * + * @ticket 63045 + * + * @covers ::count_many_users_posts + */ + public function test_count_many_users_posts_cache_invalidation() { + $user_1 = self::$user_ids[0]; + $user_2 = self::$user_ids[1]; + + // Create posts for both users. + self::factory()->post->create( array( 'post_author' => $user_1 ) ); + self::factory()->post->create( array( 'post_author' => $user_2 ) ); + + $counts1 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false ); + $this->assertSame( + array( + $user_1 => '1', + $user_2 => '1', + ), + $counts1, + 'Initial call is expected to have one post for each user.' + ); + + // Create another post for user 1. + self::factory()->post->create( array( 'post_author' => $user_1 ) ); + + $counts2 = count_many_users_posts( array( $user_1, $user_2 ), 'post', false ); + $this->assertSame( + array( + $user_1 => '2', + $user_2 => '1', + ), + $counts2, + 'Second call is expected to have two posts for user 1 and one post for user 2.' + ); + } + + /** + * Ensure different post types use different caches in count_many_users_posts(). + * + * @ticket 63045 + * + * @covers ::count_many_users_posts + */ + public function test_different_post_types_use_different_caches() { + $user_id = self::$user_ids[0]; + + // Create one post and two pages for the user. + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'post', + ) + ); + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'page', + ) + ); + self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_type' => 'page', + ) + ); + + $start_queries = get_num_queries(); + $count1 = count_many_users_posts( array( $user_id ), 'post', false ); + $end_queries = get_num_queries(); + $this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with post type "post".' ); + $this->assertSame( '1', $count1[ $user_id ], 'Expected to have one post for user with post type "post".' ); + + $start_queries = get_num_queries(); + $count2 = count_many_users_posts( array( $user_id ), 'page', false ); + $end_queries = get_num_queries(); + $this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with post type "page".' ); + $this->assertSame( '2', $count2[ $user_id ], 'Expected to have two pages for user with post type "page".' ); + } + + /** + * Ensure different users use different caches in count_many_users_posts(). + * + * @ticket 63045 + * + * @covers ::count_many_users_posts + */ + public function test_different_users_use_different_caches() { + $user_1 = self::$user_ids[0]; + $user_2 = self::$user_ids[1]; + + // Create one post for user 1, two for user 2. + self::factory()->post->create( + array( + 'post_author' => $user_1, + 'post_type' => 'post', + ) + ); + self::factory()->post->create( + array( + 'post_author' => $user_2, + 'post_type' => 'post', + ) + ); + self::factory()->post->create( + array( + 'post_author' => $user_2, + 'post_type' => 'post', + ) + ); + + $start_queries = get_num_queries(); + $count1 = count_many_users_posts( array( $user_1 ), 'post', false ); + $end_queries = get_num_queries(); + $this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with user 1.' ); + $this->assertSame( '1', $count1[ $user_1 ], 'Expected to have one post for user 1 with post type "post".' ); + + $start_queries = get_num_queries(); + $count2 = count_many_users_posts( array( $user_2 ), 'post', false ); + $end_queries = get_num_queries(); + $this->assertSame( 1, $end_queries - $start_queries, 'Expected to hit database for first call to count_many_users_posts() with user 2.' ); + $this->assertSame( '2', $count2[ $user_2 ], 'Expected to have two posts for user 2 with post type "post".' ); } /** @@ -630,13 +883,73 @@ public function test_wp_update_user_should_not_mark_user_as_spam_on_single_site( $this->assertSame( 'no_spam', $u->get_error_code() ); } + /** + * Helper to create a user and add them to multiple blogs. + * + * @param int $num_blogs Number of additional blogs to create and add the user to. + * @param bool $include_main_site Whether to add the user to the main site as well. + * @return array Array with 'user_id' and 'blogs' (array of blog IDs). + */ + private function create_user_with_blogs( $num_blogs = 1, $include_main_site = false ) { + $user_id = self::factory()->user->create(); + + $blogs = array(); + if ( $include_main_site ) { + add_user_to_blog( get_main_site_id(), $user_id, 'administrator' ); + $blogs[] = get_main_site_id(); + } + + for ( $i = 0; $i < $num_blogs; $i++ ) { + $blog_id = self::factory()->blog->create( + array( + 'site_id' => get_current_network_id(), + ) + ); + add_user_to_blog( $blog_id, $user_id, 'administrator' ); + $blogs[] = $blog_id; + } + + return array( + 'user_id' => $user_id, + 'blogs' => $blogs, + ); + } + + /** + * @ticket 61146 + */ + public function test_default_do_not_propagate_network_user_spam_to_blogs_on_multisite() { + if ( ! is_multisite() ) { + $this->markTestSkipped( 'This test is for multisite only.' ); + } + + $data = $this->create_user_with_blogs( 2 ); + $user_id = $data['user_id']; + $blogs = $data['blogs']; + + // Mark user spam in user record (this alone should not change blog spam states). + $u = wp_update_user( + array( + 'ID' => $user_id, + 'spam' => '1', + ) + ); + $this->assertNotWPError( $u ); + $user = get_userdata( $user_id ); + $this->assertSame( '1', $user->spam ); + + foreach ( $blogs as $blog_id ) { + $this->assertNotSame( '1', get_blog_status( $blog_id, 'spam' ), "Blog {$blog_id} should not be marked spam by default." ); + } + } + /** * @ticket 28315 */ public function test_user_meta_error() { $id1 = wp_insert_user( array( - 'user_login' => rand_str(), + 'user_login' => 'taco_burrito', 'user_pass' => 'password', 'user_email' => 'taco@burrito.com', ) @@ -645,7 +958,7 @@ public function test_user_meta_error() { $id2 = wp_insert_user( array( - 'user_login' => rand_str(), + 'user_login' => 'taco_burrito2', 'user_pass' => 'password', 'user_email' => 'taco@burrito.com', ) @@ -705,6 +1018,145 @@ public function test_user_update_email_error() { } } + /** + * @ticket 61175 + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_with_null() { + // Note: $this->expectWarning() is deprecated and will be removed in PHPUnit 10. + $warnings = array(); + set_error_handler( + static function ( int $errno, string $errstr ) use ( &$warnings ) { + $warnings[] = compact( 'errno', 'errstr' ); + return true; + }, + E_USER_WARNING + ); + $user = wp_insert_user( null ); + restore_error_handler(); + + $this->assertCount( 1, $warnings, 'Expected one warning.' ); + $this->assertWPError( $user ); + $this->assertSame( 'empty_user_login', $user->get_error_code() ); + } + + /** + * @ticket 61175 + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_with_stdclass() { + $data = array( + 'user_login' => 'new-admin', + 'user_pass' => 'better-password', + ); + $user_id = wp_insert_user( (object) $data ); + $this->assertIsInt( $user_id, 'Expected user to be created.' ); + $user = new WP_User( $user_id ); + $this->assertSame( $data['user_login'], $user->user_login ); + } + + /** + * @ticket 61175 + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_with_wp_user() { + $username = 'new-admin'; + $user = new WP_User(); + $user->user_login = $username; + $user->user_pass = 'better-password'; + + $user_id = wp_insert_user( $user ); + $this->assertIsInt( $user_id, 'Expected user to be created.' ); + $user = new WP_User( $user_id ); + $this->assertSame( $username, $user->user_login ); + } + + /** + * @ticket 61175 + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_with_traversable() { + $internal_data = array( + 'user_login' => 'new-admin', + 'user_pass' => 'better-password', + ); + + $array_access_user = new class( $internal_data ) implements ArrayAccess, IteratorAggregate { + private array $data; + + public function __construct( array $data ) { + $this->data = $data; + } + + public function offsetExists( $offset ): bool { + return isset( $this->data[ $offset ] ); + } + + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + return $this->data[ $offset ]; + } + + public function offsetSet( $offset, $value ): void { + $this->data[ $offset ] = $value; + } + + public function offsetUnset( $offset ): void { + unset( $this->data[ $offset ] ); + } + + public function getIterator(): ArrayIterator { + return new ArrayIterator( $this->data ); + } + }; + + $user_id = wp_insert_user( $array_access_user ); + $this->assertIsInt( $user_id, 'Expected user to be created.' ); + $user = new WP_User( $user_id ); + $this->assertSame( $internal_data['user_login'], $user->user_login ); + } + + /** + * @ticket 61175 + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_with_only_array_access() { + $internal_data = array( + 'user_login' => 'new-admin', + 'user_pass' => 'better-password', + ); + + $array_access_user = new class( $internal_data ) implements ArrayAccess { + private array $data; + + public function __construct( array $data ) { + $this->data = $data; + } + + public function offsetExists( $offset ): bool { + return isset( $this->data[ $offset ] ); + } + + #[\ReturnTypeWillChange] + public function offsetGet( $offset ) { + return $this->data[ $offset ]; + } + + public function offsetSet( $offset, $value ): void { + $this->data[ $offset ] = $value; + } + + public function offsetUnset( $offset ): void { + unset( $this->data[ $offset ] ); + } + }; + + $user_id = wp_insert_user( $array_access_user ); + $this->assertIsInt( $user_id, 'Expected user to be created.' ); + $user = new WP_User( $user_id ); + $this->assertSame( $internal_data['user_login'], $user->user_login ); + } + /** * @ticket 27317 * @dataProvider data_illegal_user_logins @@ -793,7 +1245,7 @@ public function illegal_user_logins() { */ public function test_validate_username_string() { $this->assertTrue( validate_username( 'johndoe' ) ); - $this->assertTrue( validate_username( 'test@test.com' ) ); + $this->assertTrue( validate_username( 'test@example.com' ) ); } /** @@ -826,9 +1278,9 @@ public function test_validate_username_invalid() { */ public function test_wp_insert_user_should_not_wipe_existing_password() { $user_details = array( - 'user_login' => rand_str(), + 'user_login' => 'jonsnow', 'user_pass' => 'password', - 'user_email' => rand_str() . '@example.com', + 'user_email' => 'jonsnow@example.com', ); $user_id = wp_insert_user( $user_details ); @@ -1000,6 +1452,24 @@ public function test_wp_insert_user_should_not_truncate_to_a_duplicate_user_nice $this->assertSame( $expected, $user->user_nicename ); } + /** + * @ticket 44107 + */ + public function test_wp_insert_user_should_reject_user_url_over_100_characters() { + $user_url = str_repeat( 'a', 101 ); + $u = wp_insert_user( + array( + 'user_login' => 'test', + 'user_email' => 'urltest@example.com', + 'user_pass' => 'password', + 'user_url' => $user_url, + ) + ); + + $this->assertWPError( $u ); + $this->assertSame( 'user_url_too_long', $u->get_error_code() ); + } + /** * @ticket 28004 */ @@ -1038,7 +1508,7 @@ public function test_wp_insert_user_with_empty_data() { * @ticket 35750 */ public function test_wp_update_user_should_delete_userslugs_cache() { - $u = self::factory()->user->create(); + $u = self::$sub_id; $user = get_userdata( $u ); wp_update_user( @@ -1105,6 +1575,50 @@ public function test_changing_password_invalidates_password_reset_key() { $this->assertEmpty( $user->user_activation_key ); } + /** + * @ticket 61366 + * @dataProvider data_remember_user + */ + public function test_changing_own_password_retains_current_session( bool $remember ) { + $user = $this->author; + $manager = WP_Session_Tokens::get_instance( $user->ID ); + $expiry = $remember ? ( 2 * WEEK_IN_SECONDS ) : ( 2 * DAY_IN_SECONDS ); + $token = $manager->create( time() + $expiry ); + $pass = $user->user_pass; + + wp_set_current_user( $user->ID ); + wp_set_auth_cookie( $user->ID, $remember, '', $token ); + + $cookie = $_COOKIE[ AUTH_COOKIE ]; + $userdata = array( + 'ID' => $user->ID, + 'user_pass' => 'my_new_password', + ); + $updated = wp_update_user( $userdata, $manager ); + $parsed = wp_parse_auth_cookie(); + + // Check the prerequisites: + $this->assertNotWPError( $updated ); + $this->assertNotSame( $pass, get_userdata( $user->ID )->user_pass ); + + // Check the session token: + $this->assertSame( $token, $parsed['token'] ); + $this->assertCount( 1, $manager->get_all() ); + + // Check that the newly set auth cookie is valid: + $this->assertSame( $user->ID, wp_validate_auth_cookie() ); + + // Check that, despite the session token reuse, the old auth cookie should now be invalid because the password changed: + $this->assertFalse( wp_validate_auth_cookie( $cookie ) ); + } + + public function data_remember_user() { + return array( + array( true ), + array( false ), + ); + } + public function test_search_users_login() { $users = get_users( array( @@ -1167,7 +1681,7 @@ public function test_email_case() { // Alter the case of the email address (which stays the same). $userdata = array( 'ID' => self::$editor_id, - 'user_email' => 'test@TEST.com', + 'user_email' => 'test@EXAMPLE.com', ); $update = wp_update_user( $userdata ); @@ -1181,7 +1695,7 @@ public function test_email_change() { // Change the email address. $userdata = array( 'ID' => self::$editor_id, - 'user_email' => 'test2@test.com', + 'user_email' => 'test2@example.com', ); $update = wp_update_user( $userdata ); @@ -1190,7 +1704,7 @@ public function test_email_change() { // Verify that the email address has been updated. $user = get_userdata( self::$editor_id ); - $this->assertSame( $user->user_email, 'test2@test.com' ); + $this->assertSame( $user->user_email, 'test2@example.com' ); } /** @@ -1325,6 +1839,189 @@ public function test_wp_new_user_notification_old_signature_no_password() { $this->assertFalse( $was_user_email_sent ); } + /** + * Test that admin notification of a new user registration is dependent + * on the 'wp_send_new_user_notification_to_admin' filter. + * + * @dataProvider data_wp_send_new_user_notification_filters + * + * @ticket 54874 + * + * @covers ::wp_new_user_notification + * + * @param bool $expected Whether the email should be sent. + * @param string $callback The callback to pass to the filter. + */ + public function test_wp_send_new_user_notification_to_admin_filter( $expected, $callback ) { + reset_phpmailer_instance(); + + add_filter( 'wp_send_new_user_notification_to_admin', $callback ); + + wp_new_user_notification( self::$contrib_id, null, 'admin' ); + + $mailer = tests_retrieve_phpmailer_instance(); + $recipient = $mailer->get_recipient( 'to' ); + $actual = $recipient ? WP_TESTS_EMAIL === $recipient->address : false; + + $this->assertSame( $expected, $actual, 'Admin email result was not as expected in test_wp_send_new_user_notification_to_admin_filter' ); + } + + /** + * Test that user notification of a new user registration is dependent + * on the 'wp_send_new_user_notification_to_user' filter. + * + * @dataProvider data_wp_send_new_user_notification_filters + * + * @ticket 54874 + * + * @covers ::wp_new_user_notification + * + * @param bool $expected Whether the email should be sent. + * @param string $callback The callback to pass to the filter. + */ + public function test_wp_send_new_user_notification_to_user_filter( $expected, $callback ) { + reset_phpmailer_instance(); + + add_filter( 'wp_send_new_user_notification_to_user', $callback ); + + wp_new_user_notification( self::$contrib_id, null, 'user' ); + + $mailer = tests_retrieve_phpmailer_instance(); + $recipient = $mailer->get_recipient( 'to' ); + $actual = $recipient ? 'blackburn@battlefield3.com' === $recipient->address : false; + + $this->assertSame( $expected, $actual, 'User email result was not as expected in test_wp_send_new_user_notification_to_user_filter' ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_wp_send_new_user_notification_filters() { + return array( + 'true' => array( + 'expected' => true, + 'callback' => '__return_true', + ), + 'false' => array( + 'expected' => false, + 'callback' => '__return_false', + ), + 'null' => array( + 'expected' => false, + 'callback' => '__return_null', + ), + 'empty array' => array( + 'expected' => false, + 'callback' => '__return_empty_array', + ), + 'zero int' => array( + 'expected' => false, + 'callback' => '__return_zero', + ), + 'zero float' => array( + 'expected' => false, + 'callback' => array( $this, 'cb_return_zero_float' ), + ), + 'zero string' => array( + 'expected' => false, + 'callback' => array( $this, 'cb_return_zero_string' ), + ), + 'array( true )' => array( + 'expected' => false, + 'callback' => array( $this, 'cb_return_array_true' ), + ), + ); + } + + /** + * Verifies that the notification email is sent in the correct locale. + * + * @ticket 61518 + */ + public function test_wp_new_user_notification_switches_locale_to_matching_user() { + reset_phpmailer_instance(); + + $admin_user = get_user_by( 'email', get_option( 'admin_email' ) ); + + update_option( 'WPLANG', 'en_GB' ); + update_user_meta( $admin_user->ID, 'locale', 'de_DE' ); + update_user_meta( self::$contrib_id, 'locale', 'es_ES' ); + + $admin_email_locale = null; + $user_email_locale = null; + + add_filter( + 'wp_new_user_notification_email_admin', + static function ( $email ) use ( &$admin_email_locale ) { + $admin_email_locale = get_locale(); + return $email; + } + ); + add_filter( + 'wp_new_user_notification_email', + static function ( $email ) use ( &$user_email_locale ) { + $user_email_locale = get_locale(); + return $email; + } + ); + + wp_new_user_notification( self::$contrib_id, null, 'both' ); + + $mailer = tests_retrieve_phpmailer_instance(); + + $was_admin_email_sent = false; + $was_user_email_sent = false; + + /* + * Check to see if a notification email was sent to the + * post author `blackburn@battlefield3.com` and and site admin `admin@example.org`. + */ + $first_recipient = $mailer->get_recipient( 'to' ); + if ( $first_recipient ) { + $was_admin_email_sent = WP_TESTS_EMAIL === $first_recipient->address; + $was_user_email_sent = 'blackburn@battlefield3.com' === $first_recipient->address; + } + + $second_recipient = $mailer->get_recipient( 'to', 1 ); + if ( $second_recipient ) { + $was_user_email_sent = 'blackburn@battlefield3.com' === $second_recipient->address; + } + + $this->assertTrue( $was_admin_email_sent, 'Admin email was not sent as expected' ); + $this->assertTrue( $was_user_email_sent, 'User email was not sent as expected' ); + $this->assertSame( 'de_DE', $admin_email_locale, 'Admin email was not sent in the expected locale' ); + $this->assertSame( 'es_ES', $user_email_locale, 'User email was not sent in the expected locale' ); + } + + /** + * Callback that returns 0.0. + * + * @return float 0.0. + */ + public function cb_return_zero_float() { + return 0.0; + } + + /** + * Callback that returns '0'. + * + * @return string '0'. + */ + public function cb_return_zero_string() { + return '0'; + } + + /** + * Callback that returns array( true ). + * + * @return array array( true ) + */ + public function cb_return_array_true() { + return array( true ); + } + /** * Ensure blog's admin email change notification emails do not contain encoded HTML entities * @@ -1351,7 +2048,7 @@ public function test_new_admin_email_notification_html_entities_decoded() { // Assert recipient is correct. $this->assertSame( $new_email, $recipient->address, 'Admin email change notification recipient not as expected' ); - // Assert that HTML entites have been decode in body and subject. + // Assert that HTML entities have been decode in body and subject. $this->assertStringContainsString( '\'Test\' blog\'s "name" has &', $email->subject, 'Email subject does not contain the decoded HTML entities' ); $this->assertStringNotContainsString( ''Test' blog's "name" has <html entities> &', $email->subject, $email->subject, 'Email subject does contains HTML entities' ); } @@ -1555,7 +2252,7 @@ public function test_send_confirmation_on_profile_email() { reset_phpmailer_instance(); $was_confirmation_email_sent = false; - $user = $this->factory()->user->create_and_get( + $user = self::factory()->user->create_and_get( array( 'user_email' => 'before@example.com', ) @@ -1592,7 +2289,7 @@ public function test_remove_send_confirmation_on_profile_email() { reset_phpmailer_instance(); $was_confirmation_email_sent = false; - $user = $this->factory()->user->create_and_get( + $user = self::factory()->user->create_and_get( array( 'user_email' => 'before@example.com', ) @@ -1654,11 +2351,42 @@ public function test_send_confirmation_on_profile_email_html_entities_decoded() // Assert recipient is correct. $this->assertSame( 'new-email@test.dev', $recipient->address, 'User email change confirmation recipient not as expected' ); - // Assert that HTML entites have been decoded in body and subject. + // Assert that HTML entities have been decoded in body and subject. $this->assertStringContainsString( '\'Test\' blog\'s "name" has &', $email->subject, 'Email subject does not contain the decoded HTML entities' ); $this->assertStringNotContainsString( ''Test' blog's "name" has <html entities> &', $email->subject, 'Email subject does contains HTML entities' ); } + /** + * Tests that the `WP_User::$roles` property is a sequential array. + * + * @ticket 63427 + * + * @covers WP_User::get_role_caps + */ + public function test_wp_user_roles_property_is_sequential_array() { + $user = new WP_User( self::$author_id ); + $this->assertTrue( $this->is_sequential( $user->roles ), 'Initial roles array should be sequential.' ); + + $user->remove_role( 'author' ); + $this->assertIsArray( $user->roles, 'After removing all roles, $user->roles should still be an array.' ); + $this->assertSame( array(), $user->roles, 'After removing all roles, $user->roles should be an empty array.' ); + + $user->add_role( 'author' ); + $user->add_role( 'subscriber' ); + $this->assertSame( array( 'author', 'subscriber' ), $user->roles, 'After adding multiple roles, $user->roles should contain added roles.' ); + $this->assertTrue( $this->is_sequential( $user->roles ), 'After adding multiple roles, $user->roles should still be sequential.' ); + } + + /** + * Determines whether an array has sequential numeric keys. + * + * @param array $arr The array to check. + * @return bool True if the array has sequential numeric keys, false otherwise. + */ + private function is_sequential( array $arr ) { + return array_keys( $arr ) === range( 0, count( $arr ) - 1 ); + } + /** * @ticket 42564 */ @@ -1667,11 +2395,7 @@ public function test_edit_user_role_update() { $_GET = array(); $_REQUEST = array(); - $administrator = self::factory()->user->create( - array( - 'role' => 'administrator', - ) - ); + $administrator = self::$admin_id; wp_set_current_user( $administrator ); @@ -1685,11 +2409,7 @@ public function test_edit_user_role_update() { $this->assertSame( array( 'administrator' ), get_userdata( $administrator )->roles ); // Promote an editor to an administrator. - $editor = self::factory()->user->create( - array( - 'role' => 'editor', - ) - ); + $editor = self::$editor_id; $_POST['role'] = 'administrator'; $_POST['email'] = 'administrator@administrator.test'; @@ -1706,7 +2426,7 @@ public function test_edit_user_role_update() { * @ticket 43547 */ public function test_wp_user_personal_data_exporter_no_user() { - $actual = wp_user_personal_data_exporter( 'not-a-user-email@test.com' ); + $actual = wp_user_personal_data_exporter( 'not-a-user-email@example.com' ); $expected = array( 'data' => array(), @@ -1799,7 +2519,6 @@ public function test_wp_community_events_location_city_personal_data_exporter() // Contains location longitude. $this->assertSame( 'Longitude', $actual['data'][1]['data'][3]['name'] ); $this->assertSame( '-84.5143900', $actual['data'][1]['data'][3]['value'] ); - } /** @@ -1869,6 +2588,7 @@ public function test_wp_insert_user_with_meta() { $update_data = array( 'ID' => $create_user, 'user_login' => 'test_user', + 'user_email' => 'user@example.com', 'meta_input' => array( 'test_meta_key' => 'test_meta_updated', 'custom_meta' => 'updated_value', @@ -1909,9 +2629,9 @@ public function test_wp_insert_user_with_meta() { * This hook is used in `test_wp_insert_user_with_meta()`. */ public function filter_custom_meta( $meta_input ) { - // Update some meta inputs + // Update some meta inputs. $meta_input['test_meta_key'] = 'update_from_filter'; - // Add a new meta + // Add a new meta. $meta_input['new_meta_from_filter'] = 'new_from_filter'; return $meta_input; @@ -1943,20 +2663,18 @@ public function test_filter_wp_privacy_additional_user_profile_data() { $this->assertCount( 12, $actual['data'][0]['data'] ); // Check that the item added by the filter was retained. - $this->assertSame( + $this->assertCount( 1, - count( - wp_list_filter( - $actual['data'][0]['data'], - array( - 'name' => 'Test Additional Data Name', - 'value' => 'Test Additional Data Value', - ) + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', ) ) ); - // _doing_wrong() should be called because the filter callback + // _doing_it_wrong() should be called because the filter callback // adds a item with a 'name' that is the same as one generated by core. $this->setExpectedIncorrectUsage( 'wp_user_personal_data_exporter' ); add_filter( 'wp_privacy_additional_user_profile_data', array( $this, 'export_additional_user_profile_data_with_dup_name' ) ); @@ -1975,28 +2693,24 @@ public function test_filter_wp_privacy_additional_user_profile_data() { $this->assertCount( 12, $actual['data'][0]['data'] ); // Check that the duplicate 'name' => 'User ID' was stripped. - $this->assertSame( + $this->assertCount( 1, - count( - wp_list_filter( - $actual['data'][0]['data'], - array( - 'name' => 'User ID', - ) + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'User ID', ) ) ); // Check that the item added by the filter was retained. - $this->assertSame( + $this->assertCount( 1, - count( - wp_list_filter( - $actual['data'][0]['data'], - array( - 'name' => 'Test Additional Data Name', - 'value' => 'Test Additional Data Value', - ) + wp_list_filter( + $actual['data'][0]['data'], + array( + 'name' => 'Test Additional Data Name', + 'value' => 'Test Additional Data Value', ) ) ); @@ -2051,4 +2765,113 @@ public function export_additional_user_profile_data_with_dup_name() { return $additional_profile_data; } + + /** + * Tests that wp_insert_user() does not unnecessarily update the 'use_ssl' meta. + * + * @ticket 60299 + * + * @covers ::wp_insert_user + */ + public function test_wp_insert_user_should_not_unnecessary_update_use_ssl_meta() { + $user_id = self::$contrib_id; + // Keep track of database writing calls. + $db_update_count = 0; + + // Track database updates via update_user_meta() with 'use_ssl' meta key. + add_action( + 'update_user_meta', + function ( $meta_id, $object_id, $meta_key ) use ( &$db_update_count ) { + if ( 'use_ssl' !== $meta_key ) { + return; + } + $db_update_count++; + }, + 10, + 3 + ); + + $_POST = array( + 'nickname' => 'nickname_test', + 'email' => 'email_test_1@example.com', + 'use_ssl' => 1, + ); + + $user_id = edit_user( $user_id ); + + $this->assertIsInt( $user_id ); + $this->assertSame( 1, $db_update_count ); + + // Update the user without changing the 'use_ssl' meta. + $_POST['email'] = 'email_test_2@example.com'; + $user_id = edit_user( $user_id ); + + // Verify there are no updates to 'use_ssl' user meta. + $this->assertSame( 1, $db_update_count ); + } + + /** + * Tests that `wp_set_password` action is triggered correctly during `wp_insert_user()`. + * + * @ticket 22114 + */ + public function test_set_password_action_fires_during_wp_insert_user() { + $mock_action = new MockAction(); + + add_action( 'wp_set_password', array( $mock_action, 'action' ), 10, 3 ); + + $userdata = array( + 'user_login' => 'testuser_' . wp_rand(), + 'user_pass' => 'initialpassword', + 'user_email' => 'testuser@example.com', + ); + + $user_id = wp_insert_user( $userdata ); + + // Assert that `wp_set_password` was triggered once during user creation. + $this->assertSame( 1, $mock_action->get_call_count(), 'wp_set_password was not triggered during user creation.' ); + + $args = $mock_action->get_args(); + + $this->assertSame( $userdata['user_pass'], $args[0][0], 'Wrong password argument in action.' ); + $this->assertSame( $user_id, $args[0][1], 'Wrong user ID in action.' ); + } + + /** + * Tests that `wp_set_password` action is triggered correctly during `wp_update_user()`. + * + * @ticket 22114 + */ + public function test_set_password_action_on_user_update() { + $mock_action = new MockAction(); + + add_action( 'wp_set_password', array( $mock_action, 'action' ), 10, 3 ); + + $user_id = $this->factory()->user->create( + array( + 'role' => 'subscriber', + 'user_login' => 'testuser_update', + 'user_email' => 'testuser_update@example.com', + 'user_pass' => 'initialpassword', + ) + ); + + $mock_action->reset(); + + $updated_password = 'newpassword123'; + + $userdata = array( + 'ID' => $user_id, + 'user_pass' => $updated_password, + ); + + wp_update_user( $userdata ); + + $this->assertSame( 1, $mock_action->get_call_count(), 'wp_set_password was not triggered during password update.' ); + + $args = $mock_action->get_args(); + + $this->assertSame( $updated_password, $args[0][0], 'Invalid password in wp_set_password action.' ); + $this->assertSame( $user_id, $args[0][1], 'Invalid user ID in wp_set_password action.' ); + } } diff --git a/tests/phpunit/tests/user/author.php b/tests/phpunit/tests/user/author.php deleted file mode 100644 index 5786578855624..0000000000000 --- a/tests/phpunit/tests/user/author.php +++ /dev/null @@ -1,147 +0,0 @@ -user->create( - array( - 'role' => 'author', - 'user_login' => 'test_author', - 'display_name' => 'Test Author', - 'description' => 'test_author', - ) - ); - - self::$post_id = $factory->post->create( - array( - 'post_author' => self::$author_id, - 'post_status' => 'publish', - 'post_content' => rand_str(), - 'post_title' => rand_str(), - 'post_type' => 'post', - ) - ); - } - - public function set_up() { - parent::set_up(); - - setup_postdata( get_post( self::$post_id ) ); - } - - public function test_get_the_author() { - $author_name = get_the_author(); - $user = new WP_User( self::$author_id ); - - $this->assertSame( $user->display_name, $author_name ); - $this->assertSame( 'Test Author', $author_name ); - } - - public function test_get_the_author_meta() { - $this->assertSame( 'test_author', get_the_author_meta( 'login' ) ); - $this->assertSame( 'test_author', get_the_author_meta( 'user_login' ) ); - $this->assertSame( 'Test Author', get_the_author_meta( 'display_name' ) ); - - $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); - $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); - add_user_meta( self::$author_id, 'user_description', 'user description' ); - $this->assertSame( 'user description', get_user_meta( self::$author_id, 'user_description', true ) ); - // user_description in meta is ignored. The content of description is returned instead. - // See #20285. - $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); - $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); - update_user_meta( self::$author_id, 'user_description', '' ); - $this->assertSame( '', get_user_meta( self::$author_id, 'user_description', true ) ); - $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); - $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); - - $this->assertSame( '', get_the_author_meta( 'does_not_exist' ) ); - } - - public function test_get_the_author_meta_no_authordata() { - unset( $GLOBALS['authordata'] ); - $this->assertSame( '', get_the_author_meta( 'id' ) ); - $this->assertSame( '', get_the_author_meta( 'user_login' ) ); - $this->assertSame( '', get_the_author_meta( 'does_not_exist' ) ); - } - - public function test_get_the_author_posts() { - // Test with no global post, result should be 0 because no author is found. - $this->assertSame( 0, get_the_author_posts() ); - $GLOBALS['post'] = self::$post_id; - $this->assertEquals( 1, get_the_author_posts() ); - } - - /** - * @ticket 30904 - */ - public function test_get_the_author_posts_with_custom_post_type() { - register_post_type( 'wptests_pt' ); - - $cpt_ids = self::factory()->post->create_many( - 2, - array( - 'post_author' => self::$author_id, - 'post_type' => 'wptests_pt', - ) - ); - $GLOBALS['post'] = $cpt_ids[0]; - - $this->assertEquals( 2, get_the_author_posts() ); - - _unregister_post_type( 'wptests_pt' ); - } - - /** - * @ticket 30355 - */ - public function test_get_the_author_posts_link_no_permalinks() { - $author = get_userdata( self::$author_id ); - - $GLOBALS['authordata'] = $author->data; - - $link = get_the_author_posts_link(); - - $url = sprintf( 'http://%1$s/?author=%2$s', WP_TESTS_DOMAIN, $author->ID ); - - $this->assertStringContainsString( $url, $link ); - $this->assertStringContainsString( 'Posts by Test Author', $link ); - $this->assertStringContainsString( '>Test Author', $link ); - - unset( $GLOBALS['authordata'] ); - } - - /** - * @ticket 30355 - */ - public function test_get_the_author_posts_link_with_permalinks() { - $this->set_permalink_structure( '/%postname%/' ); - - $author = get_userdata( self::$author_id ); - - $GLOBALS['authordata'] = $author; - - $link = get_the_author_posts_link(); - - $url = sprintf( 'http://%1$s/author/%2$s/', WP_TESTS_DOMAIN, $author->user_nicename ); - - $this->set_permalink_structure( '' ); - - $this->assertStringContainsString( $url, $link ); - $this->assertStringContainsString( 'Posts by Test Author', $link ); - $this->assertStringContainsString( '>Test Author', $link ); - - unset( $GLOBALS['authordata'] ); - } - -} diff --git a/tests/phpunit/tests/user/capabilities.php b/tests/phpunit/tests/user/capabilities.php index 496de9487a09f..b92b0db231ecb 100644 --- a/tests/phpunit/tests/user/capabilities.php +++ b/tests/phpunit/tests/user/capabilities.php @@ -30,6 +30,15 @@ class Tests_User_Capabilities extends WP_UnitTestCase { */ protected static $block_id; + /** + * Temporary storage for roles for tests using filter callbacks. + * + * Used in the `test_wp_roles_init_action()` method. + * + * @var array + */ + private $role_test_wp_roles_init; + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { self::$users = array( 'anonymous' => new WP_User( 0 ), @@ -57,7 +66,15 @@ public function set_up() { parent::set_up(); // Keep track of users we create. $this->flush_roles(); + } + + /** + * Clean up after each test. + */ + public function tear_down() { + unset( $this->role_test_wp_roles_init ); + parent::tear_down(); } public static function wpTearDownAfterClass() { @@ -164,7 +181,6 @@ private function _getSingleSitePrimitiveCaps() { 'subscriber' => array( 'subscriber' ), ); - } private function _getMultiSitePrimitiveCaps() { @@ -247,7 +263,6 @@ private function _getMultiSitePrimitiveCaps() { 'subscriber' => array( 'subscriber' ), ); - } private function _getSingleSiteMetaCaps() { @@ -435,12 +450,11 @@ public function test_all_caps_of_users_are_being_tested() { $this->assertSame( array(), $diff, "User with {$role} role has capabilities that aren't being tested" ); } - } /** * Test the tests. The administrator role has all primitive capabilities, therefore the - * primitive capabilitity tests can be tested by checking that the list of tested + * primitive capability tests can be tested by checking that the list of tested * capabilities matches those of the administrator role. * * @group capTestTests @@ -556,7 +570,8 @@ public function testMetaCapsTestsAreCorrect() { $expected['read_app_password'], $expected['edit_app_password'], $expected['delete_app_passwords'], - $expected['delete_app_password'] + $expected['delete_app_password'], + $expected['edit_block_binding'] ); $expected = array_keys( $expected ); @@ -729,7 +744,6 @@ public function test_link_manager_caps() { update_option( 'link_manager_enabled', '0' ); $this->assertSame( '0', get_option( 'link_manager_enabled' ) ); - } /** @@ -743,7 +757,6 @@ public function test_unfiltered_upload_caps() { $this->assertFalse( $user->has_cap( 'unfiltered_upload' ), "User with the {$role} role should not have the unfiltered_upload capability" ); $this->assertFalse( user_can( $user, 'unfiltered_upload' ), "User with the {$role} role should not have the unfiltered_upload capability" ); } - } /** @@ -899,7 +912,6 @@ public function test_user_subscriber_contributor() { $user->remove_role( 'contributor' ); // User should have one role now. $this->assertSame( array( 'subscriber' ), $user->roles ); - } /** @@ -981,7 +993,30 @@ public function test_add_role() { } /** - * Change the capabilites associated with a role and make sure the change + * Test add_role with implied capabilities grant successfully grants capabilities. + * + * @ticket 43421 + */ + public function test_add_role_with_single_level_capabilities() { + $role_name = 'janitor'; + add_role( + $role_name, + 'Janitor', + array( + 'foo', + ) + ); + $this->flush_roles(); + + // Assign a user to that role. + $id = self::factory()->user->create( array( 'role' => $role_name ) ); + $user = new WP_User( $id ); + + $this->assertTrue( $user->has_cap( 'foo' ) ); + } + + /** + * Change the capabilities associated with a role and make sure the change * is reflected in has_cap(). */ public function test_role_add_cap() { @@ -1019,11 +1054,10 @@ public function test_role_add_cap() { remove_role( $role_name ); $this->flush_roles(); $this->assertFalse( $wp_roles->is_role( $role_name ) ); - } /** - * Change the capabilites associated with a role and make sure the change + * Change the capabilities associated with a role and make sure the change * is reflected in has_cap(). */ public function test_role_remove_cap() { @@ -1064,7 +1098,6 @@ public function test_role_remove_cap() { remove_role( $role_name ); $this->flush_roles(); $this->assertFalse( $wp_roles->is_role( $role_name ) ); - } /** @@ -1103,7 +1136,6 @@ public function test_user_add_cap() { $this->assertFalse( $user_1->has_cap( $cap ), "User should not have the {$cap} capability" ); } } - } /** @@ -1135,7 +1167,6 @@ public function test_user_remove_cap() { // Check the removed cap on both users. $this->assertFalse( $user_1->has_cap( 'publish_posts' ) ); $this->assertFalse( $user_2->has_cap( 'publish_posts' ) ); - } /** @@ -1199,7 +1230,6 @@ public function test_user_remove_all_caps() { // User level should be empty. $this->assertEmpty( $user->user_level ); - } /** @@ -1296,13 +1326,9 @@ public function test_post_meta_caps() { } } - public function authorless_post_statuses() { - return array( array( 'draft' ), array( 'private' ), array( 'publish' ) ); - } - /** * @ticket 27020 - * @dataProvider authorless_post_statuses + * @dataProvider data_authorless_post */ public function test_authorless_post( $status ) { // Make a post without an author. @@ -1331,6 +1357,10 @@ public function test_authorless_post( $status ) { $this->assertSame( 'publish' === $status, $contributor->has_cap( 'read_post', $post ) ); } + public function data_authorless_post() { + return array( array( 'draft' ), array( 'private' ), array( 'publish' ) ); + } + /** * @ticket 16714 */ @@ -1529,7 +1559,6 @@ public function test_taxonomy_caps_map_correctly_to_their_meta_cap( $taxonomy ) "Role: {$role}" ); } - } public function dataTaxonomies() { @@ -1595,6 +1624,7 @@ public function test_taxonomy_meta_capabilities_with_non_existent_terms() { $editor = self::$users['editor']; + $this->setExpectedIncorrectUsage( 'map_meta_cap' ); foreach ( $caps as $cap ) { // `null` represents a non-existent term ID. $this->assertFalse( user_can( $editor->ID, $cap, null ) ); @@ -1623,40 +1653,116 @@ public function test_set_role_same_role() { $user = self::$users['administrator']; $caps = $user->caps; $this->assertNotEmpty( $user->caps ); + $user->set_role( 'administrator' ); $this->assertNotEmpty( $user->caps ); $this->assertSame( $caps, $user->caps ); } - public function test_current_user_can_for_blog() { + /** + * @ticket 54164 + */ + public function test_set_role_fires_remove_user_role_and_add_user_role_hooks() { + $user = self::$users['administrator']; + + $remove_user_role = new MockAction(); + $add_user_role = new MockAction(); + add_action( 'remove_user_role', array( $remove_user_role, 'action' ) ); + add_action( 'add_user_role', array( $add_user_role, 'action' ) ); + + $user->set_role( 'editor' ); + $user->set_role( 'administrator' ); + $this->assertSame( 2, $remove_user_role->get_call_count() ); + $this->assertSame( 2, $add_user_role->get_call_count() ); + } + + /** + * @group can_for_site + */ + public function test_current_user_can_for_site() { global $wpdb; $user = self::$users['administrator']; $old_uid = get_current_user_id(); wp_set_current_user( $user->ID ); - $this->assertTrue( current_user_can_for_blog( get_current_blog_id(), 'edit_posts' ) ); - $this->assertFalse( current_user_can_for_blog( get_current_blog_id(), 'foo_the_bar' ) ); + $this->assertTrue( current_user_can_for_site( get_current_blog_id(), 'edit_posts' ) ); + $this->assertFalse( current_user_can_for_site( get_current_blog_id(), 'foo_the_bar' ) ); + if ( ! is_multisite() ) { - $this->assertTrue( current_user_can_for_blog( 12345, 'edit_posts' ) ); + $this->assertTrue( current_user_can_for_site( 12345, 'edit_posts' ) ); + $this->assertFalse( current_user_can_for_site( 12345, 'foo_the_bar' ) ); return; } $suppress = $wpdb->suppress_errors(); - $this->assertFalse( current_user_can_for_blog( 12345, 'edit_posts' ) ); + $this->assertFalse( current_user_can_for_site( 12345, 'edit_posts' ) ); $wpdb->suppress_errors( $suppress ); $blog_id = self::factory()->blog->create( array( 'user_id' => $user->ID ) ); - $this->assertTrue( current_user_can_for_blog( $blog_id, 'edit_posts' ) ); - $this->assertFalse( current_user_can_for_blog( $blog_id, 'foo_the_bar' ) ); + + $this->assertNotWPError( $blog_id ); + $this->assertTrue( current_user_can_for_site( $blog_id, 'edit_posts' ) ); + $this->assertFalse( current_user_can_for_site( $blog_id, 'foo_the_bar' ) ); + + $another_blog_id = self::factory()->blog->create( array( 'user_id' => self::$users['author']->ID ) ); + + $this->assertNotWPError( $another_blog_id ); + + // Verify the user doesn't have a capability + $this->assertFalse( current_user_can_for_site( $another_blog_id, 'edit_posts' ) ); + + // Add the current user to the site + add_user_to_blog( $another_blog_id, $user->ID, 'author' ); + + // Verify they now have the capability + $this->assertTrue( current_user_can_for_site( $another_blog_id, 'edit_posts' ) ); wp_set_current_user( $old_uid ); } + /** + * @group can_for_site + */ + public function test_user_can_for_site() { + $user = self::$users['editor']; + + $this->assertTrue( user_can_for_site( $user->ID, get_current_blog_id(), 'edit_posts' ) ); + $this->assertFalse( user_can_for_site( $user->ID, get_current_blog_id(), 'foo_the_bar' ) ); + + if ( ! is_multisite() ) { + $this->assertTrue( user_can_for_site( $user->ID, 12345, 'edit_posts' ) ); + $this->assertFalse( user_can_for_site( $user->ID, 12345, 'foo_the_bar' ) ); + return; + } + + $blog_id = self::factory()->blog->create( array( 'user_id' => $user->ID ) ); + + $this->assertNotWPError( $blog_id ); + $this->assertTrue( user_can_for_site( $user->ID, $blog_id, 'edit_posts' ) ); + $this->assertFalse( user_can_for_site( $user->ID, $blog_id, 'foo_the_bar' ) ); + + $author = self::$users['author']; + + // Verify another user doesn't have a capability + $this->assertFalse( is_user_member_of_blog( $author->ID, $blog_id ) ); + $this->assertFalse( user_can_for_site( $author->ID, $blog_id, 'edit_posts' ) ); + + // Add the author to the site + add_user_to_blog( $blog_id, $author->ID, 'author' ); + + // Verify they now have the capability + $this->assertTrue( is_user_member_of_blog( $author->ID, $blog_id ) ); + $this->assertTrue( user_can_for_site( $author->ID, $blog_id, 'edit_posts' ) ); + + // Verify the user doesn't have a capability for a non-existent site + $this->assertFalse( user_can_for_site( $user->ID, -1, 'edit_posts' ) ); + } + /** * @group ms-required */ - public function test_borked_current_user_can_for_blog() { + public function test_borked_current_user_can_for_site() { $orig_blog_id = get_current_blog_id(); $blog_id = self::factory()->blog->create(); @@ -1664,13 +1770,13 @@ public function test_borked_current_user_can_for_blog() { add_action( 'switch_blog', array( $this, 'nullify_current_user_and_keep_nullifying_user' ) ); - current_user_can_for_blog( $blog_id, 'edit_posts' ); + current_user_can_for_site( $blog_id, 'edit_posts' ); $this->assertSame( $orig_blog_id, get_current_blog_id() ); } public function nullify_current_user() { - // Prevents fatal errors in ::tearDown()'s and other uses of restore_current_blog(). + // Prevents fatal errors in ::tear_down()'s and other uses of restore_current_blog(). $function_stack = wp_debug_backtrace_summary( null, 0, false ); if ( in_array( 'restore_current_blog', $function_stack, true ) ) { return; @@ -1724,13 +1830,49 @@ public function test_multisite_administrator_can_not_edit_users() { $this->assertFalse( current_user_can( 'edit_user', $other_user->ID ) ); } - public function test_user_can_edit_self() { - foreach ( self::$users as $role => $user ) { - wp_set_current_user( $user->ID ); - $this->assertTrue( current_user_can( 'edit_user', $user->ID ), "User with role {$role} should have the capability to edit their own profile" ); + /** + * Test if a user can edit their own profile based on their role. + * + * @ticket 63684 + * + * @dataProvider data_user_can_edit_self + * + * @param string $role The role of the user. + * @param bool $can_edit_self Whether the user can edit their own profile. + */ + public function test_user_can_edit_self( $role, $can_edit_self = true ) { + $user = self::$users[ $role ]; + wp_set_current_user( $user->ID ); + + if ( $can_edit_self ) { + $this->assertTrue( + current_user_can( 'edit_user', $user->ID ), + "User with role '{$role}' should have the capability to edit their own profile" + ); + } else { + $this->assertFalse( + current_user_can( 'edit_user', $user->ID ), + "User with role '{$role}' should not have the capability to edit their own profile" + ); } } + /** + * Data provider for test_user_can_edit_self. + * + * @return array[] Data provider. + */ + public static function data_user_can_edit_self() { + return array( + 'anonymous' => array( 'anonymous', false ), + 'administrator' => array( 'administrator', true ), + 'editor' => array( 'editor', true ), + 'author' => array( 'author', true ), + 'contributor' => array( 'contributor', true ), + 'subscriber' => array( 'subscriber', true ), + ); + } + public function test_only_admins_and_super_admins_can_remove_users() { if ( is_multisite() ) { $this->assertTrue( user_can( self::$super_admin->ID, 'remove_user', self::$users['subscriber']->ID ) ); @@ -1791,7 +1933,7 @@ public function test_contributor_cannot_edit_scheduled_post() { $contributor = self::$users['contributor']; // Give them a scheduled post. - $post = $this->factory->post->create_and_get( + $post = self::factory()->post->create_and_get( array( 'post_author' => $contributor->ID, 'post_status' => 'future', @@ -1813,7 +1955,6 @@ public function test_contributor_cannot_edit_scheduled_post() { // Ensure contributor can't edit, un-trash, or delete the post. $this->assertFalse( user_can( $contributor->ID, 'edit_post', $post->ID ) ); $this->assertFalse( user_can( $contributor->ID, 'delete_post', $post->ID ) ); - } /** @@ -1944,7 +2085,6 @@ public function test_cpt_with_page_capability_type() { $this->assertFalse( user_can( $contributor->ID, 'edit_post', $author_post->ID ) ); _unregister_post_type( 'page_capability' ); - } public function test_non_logged_in_users_have_no_capabilities() { @@ -1974,16 +2114,13 @@ public function test_wp_logout_should_clear_current_user() { wp_logout(); $this->assertSame( 0, get_current_user_id() ); - } - - protected $_role_test_wp_roles_role; /** * @ticket 23016 */ public function test_wp_roles_init_action() { - $this->_role_test_wp_roles_init = array( + $this->role_test_wp_roles_init = array( 'role' => 'test_wp_roles_init', 'info' => array( 'name' => 'Test WP Roles Init', @@ -1996,16 +2133,16 @@ public function test_wp_roles_init_action() { remove_action( 'wp_roles_init', array( $this, '_hook_wp_roles_init' ) ); - $expected = new WP_Role( $this->_role_test_wp_roles_init['role'], $this->_role_test_wp_roles_init['info']['capabilities'] ); + $expected = new WP_Role( $this->role_test_wp_roles_init['role'], $this->role_test_wp_roles_init['info']['capabilities'] ); - $role = $wp_roles->get_role( $this->_role_test_wp_roles_init['role'] ); + $role = $wp_roles->get_role( $this->role_test_wp_roles_init['role'] ); $this->assertEquals( $expected, $role ); - $this->assertContains( $this->_role_test_wp_roles_init['info']['name'], $wp_roles->role_names ); + $this->assertContains( $this->role_test_wp_roles_init['info']['name'], $wp_roles->role_names ); } public function _hook_wp_roles_init( $wp_roles ) { - $wp_roles->add_role( $this->_role_test_wp_roles_init['role'], $this->_role_test_wp_roles_init['info']['name'], $this->_role_test_wp_roles_init['info']['capabilities'] ); + $wp_roles->add_role( $this->role_test_wp_roles_init['role'], $this->role_test_wp_roles_init['info']['name'], $this->role_test_wp_roles_init['info']['capabilities'] ); } /** @@ -2357,4 +2494,103 @@ public function data_block_caps() { return $data; } + + /** + * Test `edit_block_binding` meta capability is properly mapped. + * + * @ticket 61945 + */ + public function test_edit_block_binding_caps_are_mapped_correctly() { + $author = self::$users['administrator']; + $post = self::factory()->post->create_and_get( + array( + 'post_author' => $author->ID, + 'post_type' => 'post', + ) + ); + + foreach ( self::$users as $role => $user ) { + // It should map to `edit_{post_type}` if editing a post. + $this->assertSame( + user_can( $user->ID, 'edit_post', $post->ID ), + user_can( + $user->ID, + 'edit_block_binding', + new WP_Block_Editor_Context( + array( + 'post' => $post, + 'name' => 'core/edit-post', + ) + ) + ), + "Role: {$role} in post editing" + ); + // It should map to `edit_theme_options` if editing a template. + $this->assertSame( + user_can( $user->ID, 'edit_theme_options' ), + user_can( + $user->ID, + 'edit_block_binding', + new WP_Block_Editor_Context( + array( + 'post' => null, + 'name' => 'core/edit-site', + ) + ) + ), + "Role: {$role} in template editing" + ); + } + } + + /** + * Ensure that caps are updated correctly when using `update_option()` to save roles. + * + * Compares the efficiency and accuracy of updating role capabilities when `WP_Roles` + * uses the database vs when the updates are done via `update_option()`. + * + * This method of updating roles is used in `populate_roles()` to reduce the number of + * queries by approximately 300. + * + * @ticket 37687 + */ + public function test_role_capabilities_updated_correctly_via_update_option() { + global $wp_roles; + $emcee_role = 'emcee'; + $wp_roles->add_role( $emcee_role, 'Emcee', array( 'level_1' => true ) ); + $this->flush_roles(); + + $expected_caps = array( + 'level_1' => true, + 'attend_kit_kat_klub' => true, + 'win_tony_award' => true, + ); + + $start_queries = get_num_queries(); + $wp_roles->add_cap( $emcee_role, 'attend_kit_kat_klub' ); + $wp_roles->add_cap( $emcee_role, 'win_tony_award' ); + $emcee_queries = get_num_queries() - $start_queries; + $this->flush_roles(); + $emcee_caps = $wp_roles->get_role( $emcee_role )->capabilities; + + $wp_roles->use_db = false; + $sally_role = 'sally'; + $wp_roles->add_role( $sally_role, 'Sally Bowles', array( 'level_1' => true ) ); + $start_queries = get_num_queries(); + $wp_roles->add_cap( $sally_role, 'attend_kit_kat_klub' ); + $wp_roles->add_cap( $sally_role, 'win_tony_award' ); + + update_option( $wp_roles->role_key, $wp_roles->roles, true ); + $sally_queries = get_num_queries() - $start_queries; + $wp_roles->use_db = true; + + // Restore the default value. + $this->flush_roles(); + $sally_caps = $wp_roles->get_role( $sally_role )->capabilities; + + $this->assertSameSetsWithIndex( $expected_caps, $emcee_caps, 'Emcee role should include the three expected capabilities.' ); + $this->assertSameSetsWithIndex( $expected_caps, $sally_caps, 'Sally role should include the three expected capabilities.' ); + $this->assertSameSetsWithIndex( $emcee_caps, $sally_caps, 'Emcee and Sally roles should have the same capabilities after update.' ); + $this->assertLessThan( $emcee_queries, $sally_queries, 'Updating roles via update_option should be more efficient than WP_Roles using the database.' ); + } } diff --git a/tests/phpunit/tests/user/countUserPosts.php b/tests/phpunit/tests/user/countUserPosts.php index a4244e2814aa4..d7d4e1f9c8db6 100644 --- a/tests/phpunit/tests/user/countUserPosts.php +++ b/tests/phpunit/tests/user/countUserPosts.php @@ -59,34 +59,172 @@ public function set_up() { } public function test_count_user_posts_post_type_should_default_to_post() { - $this->assertEquals( 4, count_user_posts( self::$user_id ) ); + $this->assertSame( '4', count_user_posts( self::$user_id ) ); } /** * @ticket 21364 */ public function test_count_user_posts_post_type_post() { - $this->assertEquals( 4, count_user_posts( self::$user_id, 'post' ) ); + $this->assertSame( '4', count_user_posts( self::$user_id, 'post' ) ); } /** * @ticket 21364 */ public function test_count_user_posts_post_type_cpt() { - $this->assertEquals( 3, count_user_posts( self::$user_id, 'wptests_pt' ) ); + $this->assertSame( '3', count_user_posts( self::$user_id, 'wptests_pt' ) ); } /** * @ticket 32243 */ public function test_count_user_posts_with_multiple_post_types() { - $this->assertEquals( 7, count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) ); + $this->assertSame( '7', count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ) ); } /** * @ticket 32243 */ public function test_count_user_posts_should_ignore_non_existent_post_types() { - $this->assertEquals( 4, count_user_posts( self::$user_id, array( 'foo', 'post' ) ) ); + $this->assertSame( '4', count_user_posts( self::$user_id, array( 'foo', 'post' ) ) ); + } + + /** + * Post count should be correct after reassigning posts to another user. + * + * @ticket 39242 + */ + public function test_reassigning_users_posts_modifies_count() { + // Create new user. + $new_user_id = self::factory()->user->create( + array( + 'role' => 'author', + ) + ); + + // Prior to reassigning posts. + $this->assertSame( '4', count_user_posts( self::$user_id ), 'Original user is expected to have a count of four posts prior to reassignment.' ); + $this->assertSame( '0', count_user_posts( $new_user_id ), 'New user is expected to have a count of zero posts prior to reassignment.' ); + + // Delete the original user, reassigning their posts to the new user. + wp_delete_user( self::$user_id, $new_user_id ); + + // After reassigning posts. + $this->assertSame( '0', count_user_posts( self::$user_id ), 'Original user is expected to have a count of zero posts following reassignment.' ); + $this->assertSame( '4', count_user_posts( $new_user_id ), 'New user is expected to have a count of four posts following reassignment.' ); + } + + /** + * Post count should be correct after deleting user without reassigning posts. + * + * @ticket 39242 + */ + public function test_post_count_retained_after_deleting_user_without_reassigning_posts() { + $this->assertSame( '4', count_user_posts( self::$user_id ), 'User is expected to have a count of four posts prior to deletion.' ); + + // Delete the original user without reassigning their posts. + wp_delete_user( self::$user_id ); + + $this->assertSame( '0', count_user_posts( self::$user_id ), 'User is expected to have a count of zero posts following deletion.' ); + } + + /** + * Post count should work for users that don't exist but have posts assigned. + * + * @ticket 39242 + */ + public function test_count_user_posts_for_non_existent_user() { + $next_user_id = self::$user_id + 1; + + // Assign post to next user. + self::factory()->post->create( + array( + 'post_author' => $next_user_id, + 'post_type' => 'post', + ) + ); + + $next_user_post_count = count_user_posts( $next_user_id ); + $this->assertSame( '1', $next_user_post_count, 'Non-existent user is expected to have count of one post.' ); + } + + /** + * Cached user count value should be accurate after user is created. + * + * @ticket 39242 + */ + public function test_count_user_posts_for_user_created_after_being_assigned_posts() { + global $wpdb; + $next_user_id = (int) $wpdb->get_var( "SELECT `auto_increment` FROM INFORMATION_SCHEMA.TABLES WHERE table_name = '$wpdb->users'" ); + + // Assign post to next user. + self::factory()->post->create( + array( + 'post_author' => $next_user_id, + 'post_type' => 'post', + ) + ); + + // Cache the user count. + count_user_posts( $next_user_id ); + + // Create user. + $real_next_user_id = self::factory()->user->create( + array( + 'role' => 'author', + ) + ); + + $this->assertSame( $next_user_id, $real_next_user_id, 'User ID should match calculated value' ); + $this->assertSame( '1', count_user_posts( $next_user_id ), 'User is expected to have count of one post.' ); + } + + /** + * User count cache should be hit regardless of post type order. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_regardless_of_post_type_order() { + // Prime cache. + count_user_posts( self::$user_id, array( 'wptests_pt', 'post' ) ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post', 'wptests_pt' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache should be hit regardless of post type order.' ); + } + + /** + * User count cache should be hit for string and array of post types. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_for_string_and_array_equivalent_queries() { + // Prime cache. + count_user_posts( self::$user_id, 'post' ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache should be hit for string and array equivalent post types.' ); + } + + /** + * User count cache should be hit for array duplicates and equivalent queries. + * + * @ticket 39242 + */ + public function test_cache_should_be_hit_for_and_array_duplicates_equivalent_queries() { + // Prime cache. + count_user_posts( self::$user_id, array( 'post', 'post', 'post' ) ); + + $query_num_start = get_num_queries(); + count_user_posts( self::$user_id, array( 'post' ) ); + $total_queries = get_num_queries() - $query_num_start; + + $this->assertSame( 0, $total_queries, 'Cache is expected to be hit for equivalent queries with duplicate post types' ); } } diff --git a/tests/phpunit/tests/user/countUsers.php b/tests/phpunit/tests/user/countUsers.php index 224ddcc8d0e08..901ba88f89106 100644 --- a/tests/phpunit/tests/user/countUsers.php +++ b/tests/phpunit/tests/user/countUsers.php @@ -53,7 +53,7 @@ public function test_count_users_is_accurate( $strategy ) { $count = count_users( $strategy ); $this->assertSame( 8, $count['total_users'] ); - $this->assertEquals( + $this->assertSameSetsWithIndex( array( 'administrator' => 2, 'editor' => 1, @@ -64,7 +64,6 @@ public function test_count_users_is_accurate( $strategy ) { ), $count['avail_roles'] ); - } /** @@ -133,7 +132,7 @@ public function test_count_users_multisite_is_accurate( $strategy ) { $count = count_users( $strategy ); $this->assertSame( 8, $count['total_users'] ); - $this->assertEquals( + $this->assertSameSetsWithIndex( array( 'administrator' => 2, 'editor' => 1, @@ -151,7 +150,7 @@ public function test_count_users_multisite_is_accurate( $strategy ) { restore_current_blog(); $this->assertSame( 2, $count['total_users'] ); - $this->assertEquals( + $this->assertSameSetsWithIndex( array( 'administrator' => 1, 'editor' => 1, @@ -166,7 +165,7 @@ public function test_count_users_multisite_is_accurate( $strategy ) { restore_current_blog(); $this->assertSame( 2, $count['total_users'] ); - $this->assertEquals( + $this->assertSameSetsWithIndex( array( 'administrator' => 1, 'contributor' => 1, @@ -174,7 +173,6 @@ public function test_count_users_multisite_is_accurate( $strategy ) { ), $count['avail_roles'] ); - } /** @@ -239,7 +237,7 @@ public function test_count_users_is_accurate_with_multiple_roles( $strategy ) { $count = count_users( $strategy ); $this->assertSame( 3, $count['total_users'] ); - $this->assertEquals( + $this->assertSameSetsWithIndex( array( 'administrator' => 2, 'editor' => 1, @@ -248,7 +246,6 @@ public function test_count_users_is_accurate_with_multiple_roles( $strategy ) { ), $count['avail_roles'] ); - } /** @@ -291,5 +288,4 @@ public function data_count_users_strategies() { ), ); } - } diff --git a/tests/phpunit/tests/user/getActiveBlogForUser.php b/tests/phpunit/tests/user/getActiveBlogForUser.php index 2abb726e2f46b..ef3e5e6eff6d0 100644 --- a/tests/phpunit/tests/user/getActiveBlogForUser.php +++ b/tests/phpunit/tests/user/getActiveBlogForUser.php @@ -1,101 +1,99 @@ user->create(); - } +/** + * Tests specific to users in multisite. + * + * @group user + * @group ms-required + * @group ms-user + * @group multisite + */ +class Tests_User_GetActiveBlogForUser extends WP_UnitTestCase { + + public static $user_id = false; + + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + self::$user_id = $factory->user->create(); + } - public static function wpTearDownAfterClass() { - wpmu_delete_user( self::$user_id ); + public static function wpTearDownAfterClass() { + wpmu_delete_user( self::$user_id ); - global $wp_rewrite; - $wp_rewrite->init(); - } + global $wp_rewrite; + $wp_rewrite->init(); + } - /** - * @ticket 38355 - */ - public function test_get_active_blog_for_user_with_no_sites() { - $current_site_id = get_current_blog_id(); + /** + * @ticket 38355 + */ + public function test_get_active_blog_for_user_with_no_sites() { + $current_site_id = get_current_blog_id(); - remove_user_from_blog( self::$user_id, $current_site_id ); + remove_user_from_blog( self::$user_id, $current_site_id ); - $result = get_active_blog_for_user( self::$user_id ); + $result = get_active_blog_for_user( self::$user_id ); - $this->assertNull( $result ); - } + $this->assertNull( $result ); + } - /** - * @ticket 38355 - */ - public function test_get_active_blog_for_user_with_primary_site() { - $site_id_one = self::factory()->blog->create( array( 'user_id' => self::$user_id ) ); - $site_id_two = self::factory()->blog->create( array( 'user_id' => self::$user_id ) ); + /** + * @ticket 38355 + */ + public function test_get_active_blog_for_user_with_primary_site() { + $site_id_one = self::factory()->blog->create( array( 'user_id' => self::$user_id ) ); + $site_id_two = self::factory()->blog->create( array( 'user_id' => self::$user_id ) ); - $sites = get_blogs_of_user( self::$user_id ); - $site_ids = array_keys( $sites ); - $primary_site_id = $site_ids[1]; + $sites = get_blogs_of_user( self::$user_id ); + $site_ids = array_keys( $sites ); + $primary_site_id = $site_ids[1]; - update_user_meta( self::$user_id, 'primary_blog', $primary_site_id ); + update_user_meta( self::$user_id, 'primary_blog', $primary_site_id ); - $result = get_active_blog_for_user( self::$user_id ); + $result = get_active_blog_for_user( self::$user_id ); - wp_delete_site( $site_id_one ); - wp_delete_site( $site_id_two ); + wp_delete_site( $site_id_one ); + wp_delete_site( $site_id_two ); - $this->assertSame( $primary_site_id, $result->id ); - } + $this->assertSame( $primary_site_id, $result->id ); + } - /** - * @ticket 38355 - */ - public function test_get_active_blog_for_user_without_primary_site() { - $sites = get_blogs_of_user( self::$user_id ); - $site_ids = array_keys( $sites ); - $primary_site_id = $site_ids[0]; + /** + * @ticket 38355 + */ + public function test_get_active_blog_for_user_without_primary_site() { + $sites = get_blogs_of_user( self::$user_id ); + $site_ids = array_keys( $sites ); + $primary_site_id = $site_ids[0]; - delete_user_meta( self::$user_id, 'primary_blog' ); + delete_user_meta( self::$user_id, 'primary_blog' ); - $result = get_active_blog_for_user( self::$user_id ); + $result = get_active_blog_for_user( self::$user_id ); - wp_delete_site( $primary_site_id ); + wp_delete_site( $primary_site_id ); - $this->assertSame( $primary_site_id, $result->id ); - } + $this->assertSame( $primary_site_id, $result->id ); + } - /** - * @ticket 38355 - */ - public function test_get_active_blog_for_user_with_spam_site() { - $current_site_id = get_current_blog_id(); + /** + * @ticket 38355 + */ + public function test_get_active_blog_for_user_with_spam_site() { + $current_site_id = get_current_blog_id(); - $site_id = self::factory()->blog->create( - array( - 'user_id' => self::$user_id, - 'spam' => 1, - ) - ); + $site_id = self::factory()->blog->create( + array( + 'user_id' => self::$user_id, + 'spam' => 1, + ) + ); - add_user_to_blog( $site_id, self::$user_id, 'subscriber' ); - update_user_meta( self::$user_id, 'primary_blog', $site_id ); + add_user_to_blog( $site_id, self::$user_id, 'subscriber' ); + update_user_meta( self::$user_id, 'primary_blog', $site_id ); - $result = get_active_blog_for_user( self::$user_id ); + $result = get_active_blog_for_user( self::$user_id ); - wp_delete_site( $site_id ); + wp_delete_site( $site_id ); - $this->assertSame( $current_site_id, $result->id ); - } + $this->assertSame( $current_site_id, $result->id ); } - -endif; +} diff --git a/tests/phpunit/tests/user/getTheAuthor.php b/tests/phpunit/tests/user/getTheAuthor.php new file mode 100644 index 0000000000000..ebddcb3d5abe6 --- /dev/null +++ b/tests/phpunit/tests/user/getTheAuthor.php @@ -0,0 +1,57 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + } + + public function set_up() { + parent::set_up(); + + setup_postdata( get_post( self::$post_id ) ); + } + + public function test_get_the_author() { + $author_name = get_the_author(); + $user = new WP_User( self::$author_id ); + + $this->assertSame( $user->display_name, $author_name ); + $this->assertSame( 'Test Author', $author_name ); + } + + /** + * @ticket 58157 + */ + public function test_get_the_author_should_return_empty_string_if_authordata_is_not_set() { + unset( $GLOBALS['authordata'] ); + + $this->assertSame( '', get_the_author() ); + } +} diff --git a/tests/phpunit/tests/user/getTheAuthorLink.php b/tests/phpunit/tests/user/getTheAuthorLink.php new file mode 100644 index 0000000000000..ce44774f1ce51 --- /dev/null +++ b/tests/phpunit/tests/user/getTheAuthorLink.php @@ -0,0 +1,71 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + } + + public function set_up() { + parent::set_up(); + + setup_postdata( get_post( self::$post_id ) ); + } + + /** + * @ticket 51859 + * + * @covers ::get_the_author_link + */ + public function test_get_the_author_link() { + $author_url = get_the_author_meta( 'url' ); + $author_display_name = get_the_author(); + + $link = get_the_author_link(); + + $this->assertStringContainsString( $author_url, $link, 'The link does not contain the author URL' ); + $this->assertStringContainsString( $author_display_name, $link, 'The link does not contain the author display name' ); + } + + /** + * @ticket 51859 + * + * @covers ::get_the_author_link + */ + public function test_filtered_get_the_author_link() { + $filter = new MockAction(); + + add_filter( 'the_author_link', array( &$filter, 'filter' ) ); + + get_the_author_link(); + + $this->assertSame( 1, $filter->get_call_count() ); + $this->assertSame( array( 'the_author_link' ), $filter->get_hook_names() ); + } +} diff --git a/tests/phpunit/tests/user/getTheAuthorMeta.php b/tests/phpunit/tests/user/getTheAuthorMeta.php new file mode 100644 index 0000000000000..d9d225812bb30 --- /dev/null +++ b/tests/phpunit/tests/user/getTheAuthorMeta.php @@ -0,0 +1,75 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + } + + public function set_up() { + parent::set_up(); + + setup_postdata( get_post( self::$post_id ) ); + } + + public function test_get_the_author_meta() { + $this->assertSame( 'test_author', get_the_author_meta( 'login' ) ); + $this->assertSame( 'test_author', get_the_author_meta( 'user_login' ) ); + $this->assertSame( 'Test Author', get_the_author_meta( 'display_name' ) ); + + $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); + $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); + + add_user_meta( self::$author_id, 'user_description', 'user description' ); + $this->assertSame( 'user description', get_user_meta( self::$author_id, 'user_description', true ) ); + // user_description in meta is ignored. The content of description is returned instead. + // See #20285. + $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); + $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); + + update_user_meta( self::$author_id, 'user_description', '' ); + $this->assertSame( '', get_user_meta( self::$author_id, 'user_description', true ) ); + $this->assertSame( 'test_author', get_the_author_meta( 'user_description' ) ); + $this->assertSame( 'test_author', trim( get_the_author_meta( 'description' ) ) ); + + $this->assertSame( '', get_the_author_meta( 'does_not_exist' ) ); + } + + /** + * @ticket 20529 + * @ticket 58157 + */ + public function test_get_the_author_meta_should_return_empty_string_if_authordata_is_not_set() { + unset( $GLOBALS['authordata'] ); + + $this->assertSame( '', get_the_author_meta( 'id' ) ); + $this->assertSame( '', get_the_author_meta( 'user_login' ) ); + $this->assertSame( '', get_the_author_meta( 'does_not_exist' ) ); + } +} diff --git a/tests/phpunit/tests/user/getTheAuthorPosts.php b/tests/phpunit/tests/user/getTheAuthorPosts.php new file mode 100644 index 0000000000000..3a0abac5debd5 --- /dev/null +++ b/tests/phpunit/tests/user/getTheAuthorPosts.php @@ -0,0 +1,67 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + } + + public function set_up() { + parent::set_up(); + + setup_postdata( get_post( self::$post_id ) ); + } + + public function test_get_the_author_posts() { + // Test with no global post, result should be 0 because no author is found. + $this->assertSame( 0, get_the_author_posts() ); + $GLOBALS['post'] = self::$post_id; + $this->assertSame( 1, get_the_author_posts() ); + } + + /** + * @ticket 30904 + */ + public function test_get_the_author_posts_with_custom_post_type() { + register_post_type( 'wptests_pt' ); + + $cpt_ids = self::factory()->post->create_many( + 2, + array( + 'post_author' => self::$author_id, + 'post_type' => 'wptests_pt', + ) + ); + $GLOBALS['post'] = $cpt_ids[0]; + + $this->assertSame( 2, get_the_author_posts() ); + + _unregister_post_type( 'wptests_pt' ); + } +} diff --git a/tests/phpunit/tests/user/getTheAuthorPostsLink.php b/tests/phpunit/tests/user/getTheAuthorPostsLink.php new file mode 100644 index 0000000000000..fdef10d12a445 --- /dev/null +++ b/tests/phpunit/tests/user/getTheAuthorPostsLink.php @@ -0,0 +1,89 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + } + + public function set_up() { + parent::set_up(); + + setup_postdata( get_post( self::$post_id ) ); + } + + /** + * @ticket 30355 + */ + public function test_get_the_author_posts_link_no_permalinks() { + $author = get_userdata( self::$author_id ); + + $GLOBALS['authordata'] = $author->data; + + $link = get_the_author_posts_link(); + + $url = sprintf( 'http://%1$s/?author=%2$s', WP_TESTS_DOMAIN, $author->ID ); + + $this->assertStringContainsString( $url, $link ); + $this->assertStringContainsString( '>Test Author', $link ); + + unset( $GLOBALS['authordata'] ); + } + + /** + * @ticket 30355 + */ + public function test_get_the_author_posts_link_with_permalinks() { + $this->set_permalink_structure( '/%postname%/' ); + + $author = get_userdata( self::$author_id ); + + $GLOBALS['authordata'] = $author; + + $link = get_the_author_posts_link(); + + $url = sprintf( 'http://%1$s/author/%2$s/', WP_TESTS_DOMAIN, $author->user_nicename ); + + $this->set_permalink_structure( '' ); + + $this->assertStringContainsString( $url, $link ); + $this->assertStringContainsString( '>Test Author', $link ); + + unset( $GLOBALS['authordata'] ); + } + + /** + * @ticket 58157 + */ + public function test_get_the_author_posts_link_should_return_empty_string_if_authordata_is_not_set() { + unset( $GLOBALS['authordata'] ); + + $this->assertSame( '', get_the_author_posts_link() ); + } +} diff --git a/tests/phpunit/tests/user/getTheModifiedAuthor.php b/tests/phpunit/tests/user/getTheModifiedAuthor.php new file mode 100644 index 0000000000000..af559cf338376 --- /dev/null +++ b/tests/phpunit/tests/user/getTheModifiedAuthor.php @@ -0,0 +1,99 @@ +user->create( + array( + 'role' => 'author', + 'user_login' => 'test_author', + 'display_name' => 'Test Author', + 'description' => 'test_author', + 'user_url' => 'http://example.com', + ) + ); + + self::$post_id = $factory->post->create( + array( + 'post_author' => self::$author_id, + 'post_status' => 'publish', + 'post_content' => 'content', + 'post_title' => 'title', + 'post_type' => 'post', + ) + ); + + add_post_meta( self::$post_id, '_edit_last', self::$author_id ); + } + + public function set_up() { + parent::set_up(); + + $GLOBALS['post'] = get_post( self::$post_id ); + } + + public function test_get_the_modified_author() { + $author_name = get_the_modified_author(); + $user = new WP_User( self::$author_id ); + + $this->assertSame( $user->display_name, $author_name ); + $this->assertSame( 'Test Author', $author_name ); + } + + /** + * @ticket 58157 + */ + public function test_get_the_modified_author_should_return_empty_string_if_user_id_does_not_exist() { + update_post_meta( self::$post_id, '_edit_last', -1 ); + + $this->assertSame( '', get_the_modified_author() ); + } + + /** + * @ticket 64104 + */ + public function test_get_the_modified_author_when_post_global_does_not_exist() { + $GLOBALS['post'] = null; + $this->assertNull( get_the_modified_author() ); + } + + /** + * @ticket 64104 + */ + public function test_get_the_modified_author_when_invalid_post() { + $this->assertNull( get_the_modified_author( -1 ) ); + } + + /** + * @ticket 64104 + */ + public function test_get_the_modified_author_for_another_post() { + $expected_display_name = 'Test Editor'; + + $editor_id = self::factory()->user->create( + array( + 'role' => 'editor', + 'user_login' => 'test_editor', + 'display_name' => $expected_display_name, + 'description' => 'test_editor', + ) + ); + + $another_post_id = self::factory()->post->create(); + + $this->assertNull( get_the_modified_author( $another_post_id ) ); + $this->assertNull( get_the_modified_author( get_post( $another_post_id ) ) ); + + add_post_meta( $another_post_id, '_edit_last', $editor_id ); + $this->assertSame( $expected_display_name, get_the_modified_author( $another_post_id ) ); + $this->assertSame( $expected_display_name, get_the_modified_author( get_post( $another_post_id ) ) ); + } +} diff --git a/tests/phpunit/tests/user/getUserCount.php b/tests/phpunit/tests/user/getUserCount.php new file mode 100644 index 0000000000000..5ec6e481a93e4 --- /dev/null +++ b/tests/phpunit/tests/user/getUserCount.php @@ -0,0 +1,164 @@ +network->create( + array( + 'domain' => 'wordpress.org', + 'path' => '/', + ) + ); + + delete_network_option( $different_network_id, 'user_count' ); + + wp_update_network_counts( $different_network_id ); + + $user_count = get_user_count( $different_network_id ); + + $this->assertGreaterThan( 0, $user_count ); + } + + /** + * @ticket 37866 + * @group multisite + * @group ms-required + */ + public function test_get_user_count_on_different_network() { + $different_network_id = self::factory()->network->create( + array( + 'domain' => 'wordpress.org', + 'path' => '/', + ) + ); + wp_update_network_user_counts(); + $current_network_user_count = get_user_count(); + + // Add another user to fake the network user count to be different. + wpmu_create_user( 'user', 'pass', 'user@example.com' ); + + wp_update_network_user_counts( $different_network_id ); + + $user_count = get_user_count( $different_network_id ); + + $this->assertSame( $current_network_user_count + 1, $user_count ); + } + + /** + * @ticket 22917 + * @group multisite + * @group ms-required + */ + public function test_enable_live_network_user_counts_filter() { + // False for large networks by default. + add_filter( 'enable_live_network_counts', '__return_false' ); + + // Refresh the cache. + wp_update_network_counts(); + $start_count = get_user_count(); + + wpmu_create_user( 'user', 'pass', 'user@example.com' ); + + // No change, cache not refreshed. + $count = get_user_count(); + + $this->assertSame( $start_count, $count ); + + wp_update_network_counts(); + $start_count = get_user_count(); + + add_filter( 'enable_live_network_counts', '__return_true' ); + + self::factory()->user->create( array( 'role' => 'administrator' ) ); + + $count = get_user_count(); + $this->assertSame( $start_count + 1, $count ); + } + + /** + * @ticket 38741 + */ + public function test_get_user_count_update() { + wp_update_user_counts(); + $current_network_user_count = get_user_count(); + + self::factory()->user->create( array( 'role' => 'administrator' ) ); + + $user_count = get_user_count(); + + $this->assertSame( $current_network_user_count + 1, $user_count ); + } + + /** + * @ticket 38741 + * @group ms-excluded + */ + public function test_get_user_count_update_on_delete() { + wp_update_user_counts(); + $current_network_user_count = get_user_count(); + + $u1 = self::factory()->user->create( array( 'role' => 'administrator' ) ); + + $user_count = get_user_count(); + + $this->assertSame( $current_network_user_count + 1, $user_count ); + + wp_delete_user( $u1 ); + + $user_count_after_delete = get_user_count(); + + $this->assertSame( $user_count - 1, $user_count_after_delete ); + } + + /** + * @ticket 38741 + * @group ms-required + */ + public function test_get_user_count_update_on_delete_multisite() { + wp_update_user_counts(); + $current_network_user_count = get_user_count(); + + $u1 = wpmu_create_user( 'user', 'pass', 'user@example.com' ); + + $user_count = get_user_count(); + + $this->assertSame( $current_network_user_count + 1, $user_count ); + + wpmu_delete_user( $u1 ); + + $user_count_after_delete = get_user_count(); + + $this->assertSame( $user_count - 1, $user_count_after_delete ); + } + + /** + * @ticket 38741 + * @group multisite + * @group ms-required + */ + public function test_get_user_count() { + // Refresh the cache. + wp_update_network_counts(); + $start_count = get_user_count(); + + // Only false for large networks as of 3.7. + add_filter( 'enable_live_network_counts', '__return_false' ); + self::factory()->user->create( array( 'role' => 'administrator' ) ); + + $count = get_user_count(); // No change, cache not refreshed. + $this->assertSame( $start_count, $count ); + + wp_update_network_counts(); // Magic happens here. + + $count = get_user_count(); + $this->assertSame( $start_count + 1, $count ); + } +} diff --git a/tests/phpunit/tests/user/listAuthors.php b/tests/phpunit/tests/user/listAuthors.php deleted file mode 100644 index b5daacfe50900..0000000000000 --- a/tests/phpunit/tests/user/listAuthors.php +++ /dev/null @@ -1,307 +0,0 @@ - 'name', - 'order' => 'ASC', - 'number' => null, - 'optioncount' => false, - 'exclude_admin' => true, - 'show_fullname' => false, - 'hide_empty' => true, - 'echo' => true, - 'feed' => [empty string], - 'feed_image' => [empty string], - 'feed_type' => [empty string], - 'style' => 'list', - 'html' => true ); - */ - public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { - self::$user_ids[] = $factory->user->create( - array( - 'user_login' => 'zack', - 'display_name' => 'zack', - 'role' => 'author', - 'first_name' => 'zack', - 'last_name' => 'moon', - ) - ); - self::$user_ids[] = $factory->user->create( - array( - 'user_login' => 'bob', - 'display_name' => 'bob', - 'role' => 'author', - 'first_name' => 'bob', - 'last_name' => 'reno', - ) - ); - self::$user_ids[] = $factory->user->create( - array( - 'user_login' => 'paul', - 'display_name' => 'paul', - 'role' => 'author', - 'first_name' => 'paul', - 'last_name' => 'norris', - ) - ); - self::$fred_id = $factory->user->create( - array( - 'user_login' => 'fred', - 'role' => 'author', - ) - ); - - $count = 0; - foreach ( self::$user_ids as $userid ) { - $count = $count + 1; - for ( $i = 0; $i < $count; $i++ ) { - self::$posts[] = $factory->post->create( - array( - 'post_type' => 'post', - 'post_author' => $userid, - ) - ); - } - - self::$user_urls[] = get_author_posts_url( $userid ); - } - } - - public function test_wp_list_authors_default() { - $expected['default'] = - '
    • bob
    • ' . - '
    • paul
    • ' . - '
    • zack
    • '; - - $this->assertSame( $expected['default'], wp_list_authors( array( 'echo' => false ) ) ); - } - - public function test_wp_list_authors_orderby() { - $expected['post_count'] = - '
    • zack
    • ' . - '
    • bob
    • ' . - '
    • paul
    • '; - - $this->assertSame( - $expected['post_count'], - wp_list_authors( - array( - 'echo' => false, - 'orderby' => 'post_count', - ) - ) - ); - } - - public function test_wp_list_authors_order() { - $expected['id'] = - '
    • paul
    • ' . - '
    • bob
    • ' . - '
    • zack
    • '; - - $this->assertSame( - $expected['id'], - wp_list_authors( - array( - 'echo' => false, - 'orderby' => 'id', - 'order' => 'DESC', - ) - ) - ); - } - - public function test_wp_list_authors_optioncount() { - $expected['optioncount'] = - '
    • bob (2)
    • ' . - '
    • paul (3)
    • ' . - '
    • zack (1)
    • '; - - $this->assertSame( - $expected['optioncount'], - wp_list_authors( - array( - 'echo' => false, - 'optioncount' => 1, - ) - ) - ); - } - - public function test_wp_list_authors_exclude_admin() { - self::factory()->post->create( - array( - 'post_type' => 'post', - 'post_author' => 1, - ) - ); - - $expected['exclude_admin'] = - '
    • admin
    • ' . - '
    • bob
    • ' . - '
    • paul
    • ' . - '
    • zack
    • '; - - $this->assertSame( - $expected['exclude_admin'], - wp_list_authors( - array( - 'echo' => false, - 'exclude_admin' => 0, - ) - ) - ); - } - - public function test_wp_list_authors_show_fullname() { - $expected['show_fullname'] = - '
    • bob reno
    • ' . - '
    • paul norris
    • ' . - '
    • zack moon
    • '; - - $this->assertSame( - $expected['show_fullname'], - wp_list_authors( - array( - 'echo' => false, - 'show_fullname' => 1, - ) - ) - ); - } - - public function test_wp_list_authors_hide_empty() { - $fred_id = self::$fred_id; - - $expected['hide_empty'] = - '
    • bob
    • ' . - '
    • fred
    • ' . - '
    • paul
    • ' . - '
    • zack
    • '; - - $this->assertSame( - $expected['hide_empty'], - wp_list_authors( - array( - 'echo' => false, - 'hide_empty' => 0, - ) - ) - ); - } - - public function test_wp_list_authors_echo() { - $expected['echo'] = - '
    • bob
    • ' . - '
    • paul
    • ' . - '
    • zack
    • '; - - $this->expectOutputString( $expected['echo'] ); - wp_list_authors( array( 'echo' => true ) ); - } - - public function test_wp_list_authors_feed() { - $url0 = get_author_feed_link( self::$user_ids[0] ); - $url1 = get_author_feed_link( self::$user_ids[1] ); - $url2 = get_author_feed_link( self::$user_ids[2] ); - - $expected['feed'] = - '
    • bob (link to feed)
    • ' . - '
    • paul (link to feed)
    • ' . - '
    • zack (link to feed)
    • '; - - $this->assertSame( - $expected['feed'], - wp_list_authors( - array( - 'echo' => false, - 'feed' => 'link to feed', - ) - ) - ); - } - - public function test_wp_list_authors_feed_image() { - $url0 = get_author_feed_link( self::$user_ids[0] ); - $url1 = get_author_feed_link( self::$user_ids[1] ); - $url2 = get_author_feed_link( self::$user_ids[2] ); - - $expected['feed_image'] = - '
    • bob
    • ' . - '
    • paul
    • ' . - '
    • zack
    • '; - - $this->assertSame( - $expected['feed_image'], - wp_list_authors( - array( - 'echo' => false, - 'feed_image' => WP_TESTS_DOMAIN . '/path/to/a/graphic.png', - ) - ) - ); - } - - /** - * @ticket 26538 - */ - public function test_wp_list_authors_feed_type() { - $url0 = get_author_feed_link( self::$user_ids[0], 'atom' ); - $url1 = get_author_feed_link( self::$user_ids[1], 'atom' ); - $url2 = get_author_feed_link( self::$user_ids[2], 'atom' ); - - $expected['feed_type'] = - '
    • bob (link to feed)
    • ' . - '
    • paul (link to feed)
    • ' . - '
    • zack (link to feed)
    • '; - - $this->assertSame( - $expected['feed_type'], - wp_list_authors( - array( - 'echo' => false, - 'feed' => 'link to feed', - 'feed_type' => 'atom', - ) - ) - ); - } - - public function test_wp_list_authors_style() { - $expected['style'] = - 'bob, ' . - 'paul, ' . - 'zack'; - - $this->assertSame( - $expected['style'], - wp_list_authors( - array( - 'echo' => false, - 'style' => 'none', - ) - ) - ); - } - - public function test_wp_list_authors_html() { - $expected['html'] = 'bob, paul, zack'; - - $this->assertSame( - $expected['html'], - wp_list_authors( - array( - 'echo' => false, - 'html' => 0, - ) - ) - ); - } -} diff --git a/tests/phpunit/tests/user/mapMetaCap.php b/tests/phpunit/tests/user/mapMetaCap.php index b45e6260a2857..b6c6b0ab8a151 100644 --- a/tests/phpunit/tests/user/mapMetaCap.php +++ b/tests/phpunit/tests/user/mapMetaCap.php @@ -3,6 +3,7 @@ /** * @group user * @group capabilities + * @covers ::map_meta_cap */ class Tests_User_MapMetaCap extends WP_UnitTestCase { @@ -356,7 +357,7 @@ public function test_file_edit_caps_not_reliant_on_unfiltered_html_constant() { * * @ticket 27020 */ - public function test_authorless_posts_capabilties() { + public function test_authorless_posts_capabilities() { $post_id = self::factory()->post->create( array( 'post_author' => 0, @@ -368,7 +369,6 @@ public function test_authorless_posts_capabilties() { $this->assertSame( array( 'edit_others_posts', 'edit_published_posts' ), map_meta_cap( 'edit_post', $editor, $post_id ) ); $this->assertSame( array( 'delete_others_posts', 'delete_published_posts' ), map_meta_cap( 'delete_post', $editor, $post_id ) ); - } /** @@ -410,4 +410,51 @@ public function test_only_users_who_can_manage_options_can_delete_page_for_posts $this->assertSame( array( 'manage_options' ), $caps ); } + + /** + * @dataProvider data_meta_caps_throw_doing_it_wrong_without_required_argument_provided + * @ticket 44591 + * + * @param string $cap The meta capability requiring an argument. + */ + public function test_meta_caps_throw_doing_it_wrong_without_required_argument_provided( $cap ) { + $admin_user = self::$user_id; + $this->setExpectedIncorrectUsage( 'map_meta_cap' ); + $this->assertContains( 'do_not_allow', map_meta_cap( $cap, $admin_user ) ); + } + + /** + * Data provider. + * + * @return array[] Test parameters { + * @type string $cap The meta capability requiring an argument. + * } + */ + public function data_meta_caps_throw_doing_it_wrong_without_required_argument_provided() { + return array( + array( 'delete_post' ), + array( 'delete_page' ), + array( 'edit_post' ), + array( 'edit_page' ), + array( 'read_post' ), + array( 'read_page' ), + array( 'publish_post' ), + array( 'edit_post_meta' ), + array( 'delete_post_meta' ), + array( 'add_post_meta' ), + array( 'edit_comment_meta' ), + array( 'delete_comment_meta' ), + array( 'add_comment_meta' ), + array( 'edit_term_meta' ), + array( 'delete_term_meta' ), + array( 'add_term_meta' ), + array( 'edit_user_meta' ), + array( 'delete_user_meta' ), + array( 'add_user_meta' ), + array( 'edit_comment' ), + array( 'edit_term' ), + array( 'delete_term' ), + array( 'assign_term' ), + ); + } } diff --git a/tests/phpunit/tests/user/multisite.php b/tests/phpunit/tests/user/multisite.php index adccd6044224e..d01856cb14c2f 100644 --- a/tests/phpunit/tests/user/multisite.php +++ b/tests/phpunit/tests/user/multisite.php @@ -1,454 +1,482 @@ user->create_and_get(); + $user2 = self::factory()->user->create_and_get(); - public function test_remove_user_from_blog() { - $user1 = self::factory()->user->create_and_get(); - $user2 = self::factory()->user->create_and_get(); + $post_id = self::factory()->post->create( array( 'post_author' => $user1->ID ) ); - $post_id = self::factory()->post->create( array( 'post_author' => $user1->ID ) ); + remove_user_from_blog( $user1->ID, 1, $user2->ID ); - remove_user_from_blog( $user1->ID, 1, $user2->ID ); + $post = get_post( $post_id ); - $post = get_post( $post_id ); + $this->assertNotEquals( $user1->ID, $post->post_author ); + $this->assertEquals( $user2->ID, $post->post_author ); + } - $this->assertNotEquals( $user1->ID, $post->post_author ); - $this->assertEquals( $user2->ID, $post->post_author ); + /** + * Test the returned data from get_blogs_of_user() + */ + public function test_get_blogs_of_user() { + $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + + // Maintain a list of 6 total sites and include the primary network site. + $blog_ids = self::factory()->blog->create_many( 5, array( 'user_id' => $user1_id ) ); + $blog_ids = array_merge( array( 1 ), $blog_ids ); + + // All sites are new and not marked as spam, archived, or deleted. + $blog_ids_of_user = array_keys( get_blogs_of_user( $user1_id ) ); + + // User should be a member of the created sites and the network's initial site. + $this->assertSame( $blog_ids, $blog_ids_of_user ); + + $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[0] ) ); + $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[2] ) ); + $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[4] ) ); + + unset( $blog_ids[0] ); + unset( $blog_ids[2] ); + unset( $blog_ids[4] ); + sort( $blog_ids ); + + $blogs_of_user = get_blogs_of_user( $user1_id, false ); + + // The user should still be a member of all remaining sites. + $blog_ids_of_user = array_keys( $blogs_of_user ); + $this->assertSame( $blog_ids, $blog_ids_of_user ); + + // Each site retrieved should match the expected structure. + foreach ( $blogs_of_user as $blog_id => $blog ) { + $this->assertSame( $blog_id, $blog->userblog_id ); + $this->assertObjectHasProperty( 'userblog_id', $blog ); + $this->assertObjectHasProperty( 'blogname', $blog ); + $this->assertObjectHasProperty( 'domain', $blog ); + $this->assertObjectHasProperty( 'path', $blog ); + $this->assertObjectHasProperty( 'site_id', $blog ); + $this->assertObjectHasProperty( 'siteurl', $blog ); + $this->assertObjectHasProperty( 'archived', $blog ); + $this->assertObjectHasProperty( 'spam', $blog ); + $this->assertObjectHasProperty( 'deleted', $blog ); } - /** - * Test the returned data from get_blogs_of_user() - */ - public function test_get_blogs_of_user() { - $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - - // Maintain a list of 6 total sites and include the primary network site. - $blog_ids = self::factory()->blog->create_many( 5, array( 'user_id' => $user1_id ) ); - $blog_ids = array_merge( array( 1 ), $blog_ids ); - - // All sites are new and not marked as spam, archived, or deleted. - $blog_ids_of_user = array_keys( get_blogs_of_user( $user1_id ) ); - - // User should be a member of the created sites and the network's initial site. - $this->assertSame( $blog_ids, $blog_ids_of_user ); - - $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[0] ) ); - $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[2] ) ); - $this->assertTrue( remove_user_from_blog( $user1_id, $blog_ids[4] ) ); - - unset( $blog_ids[0] ); - unset( $blog_ids[2] ); - unset( $blog_ids[4] ); - sort( $blog_ids ); - - $blogs_of_user = get_blogs_of_user( $user1_id, false ); - - // The user should still be a member of all remaining sites. - $blog_ids_of_user = array_keys( $blogs_of_user ); - $this->assertSame( $blog_ids, $blog_ids_of_user ); - - // Each site retrieved should match the expected structure. - foreach ( $blogs_of_user as $blog_id => $blog ) { - $this->assertSame( $blog_id, $blog->userblog_id ); - $this->assertObjectHasAttribute( 'userblog_id', $blog ); - $this->assertObjectHasAttribute( 'blogname', $blog ); - $this->assertObjectHasAttribute( 'domain', $blog ); - $this->assertObjectHasAttribute( 'path', $blog ); - $this->assertObjectHasAttribute( 'site_id', $blog ); - $this->assertObjectHasAttribute( 'siteurl', $blog ); - $this->assertObjectHasAttribute( 'archived', $blog ); - $this->assertObjectHasAttribute( 'spam', $blog ); - $this->assertObjectHasAttribute( 'deleted', $blog ); - } - - // Mark each remaining site as spam, archived, and deleted. - update_blog_details( $blog_ids[0], array( 'spam' => 1 ) ); - update_blog_details( $blog_ids[1], array( 'archived' => 1 ) ); - update_blog_details( $blog_ids[2], array( 'deleted' => 1 ) ); - - // Passing true as the second parameter should retrieve ALL sites, even if marked. - $blogs_of_user = get_blogs_of_user( $user1_id, true ); - $blog_ids_of_user = array_keys( $blogs_of_user ); - $this->assertSame( $blog_ids, $blog_ids_of_user ); - - // Check if sites are flagged as expected. - $this->assertEquals( 1, $blogs_of_user[ $blog_ids[0] ]->spam ); - $this->assertEquals( 1, $blogs_of_user[ $blog_ids[1] ]->archived ); - $this->assertEquals( 1, $blogs_of_user[ $blog_ids[2] ]->deleted ); - - unset( $blog_ids[0] ); - unset( $blog_ids[1] ); - unset( $blog_ids[2] ); - sort( $blog_ids ); - - // Passing false (the default) as the second parameter should retrieve only good sites. - $blog_ids_of_user = array_keys( get_blogs_of_user( $user1_id, false ) ); - $this->assertSame( $blog_ids, $blog_ids_of_user ); - } + // Mark each remaining site as spam, archived, and deleted. + update_blog_details( $blog_ids[0], array( 'spam' => 1 ) ); + update_blog_details( $blog_ids[1], array( 'archived' => 1 ) ); + update_blog_details( $blog_ids[2], array( 'deleted' => 1 ) ); + + // Passing true as the second parameter should retrieve ALL sites, even if marked. + $blogs_of_user = get_blogs_of_user( $user1_id, true ); + $blog_ids_of_user = array_keys( $blogs_of_user ); + $this->assertSame( $blog_ids, $blog_ids_of_user ); + + // Check if sites are flagged as expected. + $this->assertEquals( 1, $blogs_of_user[ $blog_ids[0] ]->spam ); + $this->assertEquals( 1, $blogs_of_user[ $blog_ids[1] ]->archived ); + $this->assertEquals( 1, $blogs_of_user[ $blog_ids[2] ]->deleted ); + + unset( $blog_ids[0] ); + unset( $blog_ids[1] ); + unset( $blog_ids[2] ); + sort( $blog_ids ); + + // Passing false (the default) as the second parameter should retrieve only good sites. + $blog_ids_of_user = array_keys( get_blogs_of_user( $user1_id, false ) ); + $this->assertSame( $blog_ids, $blog_ids_of_user ); + } - /** - * @expectedDeprecated is_blog_user - */ - public function test_is_blog_user() { - global $wpdb; + /** + * @expectedDeprecated is_blog_user + */ + public function test_is_blog_user() { + global $wpdb; - $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - $old_current = get_current_user_id(); - wp_set_current_user( $user1_id ); + $old_current = get_current_user_id(); + wp_set_current_user( $user1_id ); - $this->assertTrue( is_blog_user() ); - $this->assertTrue( is_blog_user( get_current_blog_id() ) ); + $this->assertTrue( is_blog_user() ); + $this->assertTrue( is_blog_user( get_current_blog_id() ) ); - $blog_id = self::factory()->blog->create( array( 'user_id' => get_current_user_id() ) ); + $blog_id = self::factory()->blog->create( array( 'user_id' => get_current_user_id() ) ); - $this->assertIsInt( $blog_id ); - $this->assertTrue( is_blog_user( $blog_id ) ); - $this->assertTrue( remove_user_from_blog( $user1_id, $blog_id ) ); - $this->assertFalse( is_blog_user( $blog_id ) ); + $this->assertIsInt( $blog_id ); + $this->assertTrue( is_blog_user( $blog_id ) ); + $this->assertTrue( remove_user_from_blog( $user1_id, $blog_id ) ); + $this->assertFalse( is_blog_user( $blog_id ) ); - wp_set_current_user( $old_current ); - } + wp_set_current_user( $old_current ); + } - public function test_is_user_member_of_blog() { - global $wpdb; + public function test_is_user_member_of_blog() { + global $wpdb; - $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - $user2_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $user1_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); + $user2_id = self::factory()->user->create( array( 'role' => 'administrator' ) ); - $old_current = get_current_user_id(); + $old_current = get_current_user_id(); - $this->assertSame( 0, $old_current ); + $this->assertSame( 0, $old_current ); - // Test for "get current user" when not logged in. - $this->assertFalse( is_user_member_of_blog() ); + // Test for "get current user" when not logged in. + $this->assertFalse( is_user_member_of_blog() ); - wp_set_current_user( $user1_id ); - $site_id = get_current_blog_id(); + wp_set_current_user( $user1_id ); + $site_id = get_current_blog_id(); - $this->assertTrue( is_user_member_of_blog() ); - $this->assertTrue( is_user_member_of_blog( 0, 0 ) ); - $this->assertTrue( is_user_member_of_blog( 0, $site_id ) ); - $this->assertTrue( is_user_member_of_blog( $user1_id ) ); - $this->assertTrue( is_user_member_of_blog( $user1_id, $site_id ) ); + $this->assertTrue( is_user_member_of_blog() ); + $this->assertTrue( is_user_member_of_blog( 0, 0 ) ); + $this->assertTrue( is_user_member_of_blog( 0, $site_id ) ); + $this->assertTrue( is_user_member_of_blog( $user1_id ) ); + $this->assertTrue( is_user_member_of_blog( $user1_id, $site_id ) ); - $blog_id = self::factory()->blog->create( array( 'user_id' => get_current_user_id() ) ); + $blog_id = self::factory()->blog->create( array( 'user_id' => get_current_user_id() ) ); - $this->assertIsInt( $blog_id ); + $this->assertIsInt( $blog_id ); - // Current user gets added to new blogs. - $this->assertTrue( is_user_member_of_blog( $user1_id, $blog_id ) ); - // Other users should not. - $this->assertFalse( is_user_member_of_blog( $user2_id, $blog_id ) ); + // Current user gets added to new blogs. + $this->assertTrue( is_user_member_of_blog( $user1_id, $blog_id ) ); + // Other users should not. + $this->assertFalse( is_user_member_of_blog( $user2_id, $blog_id ) ); - switch_to_blog( $blog_id ); + switch_to_blog( $blog_id ); - $this->assertTrue( is_user_member_of_blog( $user1_id ) ); - $this->assertFalse( is_user_member_of_blog( $user2_id ) ); + $this->assertTrue( is_user_member_of_blog( $user1_id ) ); + $this->assertFalse( is_user_member_of_blog( $user2_id ) ); - // Remove user 1 from blog. - $this->assertTrue( remove_user_from_blog( $user1_id, $blog_id ) ); + // Remove user 1 from blog. + $this->assertTrue( remove_user_from_blog( $user1_id, $blog_id ) ); - // Add user 2 to blog. - $this->assertTrue( add_user_to_blog( $blog_id, $user2_id, 'subscriber' ) ); + // Add user 2 to blog. + $this->assertTrue( add_user_to_blog( $blog_id, $user2_id, 'subscriber' ) ); - $this->assertFalse( is_user_member_of_blog( $user1_id ) ); - $this->assertTrue( is_user_member_of_blog( $user2_id ) ); + $this->assertFalse( is_user_member_of_blog( $user1_id ) ); + $this->assertTrue( is_user_member_of_blog( $user2_id ) ); - restore_current_blog(); + restore_current_blog(); - $this->assertFalse( is_user_member_of_blog( $user1_id, $blog_id ) ); - $this->assertTrue( is_user_member_of_blog( $user2_id, $blog_id ) ); + $this->assertFalse( is_user_member_of_blog( $user1_id, $blog_id ) ); + $this->assertTrue( is_user_member_of_blog( $user2_id, $blog_id ) ); - wpmu_delete_user( $user1_id ); - $user = new WP_User( $user1_id ); - $this->assertFalse( $user->exists() ); - $this->assertFalse( is_user_member_of_blog( $user1_id ) ); + wpmu_delete_user( $user1_id ); + $user = new WP_User( $user1_id ); + $this->assertFalse( $user->exists() ); + $this->assertFalse( is_user_member_of_blog( $user1_id ) ); - wp_set_current_user( $old_current ); - } + wp_set_current_user( $old_current ); + } - /** - * @ticket 23192 - */ - public function test_is_user_spammy() { - $user_id = self::factory()->user->create( - array( - 'role' => 'author', - 'user_login' => 'testuser1', - ) - ); - - $spam_username = (string) $user_id; - $spam_user_id = self::factory()->user->create( - array( - 'role' => 'author', - 'user_login' => $spam_username, - ) - ); - wp_update_user( - array( - 'ID' => $spam_user_id, - 'spam' => '1', - ) - ); - - $this->assertTrue( is_user_spammy( $spam_username ) ); - $this->assertFalse( is_user_spammy( 'testuser1' ) ); - } + /** + * @ticket 23192 + */ + public function test_is_user_spammy() { + $user_id = self::factory()->user->create( + array( + 'role' => 'author', + 'user_login' => 'testuser1', + ) + ); + + $spam_username = (string) $user_id; + $spam_user_id = self::factory()->user->create( + array( + 'role' => 'author', + 'user_login' => $spam_username, + ) + ); + wp_update_user( + array( + 'ID' => $spam_user_id, + 'spam' => '1', + ) + ); + + $this->assertTrue( is_user_spammy( $spam_username ) ); + $this->assertFalse( is_user_spammy( 'testuser1' ) ); + } - /** - * @ticket 20601 - */ - public function test_user_member_of_blog() { - global $wp_rewrite; + /** + * @ticket 20601 + */ + public function test_user_member_of_blog() { + global $wp_rewrite; + + self::factory()->blog->create(); + $user_id = self::factory()->user->create(); + self::factory()->blog->create( array( 'user_id' => $user_id ) ); - self::factory()->blog->create(); - $user_id = self::factory()->user->create(); - self::factory()->blog->create( array( 'user_id' => $user_id ) ); + $blogs = get_blogs_of_user( $user_id ); + $this->assertCount( 2, $blogs ); + $first = reset( $blogs )->userblog_id; + remove_user_from_blog( $user_id, $first ); - $blogs = get_blogs_of_user( $user_id ); - $this->assertCount( 2, $blogs ); - $first = reset( $blogs )->userblog_id; - remove_user_from_blog( $user_id, $first ); + $blogs = get_blogs_of_user( $user_id ); + $second = reset( $blogs )->userblog_id; + $this->assertCount( 1, $blogs ); - $blogs = get_blogs_of_user( $user_id ); - $second = reset( $blogs )->userblog_id; - $this->assertCount( 1, $blogs ); + switch_to_blog( $first ); + $wp_rewrite->init(); - switch_to_blog( $first ); - $wp_rewrite->init(); + $this->go_to( get_author_posts_url( $user_id ) ); + $this->assertQueryTrue( 'is_404' ); - $this->go_to( get_author_posts_url( $user_id ) ); - $this->assertQueryTrue( 'is_404' ); + switch_to_blog( $second ); + $wp_rewrite->init(); - switch_to_blog( $second ); - $wp_rewrite->init(); + $this->go_to( get_author_posts_url( $user_id ) ); + $this->assertQueryTrue( 'is_author', 'is_archive' ); - $this->go_to( get_author_posts_url( $user_id ) ); - $this->assertQueryTrue( 'is_author', 'is_archive' ); + add_user_to_blog( $first, $user_id, 'administrator' ); + $blogs = get_blogs_of_user( $user_id ); + $this->assertCount( 2, $blogs ); - add_user_to_blog( $first, $user_id, 'administrator' ); - $blogs = get_blogs_of_user( $user_id ); - $this->assertCount( 2, $blogs ); + switch_to_blog( $first ); + $wp_rewrite->init(); - switch_to_blog( $first ); - $wp_rewrite->init(); + $this->go_to( get_author_posts_url( $user_id ) ); + $this->assertQueryTrue( 'is_author', 'is_archive' ); + } - $this->go_to( get_author_posts_url( $user_id ) ); - $this->assertQueryTrue( 'is_author', 'is_archive' ); + public function test_revoked_super_admin_can_be_deleted() { + if ( isset( $GLOBALS['super_admins'] ) ) { + $old_global = $GLOBALS['super_admins']; + unset( $GLOBALS['super_admins'] ); } - public function test_revoked_super_admin_can_be_deleted() { - if ( isset( $GLOBALS['super_admins'] ) ) { - $old_global = $GLOBALS['super_admins']; - unset( $GLOBALS['super_admins'] ); - } + $user_id = self::factory()->user->create(); + grant_super_admin( $user_id ); + revoke_super_admin( $user_id ); - $user_id = self::factory()->user->create(); - grant_super_admin( $user_id ); - revoke_super_admin( $user_id ); + $this->assertTrue( wpmu_delete_user( $user_id ) ); - $this->assertTrue( wpmu_delete_user( $user_id ) ); + if ( isset( $old_global ) ) { + $GLOBALS['super_admins'] = $old_global; + } + } - if ( isset( $old_global ) ) { - $GLOBALS['super_admins'] = $old_global; - } + public function test_revoked_super_admin_is_deleted() { + if ( isset( $GLOBALS['super_admins'] ) ) { + $old_global = $GLOBALS['super_admins']; + unset( $GLOBALS['super_admins'] ); } - public function test_revoked_super_admin_is_deleted() { - if ( isset( $GLOBALS['super_admins'] ) ) { - $old_global = $GLOBALS['super_admins']; - unset( $GLOBALS['super_admins'] ); - } + $user_id = self::factory()->user->create(); + grant_super_admin( $user_id ); + revoke_super_admin( $user_id ); + wpmu_delete_user( $user_id ); + $user = new WP_User( $user_id ); - $user_id = self::factory()->user->create(); - grant_super_admin( $user_id ); - revoke_super_admin( $user_id ); - wpmu_delete_user( $user_id ); - $user = new WP_User( $user_id ); + $this->assertFalse( $user->exists(), 'WP_User->exists' ); - $this->assertFalse( $user->exists(), 'WP_User->exists' ); + if ( isset( $old_global ) ) { + $GLOBALS['super_admins'] = $old_global; + } + } - if ( isset( $old_global ) ) { - $GLOBALS['super_admins'] = $old_global; - } + public function test_super_admin_cannot_be_deleted() { + if ( isset( $GLOBALS['super_admins'] ) ) { + $old_global = $GLOBALS['super_admins']; + unset( $GLOBALS['super_admins'] ); } - public function test_super_admin_cannot_be_deleted() { - if ( isset( $GLOBALS['super_admins'] ) ) { - $old_global = $GLOBALS['super_admins']; - unset( $GLOBALS['super_admins'] ); - } + $user_id = self::factory()->user->create(); + grant_super_admin( $user_id ); - $user_id = self::factory()->user->create(); - grant_super_admin( $user_id ); + $this->assertFalse( wpmu_delete_user( $user_id ) ); - $this->assertFalse( wpmu_delete_user( $user_id ) ); + if ( isset( $old_global ) ) { + $GLOBALS['super_admins'] = $old_global; + } + } - if ( isset( $old_global ) ) { - $GLOBALS['super_admins'] = $old_global; - } + /** + * @ticket 27205 + */ + public function test_granting_super_admins() { + if ( isset( $GLOBALS['super_admins'] ) ) { + $old_global = $GLOBALS['super_admins']; + unset( $GLOBALS['super_admins'] ); } - /** - * @ticket 27205 - */ - public function test_granting_super_admins() { - if ( isset( $GLOBALS['super_admins'] ) ) { - $old_global = $GLOBALS['super_admins']; - unset( $GLOBALS['super_admins'] ); - } - - $user_id = self::factory()->user->create(); - - $this->assertFalse( is_super_admin( $user_id ) ); - $this->assertFalse( revoke_super_admin( $user_id ) ); - $this->assertTrue( grant_super_admin( $user_id ) ); - $this->assertTrue( is_super_admin( $user_id ) ); - $this->assertFalse( grant_super_admin( $user_id ) ); - $this->assertTrue( revoke_super_admin( $user_id ) ); - - // None of these operations should set the $super_admins global. - $this->assertFalse( isset( $GLOBALS['super_admins'] ) ); - - // Try with two users. - $second_user = self::factory()->user->create(); - $this->assertTrue( grant_super_admin( $user_id ) ); - $this->assertTrue( grant_super_admin( $second_user ) ); - $this->assertTrue( is_super_admin( $second_user ) ); - $this->assertTrue( is_super_admin( $user_id ) ); - $this->assertTrue( revoke_super_admin( $user_id ) ); - $this->assertTrue( revoke_super_admin( $second_user ) ); - - if ( isset( $old_global ) ) { - $GLOBALS['super_admins'] = $old_global; - } + $user_id = self::factory()->user->create(); + + $this->assertFalse( is_super_admin( $user_id ) ); + $this->assertFalse( revoke_super_admin( $user_id ) ); + $this->assertTrue( grant_super_admin( $user_id ) ); + $this->assertTrue( is_super_admin( $user_id ) ); + $this->assertFalse( grant_super_admin( $user_id ) ); + $this->assertTrue( revoke_super_admin( $user_id ) ); + + // None of these operations should set the $super_admins global. + $this->assertFalse( isset( $GLOBALS['super_admins'] ) ); + + // Try with two users. + $second_user = self::factory()->user->create(); + $this->assertTrue( grant_super_admin( $user_id ) ); + $this->assertTrue( grant_super_admin( $second_user ) ); + $this->assertTrue( is_super_admin( $second_user ) ); + $this->assertTrue( is_super_admin( $user_id ) ); + $this->assertTrue( revoke_super_admin( $user_id ) ); + $this->assertTrue( revoke_super_admin( $second_user ) ); + + if ( isset( $old_global ) ) { + $GLOBALS['super_admins'] = $old_global; } + } - public function test_numeric_string_user_id() { - $u = self::factory()->user->create(); + public function test_numeric_string_user_id() { + $u = self::factory()->user->create(); - $u_string = (string) $u; - $this->assertTrue( wpmu_delete_user( $u_string ) ); - $this->assertFalse( get_user_by( 'id', $u ) ); - } + $u_string = (string) $u; + $this->assertTrue( wpmu_delete_user( $u_string ) ); + $this->assertFalse( get_user_by( 'id', $u ) ); + } - /** - * @ticket 33800 - */ - public function test_should_return_false_for_non_numeric_string_user_id() { - $this->assertFalse( wpmu_delete_user( 'abcde' ) ); - } + /** + * @ticket 33800 + */ + public function test_should_return_false_for_non_numeric_string_user_id() { + $this->assertFalse( wpmu_delete_user( 'abcde' ) ); + } - /** - * @ticket 33800 - */ - public function test_should_return_false_for_object_user_id() { - $u_obj = self::factory()->user->create_and_get(); - $this->assertFalse( wpmu_delete_user( $u_obj ) ); - $this->assertSame( $u_obj->ID, username_exists( $u_obj->user_login ) ); - } + /** + * @ticket 33800 + */ + public function test_should_return_false_for_object_user_id() { + $u_obj = self::factory()->user->create_and_get(); + $this->assertFalse( wpmu_delete_user( $u_obj ) ); + $this->assertSame( $u_obj->ID, username_exists( $u_obj->user_login ) ); + } - /** - * @ticket 38356 - */ - public function test_add_user_to_blog_subscriber() { - $site_id = self::factory()->blog->create(); - $user_id = self::factory()->user->create(); + /** + * @ticket 38356 + */ + public function test_add_user_to_blog_subscriber() { + $site_id = self::factory()->blog->create(); + $user_id = self::factory()->user->create(); - add_user_to_blog( $site_id, $user_id, 'subscriber' ); + add_user_to_blog( $site_id, $user_id, 'subscriber' ); - switch_to_blog( $site_id ); - $user = get_user_by( 'id', $user_id ); - restore_current_blog(); + switch_to_blog( $site_id ); + $user = get_user_by( 'id', $user_id ); + restore_current_blog(); - wp_delete_site( $site_id ); - wpmu_delete_user( $user_id ); + wp_delete_site( $site_id ); + wpmu_delete_user( $user_id ); - $this->assertContains( 'subscriber', $user->roles ); - } + $this->assertContains( 'subscriber', $user->roles ); + } - /** - * @ticket 38356 - */ - public function test_add_user_to_blog_invalid_user() { - global $wpdb; + /** + * @ticket 38356 + */ + public function test_add_user_to_blog_invalid_user() { + global $wpdb; - $site_id = self::factory()->blog->create(); + $site_id = self::factory()->blog->create(); - $suppress = $wpdb->suppress_errors(); - $result = add_user_to_blog( 73622, $site_id, 'subscriber' ); - $wpdb->suppress_errors( $suppress ); + $suppress = $wpdb->suppress_errors(); + $result = add_user_to_blog( 73622, $site_id, 'subscriber' ); + $wpdb->suppress_errors( $suppress ); - wp_delete_site( $site_id ); + wp_delete_site( $site_id ); - $this->assertWPError( $result ); - } + $this->assertWPError( $result ); + } - /** - * @ticket 41101 - */ - public function test_should_fail_can_add_user_to_blog_filter() { - $site_id = self::factory()->blog->create(); - $user_id = self::factory()->user->create(); + /** + * @ticket 41101 + */ + public function test_should_fail_can_add_user_to_blog_filter() { + $site_id = self::factory()->blog->create(); + $user_id = self::factory()->user->create(); - add_filter( 'can_add_user_to_blog', '__return_false' ); - $result = add_user_to_blog( $site_id, $user_id, 'subscriber' ); + add_filter( 'can_add_user_to_blog', '__return_false' ); + $result = add_user_to_blog( $site_id, $user_id, 'subscriber' ); - $this->assertWPError( $result ); - } + $this->assertWPError( $result ); + } - /** - * @ticket 41101 - */ - public function test_should_succeed_can_add_user_to_blog_filter() { - $site_id = self::factory()->blog->create(); - $user_id = self::factory()->user->create(); + /** + * @ticket 41101 + */ + public function test_should_succeed_can_add_user_to_blog_filter() { + $site_id = self::factory()->blog->create(); + $user_id = self::factory()->user->create(); - add_filter( 'can_add_user_to_blog', '__return_true' ); - $result = add_user_to_blog( $site_id, $user_id, 'subscriber' ); + add_filter( 'can_add_user_to_blog', '__return_true' ); + $result = add_user_to_blog( $site_id, $user_id, 'subscriber' ); - $this->assertTrue( $result ); - } + $this->assertTrue( $result ); + } - /** - * @ticket 23016 - */ - public function test_wp_roles_global_is_reset() { - global $wp_roles; - $role = 'test_global_is_reset'; - $role_name = 'Test Global Is Reset'; - $blog_id = self::factory()->blog->create(); + /** + * @ticket 23016 + */ + public function test_wp_roles_global_is_reset() { + global $wp_roles; + $role = 'test_global_is_reset'; + $role_name = 'Test Global Is Reset'; + $blog_id = self::factory()->blog->create(); - $wp_roles->add_role( $role, $role_name, array() ); + $wp_roles->add_role( $role, $role_name, array() ); - $this->assertNotEmpty( $wp_roles->get_role( $role ) ); + $this->assertNotEmpty( $wp_roles->get_role( $role ) ); - switch_to_blog( $blog_id ); + switch_to_blog( $blog_id ); - $this->assertEmpty( $wp_roles->get_role( $role ) ); + $this->assertEmpty( $wp_roles->get_role( $role ) ); - $wp_roles->add_role( $role, $role_name, array() ); + $wp_roles->add_role( $role, $role_name, array() ); - $this->assertNotEmpty( $wp_roles->get_role( $role ) ); + $this->assertNotEmpty( $wp_roles->get_role( $role ) ); - restore_current_blog(); + restore_current_blog(); - $this->assertNotEmpty( $wp_roles->get_role( $role ) ); + $this->assertNotEmpty( $wp_roles->get_role( $role ) ); - $wp_roles->remove_role( $role ); + $wp_roles->remove_role( $role ); + } + + /** + * @ticket 39170 + */ + public function test_revoke_super_admin_with_network_email() { + if ( isset( $GLOBALS['super_admins'] ) ) { + $old_global = $GLOBALS['super_admins']; + unset( $GLOBALS['super_admins'] ); } - } + $old_network_email = get_site_option( 'admin_email' ); + $email_address = 'superadmin333@example.org'; + + $user_id = self::factory()->user->create( + array( + 'user_email' => $email_address, + ) + ); + + grant_super_admin( $user_id ); + update_site_option( 'admin_email', $email_address ); + + $result = revoke_super_admin( $user_id ); -endif; + update_site_option( 'admin_email', $old_network_email ); + + if ( isset( $old_global ) ) { + $GLOBALS['super_admins'] = $old_global; + } + + $this->assertTrue( $result ); + } +} diff --git a/tests/phpunit/tests/user/passwordHash.php b/tests/phpunit/tests/user/passwordHash.php new file mode 100644 index 0000000000000..c09fd91691efd --- /dev/null +++ b/tests/phpunit/tests/user/passwordHash.php @@ -0,0 +1,37 @@ +gensalt_blowfish( 'a password string' ); + } +} diff --git a/tests/phpunit/tests/user/query.php b/tests/phpunit/tests/user/query.php index 1c17d3d09c04d..bb6d9e391f4a9 100644 --- a/tests/phpunit/tests/user/query.php +++ b/tests/phpunit/tests/user/query.php @@ -1,6 +1,6 @@ assertEquals( '', $users->get( 'fields' ) ); + $this->assertNull( $users->get( 'fields' ) ); if ( isset( $users->query_vars['fields'] ) ) { $this->assertSame( '', $users->query_vars['fields'] ); } @@ -155,6 +155,26 @@ public function test_get_all() { } } + /** + * @ticket 55594 + */ + public function test_get_all_primed_users() { + $filter = new MockAction(); + add_filter( 'update_user_metadata_cache', array( $filter, 'filter' ), 10, 2 ); + + new WP_User_Query( + array( + 'include' => self::$author_ids, + 'fields' => 'all', + ) + ); + + $args = $filter->get_args(); + $last_args = end( $args ); + $this->assertIsArray( $last_args[1] ); + $this->assertSameSets( self::$author_ids, $last_args[1], 'Ensure that user meta is primed' ); + } + /** * @ticket 39297 */ @@ -166,7 +186,7 @@ public function test_get_total_is_int() { } /** - * @dataProvider orderby_should_convert_non_prefixed_keys_data + * @dataProvider data_orderby_should_convert_non_prefixed_keys */ public function test_orderby_should_convert_non_prefixed_keys( $short_key, $full_key ) { $q = new WP_User_Query( @@ -178,7 +198,7 @@ public function test_orderby_should_convert_non_prefixed_keys( $short_key, $full $this->assertStringContainsString( "ORDER BY $full_key", $q->query_orderby ); } - public function orderby_should_convert_non_prefixed_keys_data() { + public function data_orderby_should_convert_non_prefixed_keys() { return array( array( 'nicename', 'user_nicename' ), array( 'email', 'user_email' ), @@ -198,7 +218,7 @@ public function test_orderby_meta_value() { 'include' => self::$author_ids, 'meta_key' => 'last_name', 'orderby' => 'meta_value', - 'fields' => 'ids', + 'fields' => 'ID', ) ); @@ -220,7 +240,7 @@ public function test_orderby_meta_value_num() { 'include' => self::$author_ids, 'meta_key' => 'user_age', 'orderby' => 'meta_value_num', - 'fields' => 'ids', + 'fields' => 'ID', ) ); @@ -242,7 +262,7 @@ public function test_orderby_somekey_where_meta_key_is_somekey() { 'include' => self::$author_ids, 'meta_key' => 'foo', 'orderby' => 'foo', - 'fields' => 'ids', + 'fields' => 'ID', ) ); @@ -261,7 +281,7 @@ public function test_orderby_clause_key() { $q = new WP_User_Query( array( - 'fields' => 'ids', + 'fields' => 'ID', 'meta_query' => array( 'foo_key' => array( 'key' => 'foo', @@ -302,7 +322,7 @@ public function test_orderby_clause_key_as_secondary_sort() { $q = new WP_User_Query( array( - 'fields' => 'ids', + 'fields' => 'ID', 'meta_query' => array( 'foo_key' => array( 'key' => 'foo', @@ -332,7 +352,7 @@ public function test_orderby_more_than_one_clause_key() { $q = new WP_User_Query( array( - 'fields' => 'ids', + 'fields' => 'ID', 'meta_query' => array( 'foo_key' => array( 'key' => 'foo', @@ -562,7 +582,7 @@ public function test_prepare_query() { // All values get reset. $query->prepare_query( array( 'fields' => 'all' ) ); $this->assertEmpty( $query->query_limit ); - $this->assertEquals( '', $query->query_limit ); + $this->assertNull( $query->query_limit ); $_query_vars = $query->query_vars; $query->prepare_query(); @@ -1281,7 +1301,7 @@ public function test_paged() { 'paged' => 2, 'orderby' => 'ID', 'order' => 'DESC', // Avoid funkiness with user 1. - 'fields' => 'ids', + 'fields' => 'ID', ) ); @@ -1301,7 +1321,6 @@ public function test_query_vars_should_be_filled_in_after_pre_get_users() { foreach ( $query_vars as $query_var ) { $this->assertArrayHasKey( $query_var, $q->query_vars, "$query_var does not exist." ); } - } public function filter_pre_get_users_args( $q ) { @@ -1357,7 +1376,7 @@ public function test_get_single_role_by_string_which_is_similar() { $users = get_users( array( 'role' => 'editor', - 'fields' => 'ids', + 'fields' => 'ID', ) ); @@ -1645,7 +1664,7 @@ public function test_calling_prepare_query_a_second_time_should_not_add_another_ */ public function test_search_by_display_name_only() { - $new_user1 = $this->factory->user->create( + $new_user1 = self::factory()->user->create( array( 'user_login' => 'name1', 'display_name' => 'Sophia Andresen', @@ -1665,7 +1684,7 @@ public function test_search_by_display_name_only() { $ids = $q->get_results(); // Must include user that has the same string in display_name. - $this->assertEquals( array( $new_user1 ), $ids ); + $this->assertSameSetsWithIndex( array( (string) $new_user1 ), $ids ); } /** @@ -1673,7 +1692,7 @@ public function test_search_by_display_name_only() { */ public function test_search_by_display_name_only_ignore_others() { - $new_user1 = $this->factory->user->create( + $new_user1 = self::factory()->user->create( array( 'user_login' => 'Sophia Andresen', 'display_name' => 'name1', @@ -1700,11 +1719,9 @@ public function test_search_by_display_name_only_ignore_others() { * @ticket 44169 */ public function test_users_pre_query_filter_should_bypass_database_query() { - global $wpdb; - add_filter( 'users_pre_query', array( __CLASS__, 'filter_users_pre_query' ), 10, 2 ); - $num_queries = $wpdb->num_queries; + $num_queries = get_num_queries(); $q = new WP_User_Query( array( 'fields' => 'ID', @@ -1714,7 +1731,7 @@ public function test_users_pre_query_filter_should_bypass_database_query() { remove_filter( 'users_pre_query', array( __CLASS__, 'filter_users_pre_query' ), 10, 2 ); // Make sure no queries were executed. - $this->assertSame( $num_queries, $wpdb->num_queries ); + $this->assertSame( $num_queries, get_num_queries() ); // We manually inserted a non-existing user and overrode the results with it. $this->assertSame( array( 555 ), $q->results ); @@ -1723,6 +1740,20 @@ public function test_users_pre_query_filter_should_bypass_database_query() { $this->assertSame( 1, $q->total_users ); } + /** + * @ticket 47719 + */ + public function test_include_should_return_no_users_when_0() { + $query = new WP_User_Query( + array( + 'role' => '', + 'include' => array( 0 ), + ) + ); + + $this->assertSame( array(), $query->get_results() ); + } + public static function filter_users_pre_query( $posts, $query ) { $query->total_users = 1; @@ -1966,4 +1997,412 @@ public function test_get_single_capability_multisite_blog_id() { $this->assertContains( self::$author_ids[1], $found ); $this->assertContains( self::$author_ids[2], $found ); } + + /** + * @ticket 53177 + * @dataProvider data_returning_field_subset_as_string + * + * @param string $field + * @param mixed $expected + */ + public function test_returning_field_subset_as_string( $field, $expected ) { + $q = new WP_User_Query( + array( + 'fields' => $field, + 'include' => array( '1' ), + ) + ); + $results = $q->get_results(); + + $this->assertSameSets( $expected, $results ); + } + + /** + * Data provider + * + * @return array + */ + public function data_returning_field_subset_as_string() { + $data = array( + 'id' => array( + 'fields' => 'id', + 'expected' => array( '1' ), + ), + 'ID' => array( + 'fields' => 'ID', + 'expected' => array( '1' ), + ), + 'user_login' => array( + 'fields' => 'user_login', + 'expected' => array( 'admin' ), + ), + 'user_nicename' => array( + 'fields' => 'user_nicename', + 'expected' => array( 'admin' ), + ), + 'user_email' => array( + 'fields' => 'user_email', + 'expected' => array( WP_TESTS_EMAIL ), + ), + 'user_url' => array( + 'fields' => 'user_url', + 'expected' => array( wp_guess_url() ), + ), + 'user_status' => array( + 'fields' => 'user_status', + 'expected' => array( '0' ), + ), + 'display_name' => array( + 'fields' => 'display_name', + 'expected' => array( 'admin' ), + ), + 'invalid_field' => array( + 'fields' => 'invalid_field', + 'expected' => array( '1' ), + ), + ); + + if ( is_multisite() ) { + $data['spam'] = array( + 'fields' => 'spam', + 'expected' => array( '0' ), + ); + $data['deleted'] = array( + 'fields' => 'deleted', + 'expected' => array( '0' ), + ); + } + + return $data; + } + + /** + * @ticket 53177 + * @dataProvider data_returning_field_subset_as_array + * + * @param array $field + * @param mixed $expected + */ + public function test_returning_field_subset_as_array( $field, $expected ) { + $q = new WP_User_Query( + array( + 'fields' => $field, + 'include' => array( '1' ), + ) + ); + $results = $q->get_results(); + + if ( isset( $results[0] ) && is_object( $results[0] ) ) { + $results = (array) $results[0]; + } + + $this->assertSameSetsWithIndex( $expected, $results ); + } + + /** + * Data provider + * + * @return array + */ + public function data_returning_field_subset_as_array() { + $data = array( + 'id' => array( + 'fields' => array( 'id' ), + 'expected' => array( + 'ID' => '1', + 'id' => '1', + ), + ), + 'ID' => array( + 'fields' => array( 'ID' ), + 'expected' => array( + 'ID' => '1', + 'id' => '1', + ), + ), + 'user_login' => array( + 'fields' => array( 'user_login' ), + 'expected' => array( 'user_login' => 'admin' ), + ), + 'user_nicename' => array( + 'fields' => array( 'user_nicename' ), + 'expected' => array( 'user_nicename' => 'admin' ), + ), + 'user_email' => array( + 'fields' => array( 'user_email' ), + 'expected' => array( 'user_email' => WP_TESTS_EMAIL ), + ), + 'user_url' => array( + 'fields' => array( 'user_url' ), + 'expected' => array( 'user_url' => wp_guess_url() ), + ), + 'user_status' => array( + 'fields' => array( 'user_status' ), + 'expected' => array( 'user_status' => '0' ), + ), + 'display_name' => array( + 'fields' => array( 'display_name' ), + 'expected' => array( 'display_name' => 'admin' ), + ), + 'invalid_field' => array( + 'fields' => array( 'invalid_field' ), + 'expected' => array( + 'ID' => '1', + 'id' => '1', + ), + ), + 'valid array inc id' => array( + 'fields' => array( 'display_name', 'user_email', 'id' ), + 'expected' => array( + 'display_name' => 'admin', + 'user_email' => WP_TESTS_EMAIL, + 'ID' => '1', + 'id' => '1', + ), + ), + 'valid array inc ID' => array( + 'fields' => array( 'display_name', 'user_email', 'ID' ), + 'expected' => array( + 'display_name' => 'admin', + 'user_email' => WP_TESTS_EMAIL, + 'ID' => '1', + 'id' => '1', + ), + ), + 'partly valid array' => array( + 'fields' => array( 'display_name', 'invalid_field' ), + 'expected' => array( 'display_name' => 'admin' ), + ), + ); + + if ( is_multisite() ) { + $data['spam'] = array( + 'fields' => array( 'spam' ), + 'expected' => array( 'spam' => '0' ), + ); + $data['deleted'] = array( + 'fields' => array( 'deleted' ), + 'expected' => array( 'deleted' => '0' ), + ); + } + + return $data; + } + + /** + * @ticket 53177 + */ + public function test_returning_field_all() { + $q = new WP_User_Query( + array( + 'fields' => 'all', + 'include' => array( '1' ), + ) + ); + $results = $q->get_results(); + $user_data = (array) $results[0]->data; + + $expected_results = array( + 'ID' => '1', + 'user_login' => 'admin', + 'user_nicename' => 'admin', + 'user_url' => wp_guess_url(), + 'user_email' => WP_TESTS_EMAIL, + 'user_activation_key' => '', + 'user_status' => '0', + 'display_name' => 'admin', + ); + + if ( is_multisite() ) { + $expected_results['spam'] = '0'; + $expected_results['deleted'] = '0'; + } + + // These change for each run. + unset( $user_data['user_pass'], $user_data['user_registered'] ); + + $this->assertSameSetsWithIndex( $expected_results, $user_data ); + $this->assertInstanceOf( 'WP_User', $results[0] ); + } + + /** + * @ticket 53177 + * + * @covers WP_User_Query::prepare_query + */ + public function test_returning_field_user_registered() { + $q = new WP_User_Query( + array( + 'fields' => 'user_registered', + 'include' => array( self::$admin_ids[0] ), + ) + ); + $results = $q->get_results(); + $this->assertNotFalse( DateTime::createFromFormat( 'Y-m-d H:i:s', $results[0] ) ); + } + + /** + * @dataProvider data_compat_fields + * @ticket 58897 + * + * @covers WP_User_Query::__get() + * + * @param string $property_name Property name to get. + * @param mixed $expected Expected value. + */ + public function test_should_get_compat_fields( $property_name, $expected ) { + $user_query = new WP_User_Query(); + + $this->assertSame( $expected, $user_query->$property_name ); + } + + /** + * @ticket 58897 + * + * @covers WP_User_Query::__get() + */ + public function test_should_throw_deprecation_when_getting_dynamic_property() { + $user_query = new WP_User_Query(); + + $this->expectDeprecation(); + $this->expectDeprecationMessage( + 'WP_User_Query::__get(): ' . + 'The property `undefined_property` is not declared. Getting a dynamic property is ' . + 'deprecated since version 6.4.0! Instead, declare the property on the class.' + ); + $this->assertNull( $user_query->undefined_property, 'Getting a dynamic property should return null from WP_User_Query::__get()' ); + } + + /** + * @dataProvider data_compat_fields + * @ticket 58897 + * + * @covers WP_User_Query::__set() + * + * @param string $property_name Property name to set. + */ + public function test_should_set_compat_fields( $property_name ) { + $user_query = new WP_User_Query(); + $value = uniqid(); + + $user_query->$property_name = $value; + $this->assertSame( $value, $user_query->$property_name ); + } + + /** + * @ticket 58897 + * + * @covers WP_User_Query::__set() + */ + public function test_should_throw_deprecation_when_setting_dynamic_property() { + $user_query = new WP_User_Query(); + + $this->expectDeprecation(); + $this->expectDeprecationMessage( + 'WP_User_Query::__set(): ' . + 'The property `undefined_property` is not declared. Setting a dynamic property is ' . + 'deprecated since version 6.4.0! Instead, declare the property on the class.' + ); + $user_query->undefined_property = 'some value'; + } + + /** + * @dataProvider data_compat_fields + * @ticket 58897 + * + * @covers WP_User_Query::__isset() + * + * @param string $property_name Property name to check. + * @param mixed $expected Expected value. + */ + public function test_should_isset_compat_fields( $property_name, $expected ) { + $user_query = new WP_User_Query(); + + $actual = isset( $user_query->$property_name ); + if ( is_null( $expected ) ) { + $this->assertFalse( $actual ); + } else { + $this->assertTrue( $actual ); + } + } + + /** + * @ticket 58897 + * + * @covers WP_User_Query::__isset() + */ + public function test_should_throw_deprecation_when_isset_of_dynamic_property() { + $user_query = new WP_User_Query(); + + $this->expectDeprecation(); + $this->expectDeprecationMessage( + 'WP_User_Query::__isset(): ' . + 'The property `undefined_property` is not declared. Checking `isset()` on a dynamic property ' . + 'is deprecated since version 6.4.0! Instead, declare the property on the class.' + ); + $this->assertFalse( isset( $user_query->undefined_property ), 'Checking a dynamic property should return false from WP_User_Query::__isset()' ); + } + + /** + * @dataProvider data_compat_fields + * @ticket 58897 + * + * @covers WP_User_Query::__unset() + * + * @param string $property_name Property name to unset. + */ + public function test_should_unset_compat_fields( $property_name ) { + $user_query = new WP_User_Query(); + + unset( $user_query->$property_name ); + $this->assertFalse( isset( $user_query->$property_name ) ); + } + + /** + * @ticket 58897 + * + * @covers WP_User_Query::__unset() + */ + public function test_should_throw_deprecation_when_unset_of_dynamic_property() { + $user_query = new WP_User_Query(); + + $this->expectDeprecation(); + $this->expectDeprecationMessage( + 'WP_User_Query::__unset(): ' . + 'A property `undefined_property` is not declared. Unsetting a dynamic property is ' . + 'deprecated since version 6.4.0! Instead, declare the property on the class.' + ); + unset( $user_query->undefined_property ); + } + + /** + * Data provider. + * + * @return array + */ + public function data_compat_fields() { + return array( + 'results' => array( + 'property_name' => 'results', + 'expected' => null, + ), + 'total_users' => array( + 'property_name' => 'total_users', + 'expected' => 0, + ), + ); + } + + /** + * @ticket 56841 + */ + public function test_query_does_not_have_leading_whitespace() { + $q = new WP_User_Query( + array( + 'number' => 2, + ) + ); + + $this->assertSame( ltrim( $q->request ), $q->request, 'The query has leading whitespace' ); + } } diff --git a/tests/phpunit/tests/user/queryCache.php b/tests/phpunit/tests/user/queryCache.php new file mode 100644 index 0000000000000..fb556bfa36b21 --- /dev/null +++ b/tests/phpunit/tests/user/queryCache.php @@ -0,0 +1,805 @@ +user->create_many( + 4, + array( + 'role' => 'author', + ) + ); + + self::$sub_ids = $factory->user->create_many( + 2, + array( + 'role' => 'subscriber', + ) + ); + + self::$editor_ids = $factory->user->create_many( + 3, + array( + 'role' => 'editor', + ) + ); + + self::$contrib_id = $factory->user->create( + array( + 'role' => 'contributor', + ) + ); + + self::$admin_ids = $factory->user->create_many( + 2, + array( + 'role' => 'administrator', + ) + ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_different_count() { + $args = array( + 'count_total' => true, + ); + + $query1 = new WP_User_Query( $args ); + $users1 = wp_list_pluck( $query1->get_results(), 'ID' ); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + + $args = array( + 'count_total' => false, + ); + + $query2 = new WP_User_Query( $args ); + $users2 = wp_list_pluck( $query2->get_results(), 'ID' ); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after, 'Assert that the number of queries is not equal' ); + $this->assertNotSame( $users_total1, $users_total2, 'Assert that totals do not match' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_results() { + $args = array( + 'cache_results' => true, + ); + + $query1 = new WP_User_Query( $args ); + $users1 = wp_list_pluck( $query1->get_results(), 'ID' ); + + $queries_before = get_num_queries(); + + $args = array( + 'cache_results' => false, + ); + + $query2 = new WP_User_Query( $args ); + $users2 = wp_list_pluck( $query2->get_results(), 'ID' ); + $queries_after = get_num_queries(); + + $this->assertNotSame( $queries_before, $queries_after, 'Assert that queries are run' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + * @expectedDeprecated WP_User_Query + */ + public function test_query_cache_who() { + $args = array( + 'who' => 'authors', + 'fields' => array( 'ID' ), + ); + + $query1 = new WP_User_Query( $args ); + $users1 = $query1->get_results(); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + $query2 = new WP_User_Query( $args ); + $users2 = $query2->get_results(); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertSame( $queries_before, $queries_after, 'No queries are expected run.' ); + $this->assertSame( $users_total1, $users_total2, 'Number of users returned us expected to match.' ); + $this->assertSameSets( $users1, $users2, 'Results of the query are expected to match.' ); + } + + /** + * @ticket 40613 + * @covers ::query + * @dataProvider data_query_cache + * @param array $args Optional. See WP_User_Query::prepare_query() + */ + public function test_query_cache( array $args ) { + $query1 = new WP_User_Query( $args ); + $users1 = $query1->get_results(); + $users_total1 = $query1->get_total(); + + $queries_before = get_num_queries(); + $query2 = new WP_User_Query( $args ); + $users2 = $query2->get_results(); + $users_total2 = $query2->get_total(); + $queries_after = get_num_queries(); + + $this->assertSame( 0, $queries_after - $queries_before, 'Assert that no queries are run' ); + $this->assertSame( $users_total1, $users_total2, 'Assert that totals do match' ); + $this->assertSameSets( $users1, $users2, 'Asset that results of query match' ); + } + + /** + * Data provider + * + * @return array + */ + public function data_query_cache() { + $data = array( + 'id' => array( + 'args' => array( 'fields' => array( 'id' ) ), + + ), + 'ID' => array( + 'args' => array( 'fields' => array( 'ID' ) ), + ), + 'user_login' => array( + 'args' => array( 'fields' => array( 'user_login' ) ), + ), + 'user_nicename' => array( + 'args' => array( 'fields' => array( 'user_nicename' ) ), + ), + 'user_email' => array( + 'args' => array( 'fields' => array( 'user_email' ) ), + ), + 'user_url' => array( + 'args' => array( 'fields' => array( 'user_url' ) ), + ), + 'user_status' => array( + 'args' => array( 'fields' => array( 'user_status' ) ), + ), + 'display_name' => array( + 'args' => array( 'fields' => array( 'display_name' ) ), + ), + 'invalid_field' => array( + 'args' => array( 'fields' => array( 'invalid_field' ) ), + ), + 'valid array inc id' => array( + 'args' => array( 'fields' => array( 'display_name', 'user_email', 'id' ) ), + ), + 'valid array inc ID' => array( + 'args' => array( 'fields' => array( 'display_name', 'user_email', 'ID' ) ), + ), + 'partly valid array' => array( + 'args' => array( 'fields' => array( 'display_name', 'invalid_field' ) ), + ), + 'orderby' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'orderby' => array( 'login', 'nicename' ), + ), + ), + 'meta query' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'meta_query' => array( + 'foo_key' => array( + 'key' => 'foo', + 'compare' => 'EXISTS', + ), + ), + 'orderby' => 'foo_key', + 'order' => 'DESC', + ), + ), + 'meta query LIKE' => array( + 'args' => array( + 'fields' => array( 'ID' ), + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => '00', + 'compare' => 'LIKE', + ), + ), + 'orderby' => 'foo_key', + 'order' => 'DESC', + ), + ), + 'published posts' => array( + 'args' => array( + 'has_published_posts' => true, + 'fields' => array( 'ID' ), + ), + ), + 'published posts order' => array( + 'args' => array( + 'orderby' => 'post_count', + 'fields' => array( 'ID' ), + ), + ), + 'published count_total' => array( + 'args' => array( + + 'count_total' => false, + 'fields' => array( 'ID' ), + ), + ), + 'capability' => array( + 'args' => array( + 'capability' => 'install_plugins', + 'fields' => array( 'ID' ), + ), + ), + 'include' => array( + 'args' => array( + 'includes' => self::$author_ids, + 'fields' => array( 'ID' ), + ), + ), + 'exclude' => array( + 'args' => array( + 'exclude' => self::$author_ids, + 'fields' => array( 'ID' ), + ), + ), + 'search' => array( + 'args' => array( + 'search' => 'User', + 'fields' => array( 'ID' ), + ), + ), + ); + + if ( is_multisite() ) { + $data['spam'] = array( + 'args' => array( 'fields' => array( 'spam' ) ), + ); + $data['deleted'] = array( + 'args' => array( 'fields' => array( 'deleted' ) ), + ); + } + + return $data; + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_remove_user_role() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + $q1 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user = get_user_by( 'id', $user_id ); + $user->remove_role( 'author' ); + + $q2 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_set_user_role() { + $user_id = self::factory()->user->create( array( 'role' => 'author' ) ); + + $q1 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user = get_user_by( 'id', $user_id ); + $user->set_role( 'editor' ); + + $q2 = new WP_User_Query( + array( + 'role' => 'author', + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_delete_user() { + $user_id = self::factory()->user->create(); + + $q1 = new WP_User_Query( + array( + 'include' => array( $user_id ), + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( $user_id ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + self::delete_user( $user_id ); + + $q2 = new WP_User_Query( + array( + 'include' => array( $user_id ), + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_do_not_cache() { + $user_id = self::factory()->user->create(); + + $args = array( + 'fields' => array( + 'user_login', + 'user_nicename', + 'user_email', + 'user_url', + 'user_status', + 'display_name', + ), + 'include' => array( $user_id ), + ); + + $q1 = new WP_User_Query( $args ); + $found1 = $q1->get_results(); + $callback = static function ( $user ) { + return (array) $user; + }; + + $found1 = array_map( $callback, $found1 ); + + $queries_before = get_num_queries(); + $q2 = new WP_User_Query( $args ); + $found2 = $q2->get_results(); + $found2 = array_map( $callback, $found2 ); + $queries_after = get_num_queries(); + + $this->assertSame( $queries_after - $queries_before, 2, 'Ensure that query is not cached' ); + $this->assertSameSets( $found1, $found2, 'Expected results to match.', 'Ensure that to results match' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_update_user() { + $user_id = end( self::$admin_ids ); + + wp_update_user( + array( + 'ID' => $user_id, + 'user_nicename' => 'paul', + ) + ); + + $args = array( + 'nicename__in' => array( 'paul' ), + ); + + $q1 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( $user_id ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + wp_update_user( + array( + 'ID' => $user_id, + 'user_nicename' => 'linda', + ) + ); + + $q2 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( $user_id, $found, 'Expected not to find author in returned values.' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_query_cache_create_user() { + $user_id = end( self::$admin_ids ); + + $args = array( 'blog_id' => get_current_blog_id() ); + + $q1 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertContains( $user_id, $found, 'Expected to find author in returned values.' ); + + $user_id_2 = self::factory()->user->create(); + + $q2 = new WP_User_Query( $args ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertContains( $user_id_2, $found, 'Find author in returned values' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_has_published_posts_delete_post() { + register_post_type( 'wptests_pt_public', array( 'public' => true ) ); + + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$author_ids[2], + 'post_status' => 'publish', + 'post_type' => 'wptests_pt_public', + ) + ); + + $q1 = new WP_User_Query( + array( + 'has_published_posts' => true, + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( self::$author_ids[2] ); + + $this->assertSameSets( $expected, $found, 'Find author in returned values' ); + + wp_delete_post( $post_id, true ); + + $q2 = new WP_User_Query( + array( + 'has_published_posts' => true, + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertSameSets( array(), $found, 'Not to find author in returned values' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_has_published_posts_delete_post_order() { + register_post_type( 'wptests_pt_public', array( 'public' => true ) ); + + $user_id = self::factory()->user->create(); + + $post_id = self::factory()->post->create( + array( + 'post_author' => $user_id, + 'post_status' => 'publish', + 'post_type' => 'wptests_pt_public', + ) + ); + + $q1 = new WP_User_Query( + array( + 'orderby' => 'post_count', + ) + ); + + $found1 = wp_list_pluck( $q1->get_results(), 'ID' ); + $this->assertContains( $user_id, $found1, 'Find author in returned values in first run of WP_User_Query' ); + + wp_delete_post( $post_id, true ); + + $q2 = new WP_User_Query( + array( + 'orderby' => 'post_count', + ) + ); + + $found2 = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertContains( $user_id, $found1, 'Find author in returned values in second run of WP_User_Query' ); + $this->assertSameSets( $found1, $found2, 'Not same order' ); + } + + /** + * @ticket 40613 + * @covers ::query + */ + public function test_meta_query_cache_invalidation() { + add_user_meta( self::$author_ids[0], 'foo', 'bar' ); + add_user_meta( self::$author_ids[1], 'foo', 'bar' ); + + $q1 = new WP_User_Query( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + $expected = array( self::$author_ids[0], self::$author_ids[1] ); + + $this->assertSameSets( $expected, $found, 'Asset that results contain authors' ); + + delete_user_meta( self::$author_ids[1], 'foo' ); + + $q2 = new WP_User_Query( + array( + 'meta_query' => array( + array( + 'key' => 'foo', + 'value' => 'bar', + ), + ), + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $expected = array( self::$author_ids[0] ); + + $this->assertSameSets( $expected, $found, 'Asset that results do not contain author without meta' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_get_single_capability_multisite_blog_id() { + $blog_id = self::factory()->blog->create(); + + add_user_to_blog( $blog_id, self::$author_ids[0], 'subscriber' ); + add_user_to_blog( $blog_id, self::$author_ids[1], 'author' ); + add_user_to_blog( $blog_id, self::$author_ids[2], 'editor' ); + + $q1 = new WP_User_Query( + array( + 'capability' => 'publish_posts', + 'blog_id' => $blog_id, + ) + ); + + $found = wp_list_pluck( $q1->get_results(), 'ID' ); + + $this->assertNotContains( self::$author_ids[0], $found, 'Asset that results do not contain author 0 without capability on site on first run' ); + $this->assertContains( self::$author_ids[1], $found, 'Asset that results do contain author with capability on site on first run' ); + $this->assertContains( self::$author_ids[2], $found, 'Asset that results do contain author with capability on site on first run' ); + + remove_user_from_blog( self::$author_ids[2], $blog_id ); + + $q2 = new WP_User_Query( + array( + 'capability' => 'publish_posts', + 'blog_id' => $blog_id, + ) + ); + + $found = wp_list_pluck( $q2->get_results(), 'ID' ); + $this->assertNotContains( self::$author_ids[0], $found, 'Asset that results do not contain author 0 without capability on site on second run' ); + $this->assertContains( self::$author_ids[1], $found, 'Asset that results do contain author with capability on site on second run' ); + $this->assertNotContains( self::$author_ids[2], $found, 'Asset that results do not contain author 1 without capability on site on second run' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_query_should_respect_blog_id() { + $blogs = self::factory()->blog->create_many( 2 ); + + add_user_to_blog( $blogs[0], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[0], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[2], 'author' ); + + $q = new WP_User_Query( + array( + 'fields' => 'ids', + 'blog_id' => $blogs[0], + ) + ); + + $expected = array( (string) self::$author_ids[0], (string) self::$author_ids[1] ); + + $this->assertSameSets( $expected, $q->get_results(), 'Asset that expected users return' ); + + $q = new WP_User_Query( + array( + 'fields' => 'ids', + 'blog_id' => $blogs[1], + ) + ); + + $expected = array( (string) self::$author_ids[0], (string) self::$author_ids[1], (string) self::$author_ids[2] ); + + $this->assertSameSets( $expected, $q->get_results(), 'Asset that expected users return from different blog' ); + } + + /** + * @ticket 40613 + * @group ms-required + * @covers ::query + */ + public function test_has_published_posts_should_respect_blog_id() { + $blogs = self::factory()->blog->create_many( 2 ); + + add_user_to_blog( $blogs[0], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[0], self::$author_ids[1], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[0], 'author' ); + add_user_to_blog( $blogs[1], self::$author_ids[1], 'author' ); + + switch_to_blog( $blogs[0] ); + self::factory()->post->create( + array( + 'post_author' => self::$author_ids[0], + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ); + restore_current_blog(); + + switch_to_blog( $blogs[1] ); + $post_id = self::factory()->post->create( + array( + 'post_author' => self::$author_ids[1], + 'post_status' => 'publish', + 'post_type' => 'post', + ) + ); + restore_current_blog(); + + $q = new WP_User_Query( + array( + 'has_published_posts' => array( 'post' ), + 'blog_id' => $blogs[1], + ) + ); + + $found = wp_list_pluck( $q->get_results(), 'ID' ); + $expected = array( self::$author_ids[1] ); + + $this->assertSameSets( $expected, $found, 'Asset that expected users returned with posts on this site' ); + switch_to_blog( $blogs[1] ); + wp_delete_post( $post_id, true ); + restore_current_blog(); + + $q = new WP_User_Query( + array( + 'has_published_posts' => array( 'post' ), + 'blog_id' => $blogs[1], + ) + ); + + $found = wp_list_pluck( $q->get_results(), 'ID' ); + + $this->assertSameSets( array(), $found, 'Asset that no users returned with posts on this site as posts have been deleted' ); + } + + /** + * Ensure cache keys are generated without WPDB placeholders. + * + * @ticket 40613 + * + * @covers ::generate_cache_key + */ + public function test_generate_cache_key_placeholder() { + global $wpdb; + $query1 = new WP_User_Query( array( 'capability' => 'edit_posts' ) ); + + $query_vars = $query1->query_vars; + $request_with_placeholder = $query1->request; + $request_without_placeholder = $wpdb->remove_placeholder_escape( $query1->request ); + + $reflection = new ReflectionMethod( $query1, 'generate_cache_key' ); + if ( PHP_VERSION_ID < 80100 ) { + $reflection->setAccessible( true ); + } + + $cache_key_1 = $reflection->invoke( $query1, $query_vars, $request_with_placeholder ); + $cache_key_2 = $reflection->invoke( $query1, $query_vars, $request_without_placeholder ); + + $this->assertSame( $cache_key_1, $cache_key_2, 'Cache key differs when using wpdb placeholder.' ); + } + + /** + * Verifies that generate_cache_key() does not throw a fatal error for switch_to_blog() + * with 'orderby' => 'post_count' and the deprecated 'who' => 'authors' parameter. + * + * @ticket 59011 + * @covers ::generate_cache_key + * + * @expectedDeprecated WP_User_Query + */ + public function test_generate_cache_key_with_orderby_post_count_and_deprecated_who_parameter() { + $query = new WP_User_Query( + array( + 'fields' => 'ID', + 'orderby' => 'post_count', + 'order' => 'DESC', + 'who' => 'authors', + ) + ); + + $this->assertNotEmpty( $query->get_results() ); + } +} diff --git a/tests/phpunit/tests/user/retrievePassword.php b/tests/phpunit/tests/user/retrievePassword.php new file mode 100644 index 0000000000000..da72bed8b70de --- /dev/null +++ b/tests/phpunit/tests/user/retrievePassword.php @@ -0,0 +1,109 @@ +user = self::factory()->user->create_and_get( + array( + 'user_login' => 'jane', + 'user_email' => 'r.jane@example.com', + ) + ); + } + + /** + * The function should not error when the email was sent. + * + * @ticket 54690 + */ + public function test_retrieve_password_reset_notification_email() { + $this->assertNotWPError( retrieve_password( $this->user->user_login ), 'Sending password reset notification email failed.' ); + } + + /** + * The function should error when the email was not sent. + * + * @ticket 54690 + */ + public function test_retrieve_password_should_return_wp_error_on_failed_email() { + add_filter( + 'retrieve_password_notification_email', + static function () { + return array( 'message' => '' ); + } + ); + + $this->assertWPError( retrieve_password( $this->user->user_login ), 'Sending password reset notification email succeeded.' ); + } + + /** + * @ticket 53634 + */ + public function test_retrieve_password_should_fetch_user_by_login_if_not_found_by_email() { + self::factory()->user->create( + array( + 'user_login' => 'foo@example.com', + 'user_email' => 'bar@example.com', + ) + ); + + $this->assertTrue( retrieve_password( 'foo@example.com' ), 'Fetching user by login failed.' ); + $this->assertTrue( retrieve_password( 'bar@example.com' ), 'Fetching user by email failed.' ); + } + + /** + * Tests that PHP 8.1 "passing null to non-nullable" deprecation notice + * is not thrown when the `$user_login` parameter is empty. + * + * The notice that we should not see: + * `Deprecated: trim(): Passing null to parameter #1 ($string) of type string is deprecated`. + * + * @ticket 62298 + */ + public function test_retrieve_password_does_not_throw_deprecation_notice_with_default_parameters() { + $this->assertWPError( retrieve_password() ); + } + + /** + * Tests that a fatal error is not thrown when the login passed via `$_POST` + * is an array instead of a string. + * + * The message that we should not see: + * `TypeError: trim(): Argument #1 ($string) must be of type string, array given`. + * + * @ticket 62794 + */ + public function test_retrieve_password_does_not_throw_fatal_error_with_array_parameters() { + $_POST['user_login'] = array( 'example' ); + + $error = retrieve_password(); + $this->assertWPError( $error, 'The result should be an instance of WP_Error.' ); + + $error_codes = $error->get_error_codes(); + $this->assertContains( 'empty_username', $error_codes, 'The "empty_username" error code should be present.' ); + } +} diff --git a/tests/phpunit/tests/user/session.php b/tests/phpunit/tests/user/session.php index 574c75ef4ad48..7bc1c39cbed3e 100644 --- a/tests/phpunit/tests/user/session.php +++ b/tests/phpunit/tests/user/session.php @@ -7,6 +7,11 @@ */ class Tests_User_Session extends WP_UnitTestCase { + /** + * @var WP_User_Meta_Session_Tokens + */ + private $manager; + public function set_up() { parent::set_up(); remove_all_filters( 'session_token_manager' ); diff --git a/tests/phpunit/tests/user/slashes.php b/tests/phpunit/tests/user/slashes.php index 91fc62b321feb..756dd89b2f967 100644 --- a/tests/phpunit/tests/user/slashes.php +++ b/tests/phpunit/tests/user/slashes.php @@ -6,6 +6,20 @@ * @ticket 21767 */ class Tests_User_Slashes extends WP_UnitTestCase { + + /* + * It is important to test with both even and odd numbered slashes, + * as KSES does a strip-then-add slashes in some of its function calls. + */ + + const SLASH_1 = 'String with 1 slash \\'; + const SLASH_2 = 'String with 2 slashes \\\\'; + const SLASH_3 = 'String with 3 slashes \\\\\\'; + const SLASH_4 = 'String with 4 slashes \\\\\\\\'; + const SLASH_5 = 'String with 5 slashes \\\\\\\\\\'; + const SLASH_6 = 'String with 6 slashes \\\\\\\\\\\\'; + const SLASH_7 = 'String with 7 slashes \\\\\\\\\\\\\\'; + protected static $author_id; protected static $user_id; @@ -18,16 +32,6 @@ public function set_up() { parent::set_up(); wp_set_current_user( self::$author_id ); - - // It is important to test with both even and odd numbered slashes, - // as KSES does a strip-then-add slashes in some of its function calls. - $this->slash_1 = 'String with 1 slash \\'; - $this->slash_2 = 'String with 2 slashes \\\\'; - $this->slash_3 = 'String with 3 slashes \\\\\\'; - $this->slash_4 = 'String with 4 slashes \\\\\\\\'; - $this->slash_5 = 'String with 5 slashes \\\\\\\\\\'; - $this->slash_6 = 'String with 6 slashes \\\\\\\\\\\\'; - $this->slash_7 = 'String with 7 slashes \\\\\\\\\\\\\\'; } /** @@ -42,22 +46,22 @@ public function test_add_user() { $_POST['pass2'] = 'password'; $_POST['role'] = 'subscriber'; $_POST['email'] = 'user1@example.com'; - $_POST['first_name'] = $this->slash_1; - $_POST['last_name'] = $this->slash_3; - $_POST['nickname'] = $this->slash_5; - $_POST['display_name'] = $this->slash_7; - $_POST['description'] = $this->slash_3; + $_POST['first_name'] = self::SLASH_1; + $_POST['last_name'] = self::SLASH_3; + $_POST['nickname'] = self::SLASH_5; + $_POST['display_name'] = self::SLASH_7; + $_POST['description'] = self::SLASH_3; $_POST = add_magic_quotes( $_POST ); // The add_user() function will strip slashes. $user_id = add_user(); $user = get_user_to_edit( $user_id ); - $this->assertSame( $this->slash_1, $user->first_name ); - $this->assertSame( $this->slash_3, $user->last_name ); - $this->assertSame( $this->slash_5, $user->nickname ); - $this->assertSame( $this->slash_7, $user->display_name ); - $this->assertSame( $this->slash_3, $user->description ); + $this->assertSame( self::SLASH_1, $user->first_name ); + $this->assertSame( self::SLASH_3, $user->last_name ); + $this->assertSame( self::SLASH_5, $user->nickname ); + $this->assertSame( self::SLASH_7, $user->display_name ); + $this->assertSame( self::SLASH_3, $user->description ); $_POST = array(); $_GET = array(); @@ -67,22 +71,22 @@ public function test_add_user() { $_POST['pass2'] = 'password'; $_POST['role'] = 'subscriber'; $_POST['email'] = 'user2@example.com'; - $_POST['first_name'] = $this->slash_2; - $_POST['last_name'] = $this->slash_4; - $_POST['nickname'] = $this->slash_6; - $_POST['display_name'] = $this->slash_2; - $_POST['description'] = $this->slash_4; + $_POST['first_name'] = self::SLASH_2; + $_POST['last_name'] = self::SLASH_4; + $_POST['nickname'] = self::SLASH_6; + $_POST['display_name'] = self::SLASH_2; + $_POST['description'] = self::SLASH_4; $_POST = add_magic_quotes( $_POST ); // The add_user() function will strip slashes. $user_id = add_user(); $user = get_user_to_edit( $user_id ); - $this->assertSame( $this->slash_2, $user->first_name ); - $this->assertSame( $this->slash_4, $user->last_name ); - $this->assertSame( $this->slash_6, $user->nickname ); - $this->assertSame( $this->slash_2, $user->display_name ); - $this->assertSame( $this->slash_4, $user->description ); + $this->assertSame( self::SLASH_2, $user->first_name ); + $this->assertSame( self::SLASH_4, $user->last_name ); + $this->assertSame( self::SLASH_6, $user->nickname ); + $this->assertSame( self::SLASH_2, $user->display_name ); + $this->assertSame( self::SLASH_4, $user->description ); } /** @@ -96,44 +100,44 @@ public function test_edit_user() { $_REQUEST = array(); $_POST['role'] = 'subscriber'; $_POST['email'] = 'user1@example.com'; - $_POST['first_name'] = $this->slash_1; - $_POST['last_name'] = $this->slash_3; - $_POST['nickname'] = $this->slash_5; - $_POST['display_name'] = $this->slash_7; - $_POST['description'] = $this->slash_3; + $_POST['first_name'] = self::SLASH_1; + $_POST['last_name'] = self::SLASH_3; + $_POST['nickname'] = self::SLASH_5; + $_POST['display_name'] = self::SLASH_7; + $_POST['description'] = self::SLASH_3; $_POST = add_magic_quotes( $_POST ); // The edit_user() function will strip slashes. $user_id = edit_user( $user_id ); $user = get_user_to_edit( $user_id ); - $this->assertSame( $this->slash_1, $user->first_name ); - $this->assertSame( $this->slash_3, $user->last_name ); - $this->assertSame( $this->slash_5, $user->nickname ); - $this->assertSame( $this->slash_7, $user->display_name ); - $this->assertSame( $this->slash_3, $user->description ); + $this->assertSame( self::SLASH_1, $user->first_name ); + $this->assertSame( self::SLASH_3, $user->last_name ); + $this->assertSame( self::SLASH_5, $user->nickname ); + $this->assertSame( self::SLASH_7, $user->display_name ); + $this->assertSame( self::SLASH_3, $user->description ); $_POST = array(); $_GET = array(); $_REQUEST = array(); $_POST['role'] = 'subscriber'; $_POST['email'] = 'user2@example.com'; - $_POST['first_name'] = $this->slash_2; - $_POST['last_name'] = $this->slash_4; - $_POST['nickname'] = $this->slash_6; - $_POST['display_name'] = $this->slash_2; - $_POST['description'] = $this->slash_4; + $_POST['first_name'] = self::SLASH_2; + $_POST['last_name'] = self::SLASH_4; + $_POST['nickname'] = self::SLASH_6; + $_POST['display_name'] = self::SLASH_2; + $_POST['description'] = self::SLASH_4; $_POST = add_magic_quotes( $_POST ); // The edit_user() function will strip slashes. $user_id = edit_user( $user_id ); $user = get_user_to_edit( $user_id ); - $this->assertSame( $this->slash_2, $user->first_name ); - $this->assertSame( $this->slash_4, $user->last_name ); - $this->assertSame( $this->slash_6, $user->nickname ); - $this->assertSame( $this->slash_2, $user->display_name ); - $this->assertSame( $this->slash_4, $user->description ); + $this->assertSame( self::SLASH_2, $user->first_name ); + $this->assertSame( self::SLASH_4, $user->last_name ); + $this->assertSame( self::SLASH_6, $user->nickname ); + $this->assertSame( self::SLASH_2, $user->display_name ); + $this->assertSame( self::SLASH_4, $user->description ); } /** @@ -144,43 +148,43 @@ public function test_wp_insert_user() { array( 'user_login' => 'slash_example_user_3', 'role' => 'subscriber', - 'email' => 'user3@example.com', - 'first_name' => $this->slash_1, - 'last_name' => $this->slash_3, - 'nickname' => $this->slash_5, - 'display_name' => $this->slash_7, - 'description' => $this->slash_3, - 'user_pass' => '', + 'user_email' => 'user3@example.com', + 'first_name' => self::SLASH_1, + 'last_name' => self::SLASH_3, + 'nickname' => self::SLASH_5, + 'display_name' => self::SLASH_7, + 'description' => self::SLASH_3, + 'user_pass' => 'password', ) ); $user = get_user_to_edit( $user_id ); - $this->assertSame( wp_unslash( $this->slash_1 ), $user->first_name ); - $this->assertSame( wp_unslash( $this->slash_3 ), $user->last_name ); - $this->assertSame( wp_unslash( $this->slash_5 ), $user->nickname ); - $this->assertSame( wp_unslash( $this->slash_7 ), $user->display_name ); - $this->assertSame( wp_unslash( $this->slash_3 ), $user->description ); + $this->assertSame( wp_unslash( self::SLASH_1 ), $user->first_name ); + $this->assertSame( wp_unslash( self::SLASH_3 ), $user->last_name ); + $this->assertSame( wp_unslash( self::SLASH_5 ), $user->nickname ); + $this->assertSame( wp_unslash( self::SLASH_7 ), $user->display_name ); + $this->assertSame( wp_unslash( self::SLASH_3 ), $user->description ); $user_id = wp_insert_user( array( 'user_login' => 'slash_example_user_4', 'role' => 'subscriber', - 'email' => 'user3@example.com', - 'first_name' => $this->slash_2, - 'last_name' => $this->slash_4, - 'nickname' => $this->slash_6, - 'display_name' => $this->slash_2, - 'description' => $this->slash_4, - 'user_pass' => '', + 'user_email' => 'user4@example.com', + 'first_name' => self::SLASH_2, + 'last_name' => self::SLASH_4, + 'nickname' => self::SLASH_6, + 'display_name' => self::SLASH_2, + 'description' => self::SLASH_4, + 'user_pass' => 'password', ) ); $user = get_user_to_edit( $user_id ); - $this->assertSame( wp_unslash( $this->slash_2 ), $user->first_name ); - $this->assertSame( wp_unslash( $this->slash_4 ), $user->last_name ); - $this->assertSame( wp_unslash( $this->slash_6 ), $user->nickname ); - $this->assertSame( wp_unslash( $this->slash_2 ), $user->display_name ); - $this->assertSame( wp_unslash( $this->slash_4 ), $user->description ); + $this->assertSame( wp_unslash( self::SLASH_2 ), $user->first_name ); + $this->assertSame( wp_unslash( self::SLASH_4 ), $user->last_name ); + $this->assertSame( wp_unslash( self::SLASH_6 ), $user->nickname ); + $this->assertSame( wp_unslash( self::SLASH_2 ), $user->display_name ); + $this->assertSame( wp_unslash( self::SLASH_4 ), $user->description ); } /** @@ -192,39 +196,38 @@ public function test_wp_update_user() { array( 'ID' => $user_id, 'role' => 'subscriber', - 'first_name' => $this->slash_1, - 'last_name' => $this->slash_3, - 'nickname' => $this->slash_5, - 'display_name' => $this->slash_7, - 'description' => $this->slash_3, + 'first_name' => self::SLASH_1, + 'last_name' => self::SLASH_3, + 'nickname' => self::SLASH_5, + 'display_name' => self::SLASH_7, + 'description' => self::SLASH_3, ) ); $user = get_user_to_edit( $user_id ); - $this->assertSame( wp_unslash( $this->slash_1 ), $user->first_name ); - $this->assertSame( wp_unslash( $this->slash_3 ), $user->last_name ); - $this->assertSame( wp_unslash( $this->slash_5 ), $user->nickname ); - $this->assertSame( wp_unslash( $this->slash_7 ), $user->display_name ); - $this->assertSame( wp_unslash( $this->slash_3 ), $user->description ); + $this->assertSame( wp_unslash( self::SLASH_1 ), $user->first_name ); + $this->assertSame( wp_unslash( self::SLASH_3 ), $user->last_name ); + $this->assertSame( wp_unslash( self::SLASH_5 ), $user->nickname ); + $this->assertSame( wp_unslash( self::SLASH_7 ), $user->display_name ); + $this->assertSame( wp_unslash( self::SLASH_3 ), $user->description ); $user_id = wp_update_user( array( 'ID' => $user_id, 'role' => 'subscriber', - 'first_name' => $this->slash_2, - 'last_name' => $this->slash_4, - 'nickname' => $this->slash_6, - 'display_name' => $this->slash_2, - 'description' => $this->slash_4, + 'first_name' => self::SLASH_2, + 'last_name' => self::SLASH_4, + 'nickname' => self::SLASH_6, + 'display_name' => self::SLASH_2, + 'description' => self::SLASH_4, ) ); $user = get_user_to_edit( $user_id ); - $this->assertSame( wp_unslash( $this->slash_2 ), $user->first_name ); - $this->assertSame( wp_unslash( $this->slash_4 ), $user->last_name ); - $this->assertSame( wp_unslash( $this->slash_6 ), $user->nickname ); - $this->assertSame( wp_unslash( $this->slash_2 ), $user->display_name ); - $this->assertSame( wp_unslash( $this->slash_4 ), $user->description ); + $this->assertSame( wp_unslash( self::SLASH_2 ), $user->first_name ); + $this->assertSame( wp_unslash( self::SLASH_4 ), $user->last_name ); + $this->assertSame( wp_unslash( self::SLASH_6 ), $user->nickname ); + $this->assertSame( wp_unslash( self::SLASH_2 ), $user->display_name ); + $this->assertSame( wp_unslash( self::SLASH_4 ), $user->description ); } - } diff --git a/tests/phpunit/tests/user/wpAuthenticateSpamCheck.php b/tests/phpunit/tests/user/wpAuthenticateSpamCheck.php index fef983ddcf895..81ce98ff1d416 100644 --- a/tests/phpunit/tests/user/wpAuthenticateSpamCheck.php +++ b/tests/phpunit/tests/user/wpAuthenticateSpamCheck.php @@ -3,7 +3,7 @@ /** * @group user */ -class Tests_User_WpAuthenticateSpamCheck extends WP_UnitTestCase { +class Tests_User_wpAuthenticateSpamCheck extends WP_UnitTestCase { /** * @group ms-excluded diff --git a/tests/phpunit/tests/user/wpDeleteUser.php b/tests/phpunit/tests/user/wpDeleteUser.php index a654678174a89..e37fa43ba6584 100644 --- a/tests/phpunit/tests/user/wpDeleteUser.php +++ b/tests/phpunit/tests/user/wpDeleteUser.php @@ -3,7 +3,7 @@ /** * @group user */ -class Tests_User_WpDeleteUser extends WP_UnitTestCase { +class Tests_User_wpDeleteUser extends WP_UnitTestCase { /** * Test that usermeta cache is cleared after user deletion. diff --git a/tests/phpunit/tests/user/wpDropdownUsers.php b/tests/phpunit/tests/user/wpDropdownUsers.php index 98e9b76df6188..a0b6501eb96d4 100644 --- a/tests/phpunit/tests/user/wpDropdownUsers.php +++ b/tests/phpunit/tests/user/wpDropdownUsers.php @@ -5,7 +5,7 @@ * * @group user */ -class Tests_User_WpDropdownUsers extends WP_UnitTestCase { +class Tests_User_wpDropdownUsers extends WP_UnitTestCase { /** * @ticket 31251 @@ -13,7 +13,7 @@ class Tests_User_WpDropdownUsers extends WP_UnitTestCase { public function test_default_value_of_show_should_be_display_name() { // Create a user with a different display_name. - $u = $this->factory->user->create( + $u = self::factory()->user->create( array( 'user_login' => 'foo', 'display_name' => 'Foo Person', @@ -37,7 +37,7 @@ public function test_default_value_of_show_should_be_display_name() { public function test_show_should_display_display_name_show_is_specified_as_empty() { // Create a user with a different display_name. - $u = $this->factory->user->create( + $u = self::factory()->user->create( array( 'user_login' => 'foo', 'display_name' => 'Foo Person', @@ -63,7 +63,7 @@ public function test_show_should_display_display_name_show_is_specified_as_empty public function test_show_should_display_user_property_when_the_value_of_show_is_a_valid_user_property() { // Create a user with a different display_name. - $u = $this->factory->user->create( + $u = self::factory()->user->create( array( 'user_login' => 'foo', 'display_name' => 'Foo Person', @@ -89,7 +89,7 @@ public function test_show_should_display_user_property_when_the_value_of_show_is public function test_show_display_name_with_login() { // Create a user with a different display_name. - $u = $this->factory->user->create( + $u = self::factory()->user->create( array( 'user_login' => 'foo', 'display_name' => 'Foo Person', diff --git a/tests/phpunit/tests/user/wpGetUsersWithNoRole.php b/tests/phpunit/tests/user/wpGetUsersWithNoRole.php index 1165307aed8db..7616bbd949cd5 100644 --- a/tests/phpunit/tests/user/wpGetUsersWithNoRole.php +++ b/tests/phpunit/tests/user/wpGetUsersWithNoRole.php @@ -3,7 +3,7 @@ /** * @group user */ -class Tests_User_GetUsersWithNoRole extends WP_UnitTestCase { +class Tests_User_wpGetUsersWithNoRole extends WP_UnitTestCase { /** * @ticket 22993 @@ -42,7 +42,6 @@ public function test_get_users_with_no_role_is_accurate() { ), $users ); - } /** @@ -112,10 +111,10 @@ public function test_get_users_with_no_role_multisite_is_accurate() { */ public function test_get_users_with_no_role_matches_on_role_name() { // Create a role with a display name which would not match the role name - // in a case-insentive SQL query. + // in a case-insensitive SQL query. wp_roles()->add_role( 'somerole', 'Some role display name' ); - $someuser = self::factory()->user->create( + self::factory()->user->create( array( 'role' => 'somerole', ) @@ -149,5 +148,4 @@ public function test_get_users_with_no_role_matches_on_role_name_different_site( $this->assertEmpty( $users ); } - } diff --git a/tests/phpunit/tests/user/wpListAuthors.php b/tests/phpunit/tests/user/wpListAuthors.php new file mode 100644 index 0000000000000..3c79f8fee3a6c --- /dev/null +++ b/tests/phpunit/tests/user/wpListAuthors.php @@ -0,0 +1,343 @@ + 'name', + * 'order' => 'ASC', + * 'number' => null, + * 'optioncount' => false, + * 'exclude_admin' => true, + * 'show_fullname' => false, + * 'hide_empty' => true, + * 'echo' => true, + * 'feed' => [empty string], + * 'feed_image' => [empty string], + * 'feed_type' => [empty string], + * 'style' => 'list', + * 'html' => true, + */ + public static function wpSetUpBeforeClass( WP_UnitTest_Factory $factory ) { + global $wp_rewrite; + + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'zack', + 'display_name' => 'zack', + 'role' => 'author', + 'first_name' => 'zack', + 'last_name' => 'moon', + ) + ); + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'bob', + 'display_name' => 'bob', + 'role' => 'author', + 'first_name' => 'bob', + 'last_name' => 'reno', + ) + ); + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'paul', + 'display_name' => 'paul', + 'role' => 'author', + 'first_name' => 'paul', + 'last_name' => 'norris', + ) + ); + self::$fred_id = $factory->user->create( + array( + 'user_login' => 'fred', + 'role' => 'author', + ) + ); + + /* + * Re-initialize WP_Rewrite, so that get_author_posts_url() uses + * the default permalink structure, not affected by other tests. + */ + $wp_rewrite->init(); + + $count = 0; + foreach ( self::$user_ids as $userid ) { + $count = $count + 1; + for ( $i = 0; $i < $count; $i++ ) { + self::$posts[] = $factory->post->create( + array( + 'post_type' => 'post', + 'post_author' => $userid, + ) + ); + } + + self::$user_urls[] = get_author_posts_url( $userid ); + } + } + + public function test_wp_list_authors_default() { + $expected['default'] = + '
    • bob
    • ' . + '
    • paul
    • ' . + '
    • zack
    • '; + + $this->assertSame( $expected['default'], wp_list_authors( array( 'echo' => false ) ) ); + } + + public function test_wp_list_authors_orderby() { + $expected['post_count'] = + '
    • zack
    • ' . + '
    • bob
    • ' . + '
    • paul
    • '; + + $this->assertSame( + $expected['post_count'], + wp_list_authors( + array( + 'echo' => false, + 'orderby' => 'post_count', + ) + ) + ); + } + + public function test_wp_list_authors_order() { + $expected['id'] = + '
    • paul
    • ' . + '
    • bob
    • ' . + '
    • zack
    • '; + + $this->assertSame( + $expected['id'], + wp_list_authors( + array( + 'echo' => false, + 'orderby' => 'id', + 'order' => 'DESC', + ) + ) + ); + } + + public function test_wp_list_authors_optioncount() { + $expected['optioncount'] = + '
    • bob (2)
    • ' . + '
    • paul (3)
    • ' . + '
    • zack (1)
    • '; + + $this->assertSame( + $expected['optioncount'], + wp_list_authors( + array( + 'echo' => false, + 'optioncount' => 1, + ) + ) + ); + } + + /** + * Ensures the 'optioncount' parameter does not throw an error when there are authors without posts. + * + * @ticket 57011 + */ + public function test_wp_list_authors_optioncount_should_not_error_for_empty_authors() { + /* + * The main purpose of this test is to ensure that the error below is not thrown: + * + * Error: Object of class stdClass could not be converted to string + * + * In place of direct testing we ensure `wp_list_authors()` returns a list of authors + * at least one of which is empty. + */ + $actual = wp_list_authors( + array( + 'optioncount' => true, + 'hide_empty' => false, + 'exclude_admin' => false, + 'echo' => false, + ) + ); + $this->assertStringContainsString( '(0)', $actual ); + } + + public function test_wp_list_authors_exclude_admin() { + self::factory()->post->create( + array( + 'post_type' => 'post', + 'post_author' => 1, + ) + ); + + $expected['exclude_admin'] = + '
    • admin
    • ' . + '
    • bob
    • ' . + '
    • paul
    • ' . + '
    • zack
    • '; + + $this->assertSame( + $expected['exclude_admin'], + wp_list_authors( + array( + 'echo' => false, + 'exclude_admin' => 0, + ) + ) + ); + } + + public function test_wp_list_authors_show_fullname() { + $expected['show_fullname'] = + '
    • bob reno
    • ' . + '
    • paul norris
    • ' . + '
    • zack moon
    • '; + + $this->assertSame( + $expected['show_fullname'], + wp_list_authors( + array( + 'echo' => false, + 'show_fullname' => 1, + ) + ) + ); + } + + public function test_wp_list_authors_hide_empty() { + $fred_id = self::$fred_id; + + $expected['hide_empty'] = + '
    • bob
    • ' . + '
    • fred
    • ' . + '
    • paul
    • ' . + '
    • zack
    • '; + + $this->assertSame( + $expected['hide_empty'], + wp_list_authors( + array( + 'echo' => false, + 'hide_empty' => 0, + ) + ) + ); + } + + public function test_wp_list_authors_echo() { + $expected['echo'] = + '
    • bob
    • ' . + '
    • paul
    • ' . + '
    • zack
    • '; + + $this->expectOutputString( $expected['echo'] ); + wp_list_authors( array( 'echo' => true ) ); + } + + public function test_wp_list_authors_feed() { + $url0 = get_author_feed_link( self::$user_ids[0] ); + $url1 = get_author_feed_link( self::$user_ids[1] ); + $url2 = get_author_feed_link( self::$user_ids[2] ); + + $expected['feed'] = + '
    • bob (link to feed)
    • ' . + '
    • paul (link to feed)
    • ' . + '
    • zack (link to feed)
    • '; + + $this->assertSame( + $expected['feed'], + wp_list_authors( + array( + 'echo' => false, + 'feed' => 'link to feed', + ) + ) + ); + } + + public function test_wp_list_authors_feed_image() { + $url0 = get_author_feed_link( self::$user_ids[0] ); + $url1 = get_author_feed_link( self::$user_ids[1] ); + $url2 = get_author_feed_link( self::$user_ids[2] ); + + $expected['feed_image'] = + '
    • bob
    • ' . + '
    • paul
    • ' . + '
    • zack
    • '; + + $this->assertSame( + $expected['feed_image'], + wp_list_authors( + array( + 'echo' => false, + 'feed_image' => 'http://' . WP_TESTS_DOMAIN . '/path/to/a/graphic.png', + ) + ) + ); + } + + /** + * @ticket 26538 + */ + public function test_wp_list_authors_feed_type() { + $url0 = get_author_feed_link( self::$user_ids[0], 'atom' ); + $url1 = get_author_feed_link( self::$user_ids[1], 'atom' ); + $url2 = get_author_feed_link( self::$user_ids[2], 'atom' ); + + $expected['feed_type'] = + '
    • bob (link to feed)
    • ' . + '
    • paul (link to feed)
    • ' . + '
    • zack (link to feed)
    • '; + + $this->assertSame( + $expected['feed_type'], + wp_list_authors( + array( + 'echo' => false, + 'feed' => 'link to feed', + 'feed_type' => 'atom', + ) + ) + ); + } + + public function test_wp_list_authors_style() { + $expected['style'] = + 'bob, ' . + 'paul, ' . + 'zack'; + + $this->assertSame( + $expected['style'], + wp_list_authors( + array( + 'echo' => false, + 'style' => 'none', + ) + ) + ); + } + + public function test_wp_list_authors_html() { + $expected['html'] = 'bob, paul, zack'; + + $this->assertSame( + $expected['html'], + wp_list_authors( + array( + 'echo' => false, + 'html' => 0, + ) + ) + ); + } +} diff --git a/tests/phpunit/tests/user/wpListUsers.php b/tests/phpunit/tests/user/wpListUsers.php new file mode 100644 index 0000000000000..56bcdb5485792 --- /dev/null +++ b/tests/phpunit/tests/user/wpListUsers.php @@ -0,0 +1,202 @@ +user->create( + array( + 'user_login' => 'zack', + 'display_name' => 'zack', + 'role' => 'subscriber', + 'first_name' => 'zack', + 'last_name' => 'moon', + 'user_email' => 'm.zack@example.com', + 'user_url' => 'http://moonzack.fake', + ) + ); + + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'jane', + 'display_name' => 'jane', + 'role' => 'contributor', + 'first_name' => 'jane', + 'last_name' => 'reno', + 'user_email' => 'r.jane@example.com', + 'user_url' => 'http://janereno.fake', + ) + ); + + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'michelle', + 'display_name' => 'michelle', + 'role' => 'subscriber', + 'first_name' => 'michelle', + 'last_name' => 'jones', + 'user_email' => 'j.michelle@example.com', + 'user_url' => 'http://lemichellejones.fake', + ) + ); + + self::$user_ids[] = $factory->user->create( + array( + 'user_login' => 'paul', + 'display_name' => 'paul', + 'role' => 'subscriber', + 'first_name' => 'paul', + 'last_name' => 'norris', + 'user_email' => 'n.paul@example.com', + 'user_url' => 'http://awildpaulappeared.fake', + ) + ); + + foreach ( self::$user_ids as $user ) { + $factory->post->create( + array( + 'post_type' => 'post', + 'post_author' => $user, + ) + ); + } + } + + /** + * Test that wp_list_users() creates the expected list of users. + * + * @dataProvider data_should_create_a_user_list + * + * @ticket 15145 + * + * @param array|string $args The arguments to create a list of users. + * @param string $expected The expected result. + */ + public function test_should_create_a_user_list( $args, $expected ) { + $actual = wp_list_users( $args ); + + $expected = str_replace( + array( 'AUTHOR_ID_zack', 'AUTHOR_ID_jane', 'AUTHOR_ID_michelle', 'AUTHOR_ID_paul' ), + array( self::$user_ids[0], self::$user_ids[1], self::$user_ids[2], self::$user_ids[3] ), + $expected + ); + + if ( null === $actual ) { + $this->expectOutputString( $expected ); + } else { + $this->assertSame( $expected, $actual ); + } + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_create_a_user_list() { + return array( + 'defaults when no args are supplied' => array( + 'args' => '', + 'expected' => '
    • jane
    • michelle
    • paul
    • zack
    • ', + ), + 'the admin account included' => array( + 'args' => array( + 'exclude_admin' => false, + ), + 'expected' => '
    • admin
    • jane
    • michelle
    • paul
    • zack
    • ', + ), + 'the full name of each user' => array( + 'args' => array( + 'show_fullname' => true, + ), + 'expected' => '
    • jane reno
    • michelle jones
    • paul norris
    • zack moon
    • ', + ), + 'the feed of each user' => array( + 'args' => array( + 'feed' => 'User feed', + ), + 'expected' => '
    • jane (User feed)
    • ' . + '
    • michelle (User feed)
    • ' . + '
    • paul (User feed)
    • ' . + '
    • zack (User feed)
    • ', + ), + 'the feed of each user and an image' => array( + 'args' => array( + 'feed' => 'User feed with image', + 'feed_image' => 'http://example.org/image.jpg', + ), + 'expected' => '
    • jane User feed with image
    • ' . + '
    • michelle User feed with image
    • ' . + '
    • paul User feed with image
    • ' . + '
    • zack User feed with image
    • ', + ), + 'a feed of the specified type' => array( + 'args' => array( + 'feed' => 'User feed as atom', + 'feed_type' => 'atom', + ), + 'expected' => '
    • jane (User feed as atom)
    • ' . + '
    • michelle (User feed as atom)
    • ' . + '
    • paul (User feed as atom)
    • ' . + '
    • zack (User feed as atom)
    • ', + ), + 'no output via echo' => array( + 'args' => array( + 'echo' => false, + ), + 'expected' => '
    • jane
    • michelle
    • paul
    • zack
    • ', + ), + 'commas separating each user' => array( + 'args' => array( + 'style' => '', + ), + 'expected' => 'jane, michelle, paul, zack', + ), + 'plain text format' => array( + 'args' => array( + 'html' => false, + ), + 'expected' => 'jane, michelle, paul, zack', + ), + ); + } + + /** + * Tests that wp_list_users() does not create a user list. + * + * @dataProvider data_should_not_create_a_user_list + * + * @ticket 15145 + * + * @param array|string $args The arguments to create a list of users. + */ + public function test_should_not_create_a_user_list( $args ) { + $actual = wp_list_users( $args ); + + if ( null === $actual ) { + $this->expectOutputString( '', 'wp_list_users() did not output an empty string.' ); + } else { + $this->assertSame( $actual, 'wp_list_users() did not return an empty string.' ); + } + } + + /** + * Data provider. + * + * @return array + */ + public function data_should_not_create_a_user_list() { + return array( + 'an empty user query result' => array( + 'args' => array( + 'include' => array( 9999 ), + ), + 'expected' => '', + ), + ); + } +} diff --git a/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php b/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php new file mode 100644 index 0000000000000..a45b015ad9728 --- /dev/null +++ b/tests/phpunit/tests/user/wpRegisterPersistedPreferencesMeta.php @@ -0,0 +1,62 @@ +get_blog_prefix() . 'persisted_preferences'; + + // Test that meta key is registered. + unregister_meta_key( 'user', $meta_key ); + wp_register_persisted_preferences_meta(); + + $this->assertIsArray( $wp_meta_keys, 'No meta keys exist' ); + $this->assertArrayHasKey( + $meta_key, + $wp_meta_keys['user'][''], + 'The expected meta key was not registered' + ); + + // Test to detect changes in meta key structure. + $this->assertSame( + array( + 'type' => 'object', + 'label' => '', + 'description' => '', + 'single' => true, + 'sanitize_callback' => null, + 'auth_callback' => '__return_true', + 'show_in_rest' => array( + 'name' => 'persisted_preferences', + 'type' => 'object', + 'schema' => array( + 'type' => 'object', + 'context' => array( 'edit' ), + 'properties' => array( + '_modified' => array( + 'description' => __( 'The date and time the preferences were updated.' ), + 'type' => 'string', + 'format' => 'date-time', + 'readonly' => false, + ), + ), + 'additionalProperties' => true, + ), + ), + 'revisions_enabled' => false, + ), + $wp_meta_keys['user'][''][ $meta_key ], + 'The registered metadata did not have the expected structure' + ); + } +} diff --git a/tests/phpunit/tests/user/wpSendUserRequest.php b/tests/phpunit/tests/user/wpSendUserRequest.php index cb0c41fe69d1b..11e3130f225a5 100644 --- a/tests/phpunit/tests/user/wpSendUserRequest.php +++ b/tests/phpunit/tests/user/wpSendUserRequest.php @@ -4,18 +4,12 @@ * * @package WordPress * @since 4.9.9 - */ - -/** - * Tests_User_WpSendUserRequest class. - * - * @since 4.9.9 * * @group privacy * @group user * @covers ::wp_send_user_request */ -class Tests_User_WpSendUserRequest extends WP_UnitTestCase { +class Tests_User_wpSendUserRequest extends WP_UnitTestCase { /** * Test administrator user. diff --git a/tests/phpunit/tests/user/wpSetCurrentUser.php b/tests/phpunit/tests/user/wpSetCurrentUser.php index b451a5298b406..868b3bc71b38a 100644 --- a/tests/phpunit/tests/user/wpSetCurrentUser.php +++ b/tests/phpunit/tests/user/wpSetCurrentUser.php @@ -3,7 +3,7 @@ /** * @group user */ -class Tests_User_WpSetCurrentUser extends WP_UnitTestCase { +class Tests_User_wpSetCurrentUser extends WP_UnitTestCase { protected static $user_id; protected static $user_id2; protected static $user_ids = array(); @@ -57,5 +57,34 @@ public function test_should_set_by_name_if_id_is_null() { $this->assertSame( $user, wp_get_current_user() ); $this->assertSame( self::$user_id2, get_current_user_id() ); } -} + /** + * Ensure user switching doesn't occur for the same user, even if type is non-int. + * + * @ticket 64628 + * + * @dataProvider data_should_not_switch_to_same_user_type_equivalency + */ + public function test_should_not_switch_to_same_user_type_equivalency( string $type_function ) { + wp_set_current_user( self::$user_id ); + $this->assertSame( self::$user_id, get_current_user_id(), "Current user's ID should match the ID of the user switched to." ); + + $action = new MockAction(); + add_action( 'set_current_user', array( $action, 'action' ) ); + + wp_set_current_user( $type_function( self::$user_id ) ); + $this->assertSame( 0, $action->get_call_count(), 'set_current_user should not be fired when switching to the same user.' ); + } + + /** + * Data provider for test_should_not_switch_to_same_user_type_equivalency. + * + * @return array[] Data provider. + */ + public function data_should_not_switch_to_same_user_type_equivalency(): array { + return array( + 'integer' => array( 'type_function' => 'intval' ), + 'string' => array( 'type_function' => 'strval' ), + ); + } +} diff --git a/tests/phpunit/tests/utils.php b/tests/phpunit/tests/utils.php new file mode 100644 index 0000000000000..6a4f0d45f6dc7 --- /dev/null +++ b/tests/phpunit/tests/utils.php @@ -0,0 +1,58 @@ +assertSame( '', strip_ws( '' ) ); + $this->assertSame( 'foo', strip_ws( 'foo' ) ); + $this->assertSame( '', strip_ws( "\r\n\t \n\r\t" ) ); + + $in = "asdf\n"; + $in .= "asdf asdf\n"; + $in .= "asdf asdf\n"; + $in .= "\tasdf\n"; + $in .= "\tasdf\t\n"; + $in .= "\t\tasdf\n"; + $in .= "foo bar\n\r\n"; + $in .= "foo\n"; + + $expected = "asdf\n"; + $expected .= "asdf asdf\n"; + $expected .= "asdf asdf\n"; + $expected .= "asdf\n"; + $expected .= "asdf\n"; + $expected .= "asdf\n"; + $expected .= "foo bar\n"; + $expected .= 'foo'; + + $this->assertSame( $expected, strip_ws( $in ) ); + } + + /** + * @covers ::mask_input_value + */ + public function test_mask_input_value() { + $in = <<Assign Authors +

      To make it easier for you to edit and save the imported posts and drafts, you may want to change the name of the author of the posts. For example, you may want to import all the entries as admins entries.

      +

      If a new user is created by WordPress, the password will be set, by default, to "changeme". Quite suggestive, eh? ;)

      +
      1. Current author: Alex Shiels
        Create user
        or map to existing
      2. Current author: Alex Shiels
        Create user
        or map to existing