diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..5d2c960 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,14 @@ +# .editorconfig +root = true + +[*.php] +indent_style = tab +indent_size = 4 +tab_width = 4 +end_of_line = lf +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true + +[*.md] +trim_trailing_whitespace = false \ No newline at end of file diff --git a/.github/workflows/gh-release.yml b/.github/workflows/gh-release.yml new file mode 100644 index 0000000..8ea7b21 --- /dev/null +++ b/.github/workflows/gh-release.yml @@ -0,0 +1,55 @@ +name: "GitHub Release" +on: + # push: + # tags: + # - "*" + workflow_dispatch: +jobs: + release: + name: "Release" + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify tag is on main branch + run: | + # Fetch the latest main branch from origin + git fetch origin main + + # Check if the current commit (${{ github.sha }}) is part of the origin/main history + if git merge-base --is-ancestor ${{ github.sha }} origin/main; then + echo "Success: Tag is on the main branch." + else + echo "Error: Tag was not pushed to the main branch. Skipping release." + exit 1 + fi + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Build the project + run: bun install && bun run build + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + coverage: none + + - name: Install production Composer dependencies + run: composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist --no-progress + + - name: Create plugin zip + run: bunx bestzip build/cooked.zip cooked.php LICENSE readme.txt wpml-config.xml assets/ includes/ languages/ templates/ vendor/ + + - name: Create Release + uses: softprops/action-gh-release@v2 + with: + files: build/cooked.zip \ No newline at end of file diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 0000000..c106d08 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,31 @@ +name: PHP Lint + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +jobs: + lint: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.5, 8.4, 8.3, 8.2] + + name: PHP ${{ matrix.php }} + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + + - name: Lint PHP + run: php -l includes/ templates/ tests/ diff --git a/.github/workflows/phpcs.yml b/.github/workflows/phpcs.yml new file mode 100644 index 0000000..d770f4a --- /dev/null +++ b/.github/workflows/phpcs.yml @@ -0,0 +1,34 @@ +name: PHPCS + +on: + # push: + # branches: [ main, develop ] + # pull_request: + # branches: [ main, develop ] + workflow_dispatch: + +jobs: + phpcs: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.5, 8.4, 8.3, 8.2] + + name: PHP ${{ matrix.php }} + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + + - name: Install dependencies + run: composer install --no-interaction --no-progress + + - name: Run PHPCS + run: vendor/bin/phpcs --standard=phpcs.xml . diff --git a/.github/workflows/phpunit.yml b/.github/workflows/phpunit.yml new file mode 100644 index 0000000..f1c0f3f --- /dev/null +++ b/.github/workflows/phpunit.yml @@ -0,0 +1,34 @@ +name: PHPUnit Tests + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main, develop ] + workflow_dispatch: + +jobs: + phpunit: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + php: [8.5, 8.4, 8.3] + + name: PHP ${{ matrix.php }} + + steps: + - name: Check out code + uses: actions/checkout@v6 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: ${{ matrix.php }} + tools: composer + + - name: Install dependencies + run: composer install --no-interaction --no-progress + + - name: Run PHPUnit + run: ./vendor/bin/phpunit diff --git a/.github/workflows/wp-release.yml b/.github/workflows/wp-release.yml new file mode 100644 index 0000000..ae3594d --- /dev/null +++ b/.github/workflows/wp-release.yml @@ -0,0 +1,54 @@ +name: "Wordpress.org Release" +on: + # push: + # tags: + # - "*" + workflow_dispatch: +jobs: + tag: + name: "build" + runs-on: ubuntu-latest + steps: + + - name: Checkout code + uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Verify tag is on main branch + run: | + git fetch origin main + if git merge-base --is-ancestor ${{ github.sha }} origin/main; then + echo "Success: Tag is on the main branch." + else + echo "Error: Tag was not pushed to the main branch. Skipping release." + exit 1 + fi + + - name: Set up Bun + uses: oven-sh/setup-bun@v2 + with: + bun-version: latest + + - name: Build the project + run: bun install && bun run build + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: '8.2' + tools: composer + coverage: none + + - name: Install production Composer dependencies + run: composer install --no-dev --optimize-autoloader --no-interaction --prefer-dist --no-progress + + - name: Install SVN + run: sudo apt-get update && sudo apt-get install -y subversion + + - name: WordPress Plugin Deploy + uses: 10up/action-wordpress-plugin-deploy@stable + env: + SVN_USERNAME: ${{ secrets.SVN_USERNAME }} + SVN_PASSWORD: ${{ secrets.SVN_PASSWORD }} + SLUG: "cooked" \ No newline at end of file diff --git a/.gitignore b/.gitignore index 314f8d8..a73988e 100644 --- a/.gitignore +++ b/.gitignore @@ -6,21 +6,22 @@ # DDEV .ddev/ +.ddev/docker-compose.cooked.yaml -# Node.js +# Dev dependencies node_modules/ +vendor +# Tests tests/test-results/ tests/playwright-report/ tests/blob-report/ tests/playwright/.cache/ tests/.auth/ +tests/phpunit/.cache # Build build/* -# DDEV generated files -.ddev/docker-compose.cooked.yaml - # WordPress wordpress/ \ No newline at end of file diff --git a/.wp-env.json b/.wp-env.json index 6722618..e646bc1 100644 --- a/.wp-env.json +++ b/.wp-env.json @@ -1,6 +1,6 @@ { "core": null, - "phpVersion": "7.4", + "phpVersion": "8.2", "themes": ["WordPress/twentytwentyfive"], "plugins": ["."], "port": 8888, diff --git a/CITATION.cff b/CITATION.cff index 2310618..e32ba4d 100644 --- a/CITATION.cff +++ b/CITATION.cff @@ -1,5 +1,5 @@ cff-version: 1.2.0 -message: "If you use cooked, please cite it using the following metadata" +message: "If you use Cooked, please cite it using the following metadata" title: cooked authors: - family-names: Scheetz diff --git a/build/.gitkeep b/build/.gitkeep deleted file mode 100644 index e69de29..0000000 diff --git a/composer.json b/composer.json index eee68ff..656fa15 100644 --- a/composer.json +++ b/composer.json @@ -13,13 +13,8 @@ "homepage": "https://wordpress.org/plugins/cooked/", "version": "1.14.0", "type": "wordpress-plugin", - "license": "GPL-3.0-or-later", - "prefer-stable": true, - "minimum-stability": "dev", - "config": { - "optimize-autoloader": true - }, - "authors": [ + "license": "GPL-2.0-or-later", + "authors": [ { "name": "Gora Tech", "email": "contact@goratech.dev", @@ -33,12 +28,25 @@ "source": "https://github.com/XjSv/Cooked", "security": "https://github.com/XjSv/Cooked?tab=security-ov-file" }, - "optimize-autoloader": true, + "minimum-stability": "dev", + "prefer-stable": true, "require": { "php": ">=7.4.0", "nxp/math-executor": "^2.3" }, "require-dev": { - "php": "^7 || ^8" + "phpunit/phpunit": "^12.5", + "squizlabs/php_codesniffer": "^3.13", + "wp-coding-standards/wpcs": "^3.3", + "phpcompatibility/php-compatibility": "^10.0@alpha", + "phpcompatibility/phpcompatibility-wp": "^3.0@alpha" + }, + "config": { + "allow-plugins": { + "dealerdirect/phpcodesniffer-composer-installer": true + }, + "platform": { + "php": "7.4" + } } } diff --git a/composer.lock b/composer.lock index dd13b69..2a3dfd4 100644 --- a/composer.lock +++ b/composer.lock @@ -4,28 +4,29 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "93dff3879d013694a97a2b65c557b9d8", + "content-hash": "74f1b41ec5e7831d7cc92e8eecec51fb", "packages": [ { "name": "nxp/math-executor", - "version": "v2.3.2", + "version": "v2.3.8", "source": { "type": "git", "url": "https://github.com/neonxp/MathExecutor.git", - "reference": "c59f4cd15317754d2b50bd4bff2243012e815790" + "reference": "235deeb110839ff643d0e0804c4d362462b13550" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/neonxp/MathExecutor/zipball/c59f4cd15317754d2b50bd4bff2243012e815790", - "reference": "c59f4cd15317754d2b50bd4bff2243012e815790", + "url": "https://api.github.com/repos/neonxp/MathExecutor/zipball/235deeb110839ff643d0e0804c4d362462b13550", + "reference": "235deeb110839ff643d0e0804c4d362462b13550", "shasum": "" }, "require": { - "php": ">=7.4" + "php": ">=8.0 <8.6" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.8", - "phpunit/phpunit": ">=9.0" + "friendsofphp/php-cs-fixer": "*", + "phpstan/phpstan": "*", + "phpunit/phpunit": ">=10.0" }, "type": "library", "autoload": { @@ -59,22 +60,2335 @@ ], "support": { "issues": "https://github.com/neonxp/MathExecutor/issues", - "source": "https://github.com/neonxp/MathExecutor/tree/v2.3.2" + "source": "https://github.com/neonxp/MathExecutor/tree/v2.3.8" }, - "time": "2022-12-08T16:15:34+00:00" + "time": "2025-11-07T15:11:54+00:00" } ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": {}, - "prefer-stable": true, - "prefer-lowest": false, - "platform": { - "php": ">=7.4.0" - }, - "platform-dev": { - "php": "^7 || ^8" + "packages-dev": [ + { + "name": "dealerdirect/phpcodesniffer-composer-installer", + "version": "v1.2.1", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/composer-installer.git", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "reference": "963f0c67bffde0eac41b56be71ac0e8ba132f0bd", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.2", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" + }, + "require-dev": { + "composer/composer": "^2.2", + "ext-json": "*", + "ext-zip": "*", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", + "yoast/phpunit-polyfills": "^1.0" + }, + "type": "composer-plugin", + "extra": { + "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" + }, + "autoload": { + "psr-4": { + "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Franck Nijhof", + "email": "opensource@frenck.dev", + "homepage": "https://frenck.dev", + "role": "Open source developer" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer Standards Composer Installer Plugin", + "keywords": [ + "PHPCodeSniffer", + "PHP_CodeSniffer", + "code quality", + "codesniffer", + "composer", + "installer", + "phpcbf", + "phpcs", + "plugin", + "qa", + "quality", + "standard", + "standards", + "style guide", + "stylecheck", + "tests" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/composer-installer/issues", + "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", + "source": "https://github.com/PHPCSStandards/composer-installer" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2026-05-06T08:26:05+00:00" + }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpcompatibility/php-compatibility", + "version": "10.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibility.git", + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibility/zipball/e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "reference": "e0f0e5a3dc819a4a0f8d679a0f2453d941976e18", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.1.2", + "squizlabs/php_codesniffer": "^3.13.3 || ^4.0" + }, + "replace": { + "wimg/php-compatibility": "*" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.3", + "phpunit/phpunit": "^4.8.36 || ^5.7.21 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4 || ^10.5.32 || ^11.3.3", + "yoast/phpunit-polyfills": "^1.1.5 || ^2.0.5 || ^3.1.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-master": "9.x-dev", + "dev-develop": "10.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "homepage": "https://github.com/wimg", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCompatibility/PHPCompatibility/graphs/contributors" + } + ], + "description": "A set of sniffs for PHP_CodeSniffer that checks for PHP cross-version compatibility.", + "homepage": "https://techblog.wimgodden.be/tag/codesniffer/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibility/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibility/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibility" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-11-28T11:36:33+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-paragonie", + "version": "2.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie.git", + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityParagonie/zipball/7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "reference": "7a979711c87d8202b52f56c56bd719d09d8ed7f5", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^10.0@dev" + }, + "require-dev": { + "paragonie/random_compat": "dev-master", + "paragonie/sodium_compat": "dev-master" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A set of rulesets for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by the Paragonie polyfill libraries.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "paragonie", + "phpcs", + "polyfill", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityParagonie" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-11-29T13:09:49+00:00" + }, + { + "name": "phpcompatibility/phpcompatibility-wp", + "version": "3.0.0-alpha2", + "source": { + "type": "git", + "url": "https://github.com/PHPCompatibility/PHPCompatibilityWP.git", + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCompatibility/PHPCompatibilityWP/zipball/bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "reference": "bd53f24e7528422ac51d64dc8d53e8d4c4a877b3", + "shasum": "" + }, + "require": { + "phpcompatibility/php-compatibility": "^10.0@dev", + "phpcompatibility/phpcompatibility-paragonie": "^2.0@dev" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Wim Godden", + "role": "lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "lead" + } + ], + "description": "A ruleset for PHP_CodeSniffer to check for PHP cross-version compatibility issues in projects, while accounting for polyfills provided by WordPress.", + "homepage": "http://phpcompatibility.com/", + "keywords": [ + "compatibility", + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/PHPCompatibility/PHPCompatibilityWP/issues", + "security": "https://github.com/PHPCompatibility/PHPCompatibilityWP/security/policy", + "source": "https://github.com/PHPCompatibility/PHPCompatibilityWP" + }, + "funding": [ + { + "url": "https://github.com/PHPCompatibility", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcompatibility", + "type": "thanks_dev" + } + ], + "time": "2025-12-16T13:35:20+00:00" + }, + { + "name": "phpcsstandards/phpcsextra", + "version": "1.5.0", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", + "reference": "b598aa890815b8df16363271b659d73280129101" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", + "reference": "b598aa890815b8df16363271b659d73280129101", + "shasum": "" + }, + "require": { + "php": ">=5.4", + "phpcsstandards/phpcsutils": "^1.2.0", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "phpcsstandards/phpcsdevtools": "^1.2.1", + "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" + } + ], + "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", + "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSExtra" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-12T23:06:57+00:00" + }, + { + "name": "phpcsstandards/phpcsutils", + "version": "1.2.2", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "reference": "c216317e96c8b3f5932808f9b0f1f7a14e3bbf55", + "shasum": "" + }, + "require": { + "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", + "php": ">=5.4", + "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" + }, + "require-dev": { + "ext-filter": "*", + "php-parallel-lint/php-console-highlighter": "^1.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcsstandards/phpcsdevcs": "^1.2.0", + "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" + }, + "type": "phpcodesniffer-standard", + "extra": { + "branch-alias": { + "dev-stable": "1.x-dev", + "dev-develop": "1.x-dev" + } + }, + "autoload": { + "classmap": [ + "PHPCSUtils/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "LGPL-3.0-or-later" + ], + "authors": [ + { + "name": "Juliette Reinders Folmer", + "homepage": "https://github.com/jrfnl", + "role": "lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" + } + ], + "description": "A suite of utility functions for use with PHP_CodeSniffer", + "homepage": "https://phpcsutils.com/", + "keywords": [ + "PHP_CodeSniffer", + "phpcbf", + "phpcodesniffer-standard", + "phpcs", + "phpcs3", + "phpcs4", + "standards", + "static analysis", + "tokens", + "utility" + ], + "support": { + "docs": "https://phpcsutils.com/", + "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", + "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", + "source": "https://github.com/PHPCSStandards/PHPCSUtils" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-12-08T14:27:58+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "12.5.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "876099a072646c7745f673d7aeab5382c4439691" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/876099a072646c7745f673d7aeab5382c4439691", + "reference": "876099a072646c7745f673d7aeab5382c4439691", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^5.7.0", + "php": ">=8.3", + "phpunit/php-text-template": "^5.0", + "sebastian/complexity": "^5.0", + "sebastian/environment": "^8.0.3", + "sebastian/lines-of-code": "^4.0", + "sebastian/version": "^6.0", + "theseer/tokenizer": "^2.0.1" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/12.5.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-code-coverage", + "type": "tidelift" + } + ], + "time": "2026-04-15T08:23:17+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "6.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "reference": "3d1cd096ef6bea4bf2762ba586e35dbd317cbfd5", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/6.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/phpunit/php-file-iterator", + "type": "tidelift" + } + ], + "time": "2026-02-02T14:04:18+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "reference": "12b54e689b07a25a9b41e57736dfab6ec9ae5406", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:58+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/e1367a453f0eda562eedb4f659e13aa900d66c53", + "reference": "e1367a453f0eda562eedb4f659e13aa900d66c53", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:16+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "8.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "reference": "f258ce36aa457f3aa3339f9ed4c81fc66dc8c2cc", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/8.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:59:38+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "12.5.25", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "792c2980442dfce319226b88fa845b8b6de3b333" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/792c2980442dfce319226b88fa845b8b6de3b333", + "reference": "792c2980442dfce319226b88fa845b8b6de3b333", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.3", + "phpunit/php-code-coverage": "^12.5.6", + "phpunit/php-file-iterator": "^6.0.1", + "phpunit/php-invoker": "^6.0.0", + "phpunit/php-text-template": "^5.0.0", + "phpunit/php-timer": "^8.0.0", + "sebastian/cli-parser": "^4.2.0", + "sebastian/comparator": "^7.1.6", + "sebastian/diff": "^7.0.0", + "sebastian/environment": "^8.1.0", + "sebastian/exporter": "^7.0.2", + "sebastian/global-state": "^8.0.2", + "sebastian/object-enumerator": "^7.0.0", + "sebastian/recursion-context": "^7.0.1", + "sebastian/type": "^6.0.3", + "sebastian/version": "^6.0.0", + "staabm/side-effects-detector": "^1.0.5" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "12.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/12.5.25" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsoring.html", + "type": "other" + } + ], + "time": "2026-05-13T03:56:57+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "4.2.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/7d05781b13f7dec9043a629a21d086ed74582a15", + "reference": "7d05781b13f7dec9043a629a21d086ed74582a15", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.5.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/4.2.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/cli-parser", + "type": "tidelift" + } + ], + "time": "2026-05-17T05:29:34+00:00" + }, + { + "name": "sebastian/comparator", + "version": "7.1.6", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/c769009dee98f494e0edc3fd4f4087501688f11e", + "reference": "c769009dee98f494e0edc3fd4f4087501688f11e", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/diff": "^7.0", + "sebastian/exporter": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.2" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/7.1.6" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-04-14T08:23:15+00:00" + }, + { + "name": "sebastian/complexity", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/bad4316aba5303d0221f43f8cee37eb58d384bbb", + "reference": "bad4316aba5303d0221f43f8cee37eb58d384bbb", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:25+00:00" + }, + { + "name": "sebastian/diff", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/7ab1ea946c012266ca32390913653d844ecd085f", + "reference": "7ab1ea946c012266ca32390913653d844ecd085f", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0", + "symfony/process": "^7.2" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:55:46+00:00" + }, + { + "name": "sebastian/environment", + "version": "8.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/b121608b28a13f721e76ffbbd386d08eff58f3f6", + "reference": "b121608b28a13f721e76ffbbd386d08eff58f3f6", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/8.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/environment", + "type": "tidelift" + } + ], + "time": "2026-04-15T12:13:01+00:00" + }, + { + "name": "sebastian/exporter", + "version": "7.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/016951ae10980765e4e7aee491eb288c64e505b7", + "reference": "016951ae10980765e4e7aee491eb288c64e505b7", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.3", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/7.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:16:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "8.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/ef1377171613d09edd25b7816f05be8313f9115d", + "reference": "ef1377171613d09edd25b7816f05be8313f9115d", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "8.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/8.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/global-state", + "type": "tidelift" + } + ], + "time": "2025-08-29T11:29:25+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "reference": "97ffee3bcfb5805568d6af7f0f893678fc076d2f", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^5.0", + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:28+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "7.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "reference": "1effe8e9b8e068e9ae228e542d5d11b5d16db894", + "shasum": "" + }, + "require": { + "php": ">=8.3", + "sebastian/object-reflector": "^5.0", + "sebastian/recursion-context": "^7.0" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/7.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:57:48+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/4bfa827c969c98be1e527abd576533293c634f6a", + "reference": "4bfa827c969c98be1e527abd576533293c634f6a", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T04:58:17+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "7.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "reference": "0b01998a7d5b1f122911a66bebcb8d46f0c82d8c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "7.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/7.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-13T04:44:59+00:00" + }, + { + "name": "sebastian/type", + "version": "6.0.3", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/e549163b9760b8f71f191651d22acf32d56d6d4d", + "reference": "e549163b9760b8f71f191651d22acf32d56d6d4d", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "require-dev": { + "phpunit/phpunit": "^12.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/6.0.3" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/type", + "type": "tidelift" + } + ], + "time": "2025-08-09T06:57:12+00:00" + }, + { + "name": "sebastian/version", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/3e6ccf7657d4f0a59200564b08cead899313b53c", + "reference": "3e6ccf7657d4f0a59200564b08cead899313b53c", + "shasum": "" + }, + "require": { + "php": ">=8.3" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that helps with managing the version number of Git-hosted PHP projects", + "homepage": "https://github.com/sebastianbergmann/version", + "support": { + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2025-02-07T05:00:38+00:00" + }, + { + "name": "squizlabs/php_codesniffer", + "version": "3.13.5", + "source": { + "type": "git", + "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "reference": "0ca86845ce43291e8f5692c7356fccf3bcf02bf4", + "shasum": "" + }, + "require": { + "ext-simplexml": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": ">=5.4.0" + }, + "require-dev": { + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" + }, + "bin": [ + "bin/phpcbf", + "bin/phpcs" + ], + "type": "library", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Greg Sherwood", + "role": "Former lead" + }, + { + "name": "Juliette Reinders Folmer", + "role": "Current lead" + }, + { + "name": "Contributors", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer tokenizes PHP, JavaScript and CSS files and detects violations of a defined set of coding standards.", + "homepage": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "keywords": [ + "phpcs", + "standards", + "static analysis" + ], + "support": { + "issues": "https://github.com/PHPCSStandards/PHP_CodeSniffer/issues", + "security": "https://github.com/PHPCSStandards/PHP_CodeSniffer/security/policy", + "source": "https://github.com/PHPCSStandards/PHP_CodeSniffer", + "wiki": "https://github.com/PHPCSStandards/PHP_CodeSniffer/wiki" + }, + "funding": [ + { + "url": "https://github.com/PHPCSStandards", + "type": "github" + }, + { + "url": "https://github.com/jrfnl", + "type": "github" + }, + { + "url": "https://opencollective.com/php_codesniffer", + "type": "open_collective" + }, + { + "url": "https://thanks.dev/u/gh/phpcsstandards", + "type": "thanks_dev" + } + ], + "time": "2025-11-04T16:30:35+00:00" + }, + { + "name": "staabm/side-effects-detector", + "version": "1.0.5", + "source": { + "type": "git", + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", + "shasum": "" + }, + "require": { + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "phpstan/extension-installer": "^1.4.3", + "phpstan/phpstan": "^1.12.6", + "phpunit/phpunit": "^9.6.21", + "symfony/var-dumper": "^5.4.43", + "tomasvotruba/type-coverage": "1.0.0", + "tomasvotruba/unused-public": "1.0.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "lib/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" + ], + "support": { + "issues": "https://github.com/staabm/side-effects-detector/issues", + "source": "https://github.com/staabm/side-effects-detector/tree/1.0.5" + }, + "funding": [ + { + "url": "https://github.com/staabm", + "type": "github" + } + ], + "time": "2024-10-20T05:08:20+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "reference": "7989e43bf381af0eac72e4f0ca5bcbfa81658be4", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^8.1" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-12-08T11:19:18+00:00" + }, + { + "name": "wp-coding-standards/wpcs", + "version": "3.3.0", + "source": { + "type": "git", + "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "reference": "7795ec6fa05663d716a549d0b44e47ffc8b0d4a6", + "shasum": "" + }, + "require": { + "ext-filter": "*", + "ext-libxml": "*", + "ext-tokenizer": "*", + "ext-xmlreader": "*", + "php": ">=7.2", + "phpcsstandards/phpcsextra": "^1.5.0", + "phpcsstandards/phpcsutils": "^1.1.0", + "squizlabs/php_codesniffer": "^3.13.4" + }, + "require-dev": { + "php-parallel-lint/php-console-highlighter": "^1.0.0", + "php-parallel-lint/php-parallel-lint": "^1.4.0", + "phpcompatibility/php-compatibility": "^10.0.0@dev", + "phpcsstandards/phpcsdevtools": "^1.2.0", + "phpunit/phpunit": "^8.0 || ^9.0" + }, + "suggest": { + "ext-iconv": "For improved results", + "ext-mbstring": "For improved results" + }, + "type": "phpcodesniffer-standard", + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Contributors", + "homepage": "https://github.com/WordPress/WordPress-Coding-Standards/graphs/contributors" + } + ], + "description": "PHP_CodeSniffer rules (sniffs) to enforce WordPress coding conventions", + "keywords": [ + "phpcs", + "standards", + "static analysis", + "wordpress" + ], + "support": { + "issues": "https://github.com/WordPress/WordPress-Coding-Standards/issues", + "source": "https://github.com/WordPress/WordPress-Coding-Standards", + "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" + }, + "funding": [ + { + "url": "https://opencollective.com/php_codesniffer", + "type": "custom" + } + ], + "time": "2025-11-25T12:08:04+00:00" + } + ], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": true, + "prefer-lowest": false, + "platform": { + "php": ">=7.4.0" }, - "plugin-api-version": "2.9.0" + "platform-dev": [], + "plugin-api-version": "2.6.0" } diff --git a/cooked.php b/cooked.php index 230b73b..15958d9 100644 --- a/cooked.php +++ b/cooked.php @@ -1,32 +1,41 @@ 0, - 'errors' => [], - 'total' => 0 - ]; - - if ( ! file_exists( $file_path ) ) { - $results['errors'][] = __( 'CSV file not found.', 'cooked' ); - return $results; - } - - // Open and parse CSV file - $handle = fopen( $file_path, 'r' ); - if ( $handle === false ) { - $results['errors'][] = __( 'Could not open CSV file.', 'cooked' ); - return $results; - } - - // Read header row - $headers = fgetcsv( $handle ); - if ( $headers === false || empty( $headers ) ) { - $results['errors'][] = __( 'CSV file is empty or invalid.', 'cooked' ); - fclose( $handle ); - return $results; - } - - // Normalize headers (trim and lowercase) - $headers = array_map( 'trim', $headers ); - $headers = array_map( 'strtolower', $headers ); - - // Check for required title column - if ( ! in_array( 'title', $headers ) ) { - $results['errors'][] = __( 'CSV file must contain a "title" column.', 'cooked' ); - fclose( $handle ); - return $results; - } - - $row_number = 1; - while ( ( $row = fgetcsv( $handle ) ) !== false ) { - $row_number++; - $results['total']++; - - // Skip empty rows - if ( empty( array_filter( $row ) ) ) { - continue; - } - - // Map row data to headers - $data = []; - foreach ( $headers as $index => $header ) { - $data[ $header ] = isset( $row[ $index ] ) ? trim( $row[ $index ] ) : ''; - } - - // Import this recipe - try { - $import_result = self::import_recipe( $data, $row_number ); - if ( $import_result['success'] ) { - $results['success']++; - } else { - $error_msg = isset( $import_result['error'] ) ? $import_result['error'] : __( 'Unknown error', 'cooked' ); - $results['errors'][] = sprintf( __( 'Row %d: %s', 'cooked' ), $row_number, $error_msg ); - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - error_log( 'Cooked CSV Import Error Row ' . $row_number . ': ' . $error_msg ); - } - } - } catch ( Exception $e ) { - $error_msg = $e->getMessage(); - $results['errors'][] = sprintf( __( 'Row %d: %s', 'cooked' ), $row_number, $error_msg ); - if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { - error_log( 'Cooked CSV Import Exception Row ' . $row_number . ': ' . $error_msg ); - error_log( 'Stack trace: ' . $e->getTraceAsString() ); - } - } - } - - fclose( $handle ); - return $results; - } - - /** - * Import a single recipe from CSV data - * - * @param array $data Recipe data from CSV row - * @param int $row_number Row number for error reporting - * @return array Result with success status and error message if any - */ - public static function import_recipe( $data, $row_number = 0 ) { - global $_cooked_settings; - - // Validate required fields - if ( empty( $data['title'] ) ) { - return [ - 'success' => false, - 'error' => __( 'Title is required', 'cooked' ) - ]; - } - - // Get default content - if ( isset( $_cooked_settings['default_content'] ) ) { - $default_content = stripslashes( $_cooked_settings['default_content'] ); - } else { - $default_content = Cooked_Recipes::default_content(); - } - - // Create new recipe post - $new_recipe = [ - 'post_type' => 'cp_recipe', - 'post_status' => 'draft', - 'post_title' => sanitize_text_field( $data['title'] ), - 'post_content' => '', - 'post_author' => get_current_user_id(), - ]; - - $recipe_id = wp_insert_post( $new_recipe ); - if ( is_wp_error( $recipe_id ) ) { - return [ - 'success' => false, - 'error' => $recipe_id->get_error_message() - ]; - } - - // Prepare recipe meta - $recipe_meta = []; - $recipe_meta['cooked_version'] = COOKED_VERSION; - $recipe_meta['content'] = $default_content; - $recipe_meta['excerpt'] = isset( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : ''; - $recipe_meta['seo_description'] = isset( $data['seo_description'] ) ? sanitize_text_field( $data['seo_description'] ) : ( isset( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : '' ); - $recipe_meta['notes'] = isset( $data['notes'] ) ? wp_kses_post( $data['notes'] ) : ''; - - // Difficulty level - $difficulty_level = isset( $data['difficulty_level'] ) ? intval( $data['difficulty_level'] ) : 0; - if ( $difficulty_level < 1 || $difficulty_level > 3 ) { - $difficulty_level = 0; - } - $recipe_meta['difficulty_level'] = $difficulty_level; - - // Times - $recipe_meta['prep_time'] = isset( $data['prep_time'] ) ? intval( $data['prep_time'] ) : 0; - $recipe_meta['cook_time'] = isset( $data['cook_time'] ) ? intval( $data['cook_time'] ) : 0; - $recipe_meta['total_time'] = $recipe_meta['prep_time'] + $recipe_meta['cook_time']; - if ( isset( $data['total_time'] ) && ! empty( $data['total_time'] ) ) { - $recipe_meta['total_time'] = intval( $data['total_time'] ); - } - - // Parse ingredients - $recipe_meta['ingredients'] = []; - if ( ! empty( $data['ingredients'] ) ) { - $measurements = Cooked_Measurements::get(); - - // Split by | to get all parts - // Format: amount|measurement|name|amount|measurement|name||sub_amount|sub_measurement|sub_name|... - // When we see ||, it becomes two consecutive empty strings in the array - $all_parts = array_map( 'trim', explode( '|', $data['ingredients'] ) ); - $i = 0; - - while ( $i < count( $all_parts ) ) { - // Skip empty parts (they come from || separator) - if ( empty( $all_parts[ $i ] ) ) { - $i++; - continue; - } - - $part = $all_parts[ $i ]; - - // Check if it's a section heading (starts with #) - if ( strpos( $part, '#' ) === 0 ) { - $recipe_meta['ingredients'][] = [ - 'section_heading_name' => trim( $part, '#' ), - ]; - $i++; - continue; - } - - // Collect next 3 non-empty parts for an ingredient (amount|measurement|name) - $ingredient_parts = []; - $j = $i; - while ( count( $ingredient_parts ) < 3 && $j < count( $all_parts ) ) { - $p = trim( $all_parts[ $j ] ); - if ( ! empty( $p ) ) { - $ingredient_parts[] = $p; - } - $j++; - } - - if ( count( $ingredient_parts ) >= 3 ) { - $ingredient = self::parse_ingredient_parts( $ingredient_parts, $measurements ); - $i = $j; // Move past the collected parts - - // Check if next parts are empty (indicating || separator for substitution) - // Look ahead to see if we have empty parts followed by non-empty parts - $next_empty_count = 0; - $k = $i; - while ( $k < count( $all_parts ) && empty( trim( $all_parts[ $k ] ) ) ) { - $next_empty_count++; - $k++; - } - - // If we have empty parts (from ||) and then more parts, it's a substitution - if ( $next_empty_count > 0 && $k < count( $all_parts ) ) { - // Collect substitution parts (next 3 non-empty parts) - $sub_parts = []; - $sub_i = $k; - while ( count( $sub_parts ) < 3 && $sub_i < count( $all_parts ) ) { - $sub_part = trim( $all_parts[ $sub_i ] ); - if ( ! empty( $sub_part ) ) { - $sub_parts[] = $sub_part; - } - $sub_i++; - } - - // Parse substitution - if ( count( $sub_parts ) >= 3 ) { - $ingredient['sub_amount'] = sanitize_text_field( $sub_parts[0] ); - $sub_measurement = sanitize_text_field( $sub_parts[1] ); - $matched_sub_measurement = self::match_measurement( $sub_measurement, $measurements ); - if ( $matched_sub_measurement ) { - $ingredient['sub_measurement'] = $matched_sub_measurement; - } - $ingredient['sub_name'] = sanitize_text_field( $sub_parts[2] ); - } elseif ( count( $sub_parts ) == 2 ) { - $ingredient['sub_amount'] = sanitize_text_field( $sub_parts[0] ); - $ingredient['sub_name'] = sanitize_text_field( $sub_parts[1] ); - } elseif ( count( $sub_parts ) == 1 ) { - $ingredient['sub_name'] = sanitize_text_field( $sub_parts[0] ); - } - - $i = $sub_i; // Move past substitution - } - - $recipe_meta['ingredients'][] = $ingredient; - } else { - // Not enough parts for a complete ingredient, skip - $i++; - } - } - } - - // Parse directions - $recipe_meta['directions'] = []; - if ( ! empty( $data['directions'] ) ) { - $directions = explode( '|', $data['directions'] ); - foreach ( $directions as $direction_string ) { - $direction_string = trim( $direction_string ); - if ( empty( $direction_string ) ) { - continue; - } - - // Check if it's a section heading (starts with #) - if ( strpos( $direction_string, '#' ) === 0 ) { - $recipe_meta['directions'][] = [ - 'section_heading_name' => trim( $direction_string, '#' ), - ]; - continue; - } - - $recipe_meta['directions'][] = [ - 'content' => wp_kses_post( $direction_string ), - ]; - } - } - - // Nutrition data - $recipe_meta['nutrition'] = []; - if ( isset( $data['servings'] ) && ! empty( $data['servings'] ) ) { - $recipe_meta['nutrition']['servings'] = sanitize_text_field( $data['servings'] ); - } - if ( isset( $data['calories'] ) && ! empty( $data['calories'] ) ) { - $recipe_meta['nutrition']['calories'] = intval( $data['calories'] ); - } - - // Save recipe meta - $recipe_meta = Cooked_Recipe_Meta::meta_cleanup( $recipe_meta ); - update_post_meta( $recipe_id, '_recipe_settings', $recipe_meta ); - - // Update post excerpt - $recipe_excerpt = ! empty( $recipe_meta['excerpt'] ) ? $recipe_meta['excerpt'] : get_the_title( $recipe_id ); - $seo_content = apply_filters( 'cooked_seo_recipe_content', '

' . wp_kses_post( $recipe_excerpt ) . '

' . __( 'Ingredients', 'cooked' ) . '

[cooked-ingredients checkboxes=false]

' . __( 'Directions', 'cooked' ) . '

[cooked-directions numbers=false]' ); - $seo_content = do_shortcode( $seo_content ); - - $should_update_content = apply_filters( 'cooked_should_update_post_content', true, $recipe_id ); - if ( $should_update_content ) { - wp_update_post( [ - 'ID' => $recipe_id, - 'post_excerpt' => $recipe_excerpt, - 'post_content' => $seo_content - ] ); - } else { - wp_update_post( [ - 'ID' => $recipe_id, - 'post_excerpt' => $recipe_excerpt - ] ); - } - - // Handle taxonomies - if ( ! empty( $data['categories'] ) && taxonomy_exists( 'cp_recipe_category' ) ) { - $categories = array_map( 'trim', explode( ',', $data['categories'] ) ); - $category_ids = []; - foreach ( $categories as $category_name ) { - $category_name = sanitize_text_field( $category_name ); - if ( ! empty( $category_name ) ) { - $term = get_term_by( 'name', $category_name, 'cp_recipe_category' ); - if ( ! $term ) { - $term = wp_insert_term( $category_name, 'cp_recipe_category' ); - if ( ! is_wp_error( $term ) ) { - $category_ids[] = $term['term_id']; - } - } else { - $category_ids[] = $term->term_id; - } - } - } - if ( ! empty( $category_ids ) ) { - wp_set_object_terms( $recipe_id, $category_ids, 'cp_recipe_category' ); - } - } - - if ( defined('COOKED_PRO_VERSION') ) { - if ( ! empty( $data['cuisine'] ) && taxonomy_exists( 'cp_recipe_cuisine' ) ) { - $cuisines = array_map( 'trim', explode( ',', $data['cuisine'] ) ); - $cuisine_ids = []; - foreach ( $cuisines as $cuisine_name ) { - $cuisine_name = sanitize_text_field( $cuisine_name ); - if ( ! empty( $cuisine_name ) ) { - $term = get_term_by( 'name', $cuisine_name, 'cp_recipe_cuisine' ); - if ( ! $term ) { - $term = wp_insert_term( $cuisine_name, 'cp_recipe_cuisine' ); - if ( ! is_wp_error( $term ) ) { - $cuisine_ids[] = $term['term_id']; - } - } else { - $cuisine_ids[] = $term->term_id; - } - } - } - if ( ! empty( $cuisine_ids ) ) { - wp_set_object_terms( $recipe_id, $cuisine_ids, 'cp_recipe_cuisine' ); - } - } - - if ( ! empty( $data['cooking_method'] ) && taxonomy_exists( 'cp_recipe_cooking_method' ) ) { - $cooking_methods = array_map( 'trim', explode( ',', $data['cooking_method'] ) ); - $cooking_method_ids = []; - foreach ( $cooking_methods as $cooking_method_name ) { - $cooking_method_name = sanitize_text_field( $cooking_method_name ); - if ( ! empty( $cooking_method_name ) ) { - $term = get_term_by( 'name', $cooking_method_name, 'cp_recipe_cooking_method' ); - if ( ! $term ) { - $term = wp_insert_term( $cooking_method_name, 'cp_recipe_cooking_method' ); - if ( ! is_wp_error( $term ) ) { - $cooking_method_ids[] = $term['term_id']; - } - } else { - $cooking_method_ids[] = $term->term_id; - } - } - } - if ( ! empty( $cooking_method_ids ) ) { - wp_set_object_terms( $recipe_id, $cooking_method_ids, 'cp_recipe_cooking_method' ); - } - } - - if ( ! empty( $data['diet'] ) && taxonomy_exists( 'cp_recipe_diet' ) ) { - $diets = array_map( 'trim', explode( ',', $data['diet'] ) ); - $diet_ids = []; - foreach ( $diets as $diet_name ) { - $diet_name = sanitize_text_field( $diet_name ); - if ( empty( $diet_name ) ) { - continue; - } - // cp_recipe_diet is restricted to Schema.org RestrictedDiet values - only assign existing terms - $term = get_term_by( 'name', $diet_name, 'cp_recipe_diet' ); - - if ( $term ) { - $diet_ids[] = $term->term_id; - } - } - if ( ! empty( $diet_ids ) ) { - wp_set_object_terms( $recipe_id, $diet_ids, 'cp_recipe_diet' ); - } - } - - if ( ! empty( $data['tags'] ) && taxonomy_exists( 'cp_recipe_tags' ) ) { - $tags = array_map( 'trim', explode( ',', $data['tags'] ) ); - $tag_ids = []; - foreach ( $tags as $tag_name ) { - if ( ! empty( $tag_name ) ) { - $term = get_term_by( 'name', $tag_name, 'cp_recipe_tags' ); - if ( ! $term ) { - $term = wp_insert_term( $tag_name, 'cp_recipe_tags' ); - if ( ! is_wp_error( $term ) ) { - $tag_ids[] = $term['term_id']; - } - } else { - $tag_ids[] = $term->term_id; - } - } - } - if ( ! empty( $tag_ids ) ) { - wp_set_object_terms( $recipe_id, $tag_ids, 'cp_recipe_tags' ); - } - } - } - - return [ - 'success' => true, - 'recipe_id' => $recipe_id - ]; - } - - /** - * Match a measurement string to a measurement key - * Checks exact key match, variations, singular, and plural forms - * - * @param string $measurement_string The measurement string from CSV - * @param array $measurements Full measurements array - * @return string|false The measurement key or false if not found - */ - private static function match_measurement( $measurement_string, $measurements ) { - $measurement_string = strtolower( trim( $measurement_string ) ); - - // First, check for exact key match - if ( isset( $measurements[ $measurement_string ] ) ) { - return $measurement_string; - } - - // Check variations, singular, and plural for each measurement - foreach ( $measurements as $key => $measurement_data ) { - // Check variations - if ( isset( $measurement_data['variations'] ) && is_array( $measurement_data['variations'] ) ) { - foreach ( $measurement_data['variations'] as $variation ) { - if ( strtolower( $variation ) === $measurement_string ) { - return $key; - } - } - } - - // Check singular - if ( isset( $measurement_data['singular'] ) && strtolower( $measurement_data['singular'] ) === $measurement_string ) { - return $key; - } - - // Check plural - if ( isset( $measurement_data['plural'] ) && strtolower( $measurement_data['plural'] ) === $measurement_string ) { - return $key; - } - - // Check singular abbreviation - if ( isset( $measurement_data['singular_abbr'] ) && strtolower( $measurement_data['singular_abbr'] ) === $measurement_string ) { - return $key; - } - - // Check plural abbreviation - if ( isset( $measurement_data['plural_abbr'] ) && strtolower( $measurement_data['plural_abbr'] ) === $measurement_string ) { - return $key; - } - } - - return false; - } - - /** - * Parse ingredient parts into ingredient array - * - * @param array $parts Array of ingredient parts (amount, measurement, name, etc.) - * @param array $measurements Full measurements array - * @return array|false Ingredient array or false on error - */ - private static function parse_ingredient_parts( $parts, $measurements ) { - if ( empty( $parts ) ) { - return false; - } - - $ingredient = [ - 'amount' => '', - 'measurement' => '', - 'name' => '', - 'url' => '', - 'description' => '', - 'sub_amount' => '', - 'sub_measurement' => '', - 'sub_name' => '', - ]; - - if ( count( $parts ) >= 3 ) { - // Format: amount|measurement|name - $ingredient['amount'] = sanitize_text_field( $parts[0] ); - $measurement = sanitize_text_field( $parts[1] ); - $matched_measurement = self::match_measurement( $measurement, $measurements ); - if ( $matched_measurement ) { - $ingredient['measurement'] = $matched_measurement; - } - $ingredient['name'] = sanitize_text_field( $parts[2] ); - if ( isset( $parts[3] ) ) { - $ingredient['description'] = sanitize_text_field( $parts[3] ); - } - } elseif ( count( $parts ) == 2 ) { - // Format: amount|name (no measurement) - $ingredient['amount'] = sanitize_text_field( $parts[0] ); - $ingredient['name'] = sanitize_text_field( $parts[1] ); - } else { - // Format: name only - $ingredient['name'] = sanitize_text_field( $parts[0] ); - } - - return $ingredient; - } + /** + * Parse and import recipes from CSV file + * + * @param string $file_path Path to the CSV file. + * @return array Results array with success count and errors + */ + public static function import_from_file( $file_path ) { + global $_cooked_settings; + + $results = array( + 'success' => 0, + 'errors' => array(), + 'total' => 0, + ); + + if ( ! file_exists( $file_path ) ) { + $results['errors'][] = __( 'CSV file not found.', 'cooked' ); + return $results; + } + + /** + * Open and parse CSV file + */ + $handle = fopen( $file_path, 'r' ); + if ( false === $handle ) { + $results['errors'][] = __( 'Could not open CSV file.', 'cooked' ); + return $results; + } + + /** + * Read header row + */ + $headers = fgetcsv( $handle ); + if ( false === $headers || empty( $headers ) ) { + $results['errors'][] = __( 'CSV file is empty or invalid.', 'cooked' ); + fclose( $handle ); + return $results; + } + + /** + * Normalize headers (trim and lowercase) + */ + $headers = array_map( 'trim', $headers ); + $headers = array_map( 'strtolower', $headers ); + + /** + * Check for required title column + */ + if ( ! in_array( 'title', $headers ) ) { + $results['errors'][] = __( 'CSV file must contain a "title" column.', 'cooked' ); + fclose( $handle ); + return $results; + } + + $row_number = 1; + while ( ( $row = fgetcsv( $handle ) ) !== false ) { + ++$row_number; + ++$results['total']; + + /** + * Skip empty rows + */ + if ( empty( array_filter( $row ) ) ) { + continue; + } + + /** + * Map row data to headers + */ + $data = array(); + foreach ( $headers as $index => $header ) { + $data[ $header ] = isset( $row[ $index ] ) ? trim( $row[ $index ] ) : ''; + } + + /** + * Import this recipe + */ + try { + $import_result = self::import_recipe( $data, $row_number ); + if ( $import_result['success'] ) { + ++$results['success']; + } else { + $error_msg = isset( $import_result['error'] ) ? $import_result['error'] : sprintf( + /* translators: %d: row number */ + __( 'Row %1$d: Unknown error', 'cooked' ), + $row_number + ); + $results['errors'][] = $error_msg; + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // phpcs:disable + error_log( 'Cooked CSV Import Error Row ' . $row_number . ': ' . $error_msg ); + // phpcs:enable + } + } + } catch ( Exception $e ) { + $error_msg = $e->getMessage(); + $results['errors'][] = sprintf( __( 'Row %1$d: %2$s', 'cooked' ), $row_number, $error_msg ); + + if ( defined( 'WP_DEBUG' ) && WP_DEBUG ) { + // phpcs:disable + error_log( 'Cooked CSV Import Exception Row ' . $row_number . ': ' . $error_msg ); + error_log( 'Stack trace: ' . $e->getTraceAsString() ); + // phpcs:enable + } + } + } + + fclose( $handle ); + return $results; + } + + /** + * Import a single recipe from CSV data + * + * @param array $data Recipe data from CSV row. + * @param int $row_number Row number for error reporting. + * @return array Result with success status and error message if any + */ + public static function import_recipe( $data, $row_number = 0 ) { + global $_cooked_settings; + + /** + * Validate required fields + */ + if ( empty( $data['title'] ) ) { + return array( + 'success' => false, + 'error' => sprintf( + /* translators: %d: row number */ + __( 'Row %1$d: Title is required', 'cooked' ), + $row_number + ), + ); + } + + /** + * Get default content + */ + if ( isset( $_cooked_settings['default_content'] ) ) { + $default_content = stripslashes( $_cooked_settings['default_content'] ); + } else { + $default_content = Cooked_Recipes::default_content(); + } + + /** + * Create new recipe post + */ + $new_recipe = array( + 'post_type' => 'cp_recipe', + 'post_status' => 'draft', + 'post_title' => sanitize_text_field( $data['title'] ), + 'post_content' => '', + 'post_author' => get_current_user_id(), + ); + + $recipe_id = wp_insert_post( $new_recipe ); + if ( is_wp_error( $recipe_id ) ) { + return array( + 'success' => false, + 'error' => sprintf( + /* translators: 1: row number, 2: error message */ + __( 'Row %1$d: %2$s', 'cooked' ), + $row_number, + $recipe_id->get_error_message() + ), + ); + } + + /** + * Prepare recipe meta + */ + $recipe_meta = array(); + $recipe_meta['cooked_version'] = COOKED_VERSION; + $recipe_meta['content'] = $default_content; + $recipe_meta['excerpt'] = isset( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : ''; + $recipe_meta['seo_description'] = isset( $data['seo_description'] ) ? sanitize_text_field( $data['seo_description'] ) : ( isset( $data['excerpt'] ) ? sanitize_text_field( $data['excerpt'] ) : '' ); + $recipe_meta['notes'] = isset( $data['notes'] ) ? wp_kses_post( $data['notes'] ) : ''; + + /** + * Difficulty level + */ + $difficulty_level = isset( $data['difficulty_level'] ) ? intval( $data['difficulty_level'] ) : 0; + if ( $difficulty_level < 1 || $difficulty_level > 3 ) { + $difficulty_level = 0; + } + $recipe_meta['difficulty_level'] = $difficulty_level; + + /** + * Times + */ + $recipe_meta['prep_time'] = isset( $data['prep_time'] ) ? intval( $data['prep_time'] ) : 0; + $recipe_meta['cook_time'] = isset( $data['cook_time'] ) ? intval( $data['cook_time'] ) : 0; + $recipe_meta['total_time'] = $recipe_meta['prep_time'] + $recipe_meta['cook_time']; + if ( isset( $data['total_time'] ) && ! empty( $data['total_time'] ) ) { + $recipe_meta['total_time'] = intval( $data['total_time'] ); + } + + /** + * Parse ingredients + */ + $recipe_meta['ingredients'] = array(); + if ( ! empty( $data['ingredients'] ) ) { + $measurements = Cooked_Measurements::get(); + + /** Split by | to get all parts + * Format: amount|measurement|name|amount|measurement|name||sub_amount|sub_measurement|sub_name|... + * When we see ||, it becomes two consecutive empty strings in the array + */ + $all_parts = array_map( 'trim', explode( '|', $data['ingredients'] ) ); + $i = 0; + + while ( $i < count( $all_parts ) ) { + /** + * Skip empty parts (they come from || separator) + */ + if ( empty( $all_parts[ $i ] ) ) { + ++$i; + continue; + } + + $part = $all_parts[ $i ]; + + /** + * Check if it's a section heading (starts with #) + */ + if ( strpos( $part, '#' ) === 0 ) { + $recipe_meta['ingredients'][] = array( + 'section_heading_name' => trim( $part, '#' ), + ); + ++$i; + continue; + } + + /** + * Collect next 3 non-empty parts for an ingredient (amount|measurement|name) + */ + $ingredient_parts = array(); + $j = $i; + while ( count( $ingredient_parts ) < 3 && $j < count( $all_parts ) ) { + $p = trim( $all_parts[ $j ] ); + if ( ! empty( $p ) ) { + $ingredient_parts[] = $p; + } + ++$j; + } + + if ( count( $ingredient_parts ) >= 3 ) { + $ingredient = self::parse_ingredient_parts( $ingredient_parts, $measurements ); + /** + * Move past the collected parts + */ + $i = $j; + + /** + * Check if next parts are empty (indicating || separator for substitution) + * + * Look ahead to see if we have empty parts followed by non-empty parts + */ + $next_empty_count = 0; + $k = $i; + while ( $k < count( $all_parts ) && empty( trim( $all_parts[ $k ] ) ) ) { + ++$next_empty_count; + ++$k; + } + + /** + * If we have empty parts (from ||) and then more parts, it's a substitution + */ + if ( $next_empty_count > 0 && $k < count( $all_parts ) ) { + /** + * Collect substitution parts (next 3 non-empty parts) + */ + $sub_parts = array(); + $sub_i = $k; + while ( count( $sub_parts ) < 3 && $sub_i < count( $all_parts ) ) { + $sub_part = trim( $all_parts[ $sub_i ] ); + if ( ! empty( $sub_part ) ) { + $sub_parts[] = $sub_part; + } + ++$sub_i; + } + + /** + * Parse substitution + */ + if ( count( $sub_parts ) >= 3 ) { + $ingredient['sub_amount'] = sanitize_text_field( $sub_parts[0] ); + $sub_measurement = sanitize_text_field( $sub_parts[1] ); + $matched_sub_measurement = self::match_measurement( $sub_measurement, $measurements ); + if ( $matched_sub_measurement ) { + $ingredient['sub_measurement'] = $matched_sub_measurement; + } + $ingredient['sub_name'] = sanitize_text_field( $sub_parts[2] ); + } elseif ( count( $sub_parts ) === 2 ) { + $ingredient['sub_amount'] = sanitize_text_field( $sub_parts[0] ); + $ingredient['sub_name'] = sanitize_text_field( $sub_parts[1] ); + } elseif ( count( $sub_parts ) === 1 ) { + $ingredient['sub_name'] = sanitize_text_field( $sub_parts[0] ); + } + + /** + * Move past substitution + */ + $i = $sub_i; + } + + $recipe_meta['ingredients'][] = $ingredient; + } else { + /** + * Not enough parts for a complete ingredient, skip + */ + ++$i; + } + } + } + + /** + * Parse directions + */ + $recipe_meta['directions'] = array(); + if ( ! empty( $data['directions'] ) ) { + $directions = explode( '|', $data['directions'] ); + foreach ( $directions as $direction_string ) { + $direction_string = trim( $direction_string ); + if ( empty( $direction_string ) ) { + continue; + } + + /** + * Check if it's a section heading (starts with #) + */ + if ( strpos( $direction_string, '#' ) === 0 ) { + $recipe_meta['directions'][] = array( + 'section_heading_name' => trim( $direction_string, '#' ), + ); + continue; + } + + $recipe_meta['directions'][] = array( + 'content' => wp_kses_post( $direction_string ), + ); + } + } + + /** + * Nutrition data + */ + $recipe_meta['nutrition'] = array(); + if ( isset( $data['servings'] ) && ! empty( $data['servings'] ) ) { + $recipe_meta['nutrition']['servings'] = sanitize_text_field( $data['servings'] ); + } + if ( isset( $data['calories'] ) && ! empty( $data['calories'] ) ) { + $recipe_meta['nutrition']['calories'] = intval( $data['calories'] ); + } + + /** + * Save recipe meta + */ + $recipe_meta = Cooked_Recipe_Meta::meta_cleanup( $recipe_meta ); + update_post_meta( $recipe_id, '_recipe_settings', $recipe_meta ); + + /** + * Update post excerpt + */ + $recipe_excerpt = ! empty( $recipe_meta['excerpt'] ) ? $recipe_meta['excerpt'] : get_the_title( $recipe_id ); + $seo_content = apply_filters( 'cooked_seo_recipe_content', '

' . wp_kses_post( $recipe_excerpt ) . '

' . __( 'Ingredients', 'cooked' ) . '

[cooked-ingredients checkboxes=false]

' . __( 'Directions', 'cooked' ) . '

[cooked-directions numbers=false]' ); + $seo_content = do_shortcode( $seo_content ); + + $should_update_content = apply_filters( 'cooked_should_update_post_content', true, $recipe_id ); + if ( $should_update_content ) { + wp_update_post( + array( + 'ID' => $recipe_id, + 'post_excerpt' => $recipe_excerpt, + 'post_content' => $seo_content, + ) + ); + } else { + wp_update_post( + array( + 'ID' => $recipe_id, + 'post_excerpt' => $recipe_excerpt, + ) + ); + } + + /** + * Handle taxonomies + */ + if ( ! empty( $data['categories'] ) && taxonomy_exists( 'cp_recipe_category' ) ) { + $categories = array_map( 'trim', explode( ',', $data['categories'] ) ); + $category_ids = array(); + foreach ( $categories as $category_name ) { + $category_name = sanitize_text_field( $category_name ); + if ( ! empty( $category_name ) ) { + $term = get_term_by( 'name', $category_name, 'cp_recipe_category' ); + if ( ! $term ) { + $term = wp_insert_term( $category_name, 'cp_recipe_category' ); + if ( ! is_wp_error( $term ) ) { + $category_ids[] = $term['term_id']; + } + } else { + $category_ids[] = $term->term_id; + } + } + } + if ( ! empty( $category_ids ) ) { + wp_set_object_terms( $recipe_id, $category_ids, 'cp_recipe_category' ); + } + } + + if ( defined( 'COOKED_PRO_VERSION' ) ) { + if ( ! empty( $data['cuisine'] ) && taxonomy_exists( 'cp_recipe_cuisine' ) ) { + $cuisines = array_map( 'trim', explode( ',', $data['cuisine'] ) ); + $cuisine_ids = array(); + foreach ( $cuisines as $cuisine_name ) { + $cuisine_name = sanitize_text_field( $cuisine_name ); + if ( ! empty( $cuisine_name ) ) { + $term = get_term_by( 'name', $cuisine_name, 'cp_recipe_cuisine' ); + if ( ! $term ) { + $term = wp_insert_term( $cuisine_name, 'cp_recipe_cuisine' ); + if ( ! is_wp_error( $term ) ) { + $cuisine_ids[] = $term['term_id']; + } + } else { + $cuisine_ids[] = $term->term_id; + } + } + } + if ( ! empty( $cuisine_ids ) ) { + wp_set_object_terms( $recipe_id, $cuisine_ids, 'cp_recipe_cuisine' ); + } + } + + if ( ! empty( $data['cooking_method'] ) && taxonomy_exists( 'cp_recipe_cooking_method' ) ) { + $cooking_methods = array_map( 'trim', explode( ',', $data['cooking_method'] ) ); + $cooking_method_ids = array(); + foreach ( $cooking_methods as $cooking_method_name ) { + $cooking_method_name = sanitize_text_field( $cooking_method_name ); + if ( ! empty( $cooking_method_name ) ) { + $term = get_term_by( 'name', $cooking_method_name, 'cp_recipe_cooking_method' ); + if ( ! $term ) { + $term = wp_insert_term( $cooking_method_name, 'cp_recipe_cooking_method' ); + if ( ! is_wp_error( $term ) ) { + $cooking_method_ids[] = $term['term_id']; + } + } else { + $cooking_method_ids[] = $term->term_id; + } + } + } + if ( ! empty( $cooking_method_ids ) ) { + wp_set_object_terms( $recipe_id, $cooking_method_ids, 'cp_recipe_cooking_method' ); + } + } + + if ( ! empty( $data['diet'] ) && taxonomy_exists( 'cp_recipe_diet' ) ) { + $diets = array_map( 'trim', explode( ',', $data['diet'] ) ); + $diet_ids = array(); + foreach ( $diets as $diet_name ) { + $diet_name = sanitize_text_field( $diet_name ); + if ( empty( $diet_name ) ) { + continue; + } + /** + * The cp_recipe_diet taxonomy is restricted to Schema.org RestrictedDiet values - only assign existing terms + */ + $term = get_term_by( 'name', $diet_name, 'cp_recipe_diet' ); + + if ( $term ) { + $diet_ids[] = $term->term_id; + } + } + if ( ! empty( $diet_ids ) ) { + wp_set_object_terms( $recipe_id, $diet_ids, 'cp_recipe_diet' ); + } + } + + if ( ! empty( $data['tags'] ) && taxonomy_exists( 'cp_recipe_tags' ) ) { + $tags = array_map( 'trim', explode( ',', $data['tags'] ) ); + $tag_ids = array(); + foreach ( $tags as $tag_name ) { + if ( ! empty( $tag_name ) ) { + $term = get_term_by( 'name', $tag_name, 'cp_recipe_tags' ); + if ( ! $term ) { + $term = wp_insert_term( $tag_name, 'cp_recipe_tags' ); + if ( ! is_wp_error( $term ) ) { + $tag_ids[] = $term['term_id']; + } + } else { + $tag_ids[] = $term->term_id; + } + } + } + if ( ! empty( $tag_ids ) ) { + wp_set_object_terms( $recipe_id, $tag_ids, 'cp_recipe_tags' ); + } + } + } + + return array( + 'success' => true, + 'recipe_id' => $recipe_id, + ); + } + + /** + * Match a measurement string to a measurement key. + * Checks exact key match, variations, singular, and plural forms. + * + * @param string $measurement_string The measurement string from CSV. + * @param array $measurements Full measurements array. + * @return string|false The measurement key or false if not found + */ + private static function match_measurement( $measurement_string, $measurements ) { + $measurement_string = strtolower( trim( $measurement_string ) ); + + /** + * First, check for exact key match + */ + if ( isset( $measurements[ $measurement_string ] ) ) { + return $measurement_string; + } + + /** + * Check variations, singular, and plural for each measurement + */ + foreach ( $measurements as $key => $measurement_data ) { + /** + * Check variations + */ + if ( isset( $measurement_data['variations'] ) && is_array( $measurement_data['variations'] ) ) { + foreach ( $measurement_data['variations'] as $variation ) { + if ( strtolower( $variation ) === $measurement_string ) { + return $key; + } + } + } + + /** + * Check singular + */ + if ( isset( $measurement_data['singular'] ) && strtolower( $measurement_data['singular'] ) === $measurement_string ) { + return $key; + } + + /** + * Check plural + */ + if ( isset( $measurement_data['plural'] ) && strtolower( $measurement_data['plural'] ) === $measurement_string ) { + return $key; + } + + /** + * Check singular abbreviation + */ + if ( isset( $measurement_data['singular_abbr'] ) && strtolower( $measurement_data['singular_abbr'] ) === $measurement_string ) { + return $key; + } + + /** + * Check plural abbreviation + */ + if ( isset( $measurement_data['plural_abbr'] ) && strtolower( $measurement_data['plural_abbr'] ) === $measurement_string ) { + return $key; + } + } + + return false; + } + + /** + * Parse ingredient parts into ingredient array + * + * @param array $parts Array of ingredient parts (amount, measurement, name, etc.). + * @param array $measurements Full measurements array. + * @return array|false Ingredient array or false on error + */ + private static function parse_ingredient_parts( $parts, $measurements ) { + if ( empty( $parts ) ) { + return false; + } + + $ingredient = array( + 'amount' => '', + 'measurement' => '', + 'name' => '', + 'url' => '', + 'description' => '', + 'sub_amount' => '', + 'sub_measurement' => '', + 'sub_name' => '', + ); + + if ( count( $parts ) >= 3 ) { + /** + * Format: amount|measurement|name + */ + $ingredient['amount'] = sanitize_text_field( $parts[0] ); + $measurement = sanitize_text_field( $parts[1] ); + $matched_measurement = self::match_measurement( $measurement, $measurements ); + if ( $matched_measurement ) { + $ingredient['measurement'] = $matched_measurement; + } + $ingredient['name'] = sanitize_text_field( $parts[2] ); + if ( isset( $parts[3] ) ) { + $ingredient['description'] = sanitize_text_field( $parts[3] ); + } + } elseif ( count( $parts ) === 2 ) { + /** + * Format: amount|name (no measurement) + */ + $ingredient['amount'] = sanitize_text_field( $parts[0] ); + $ingredient['name'] = sanitize_text_field( $parts[1] ); + } else { + /** + * Format: name only + */ + $ingredient['name'] = sanitize_text_field( $parts[0] ); + } + + return $ingredient; + } } - diff --git a/includes/class.cooked-elementor.php b/includes/class.cooked-elementor.php index 6a13f25..ca5030e 100644 --- a/includes/class.cooked-elementor.php +++ b/includes/class.cooked-elementor.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage ELementor Support - * @since 1.0.0 + * @since 1.5.3 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles Elementor support. * - * @since 1.0.0 + * @since 1.5.3 */ class Cooked_Elementor { diff --git a/includes/class.cooked-gutenberg.php b/includes/class.cooked-gutenberg.php index 9c45c20..8e06f68 100644 --- a/includes/class.cooked-gutenberg.php +++ b/includes/class.cooked-gutenberg.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Gutenberg Functions - * @since 1.0.0 + * @since 1.5.2 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles the Cooked Recipe Meta Box creation. * - * @since 1.0.0 + * @since 1.5.2 */ class Cooked_Gutenberg { diff --git a/includes/class.cooked-import.php b/includes/class.cooked-import.php index 9229c6d..b623c81 100644 --- a/includes/class.cooked-import.php +++ b/includes/class.cooked-import.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Import - * @since 1.0.0 + * @since 1.8.2 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles the import of recipes from other plugins. * - * @since 1.0.0 + * @since 1.8.2 */ class Cooked_Import { @@ -156,8 +156,8 @@ public static function tabs_fields() { $html_desc .= '
  • prep_time - ' . __( 'Prep time in minutes', 'cooked' ) . '
  • '; $html_desc .= '
  • cook_time - ' . __( 'Cook time in minutes', 'cooked' ) . '
  • '; $html_desc .= '
  • difficulty_level - ' . __( 'Difficulty level (1=Beginner, 2=Intermediate, 3=Advanced)', 'cooked' ) . '
  • '; - $html_desc .= '
  • ingredients - ' . __( 'Ingredients, separated by pipe (|). Format: "amount|measurement|name" or "name" for simple ingredients. Add substitutions with double pipe (||): "amount|measurement|name||sub_amount|sub_measurement|sub_name"', 'cooked' ) . '
  • '; - $html_desc .= '
  • directions - ' . __( 'Directions/instructions, separated by pipe (|)', 'cooked' ) . '
  • '; + $html_desc .= '
  • ingredients - ' . __( 'Ingredients, separated by pipe (|).
    Format: "amount|measurement|name" or "name" for simple ingredients.
    Add substitutions with double pipe (||): "amount|measurement|name||sub_amount|sub_measurement|sub_name"', 'cooked' ) . '
  • '; + $html_desc .= '
  • directions - ' . __( 'Directions/instructions, separated by pipe (|)', 'cooked' ) . '
  • '; $html_desc .= '
  • notes - ' . __( 'Notes', 'cooked' ) . '
  • '; $html_desc .= '
  • category - ' . __( 'Category, separated by comma', 'cooked' ) . '
  • '; if ( defined('COOKED_PRO_VERSION') ) { diff --git a/includes/class.cooked-migration.php b/includes/class.cooked-migration.php index afc246c..3b49039 100644 --- a/includes/class.cooked-migration.php +++ b/includes/class.cooked-migration.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Migration - * @since 1.0.0 + * @since 1.3.0 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles the migration from Cooked Classic. * - * @since 1.0.0 + * @since 1.3.0 */ class Cooked_Migration { diff --git a/includes/class.cooked-multilingual.php b/includes/class.cooked-multilingual.php index d8ed9ba..be95c69 100644 --- a/includes/class.cooked-multilingual.php +++ b/includes/class.cooked-multilingual.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Multilingual Support - * @since 1.13.0 + * @since 1.12.0 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles multilingual plugin support (Polylang, WPML, etc.). * - * @since 1.13.0 + * @since 1.12.0 */ class Cooked_Multilingual { diff --git a/includes/class.cooked-plugin-extra.php b/includes/class.cooked-plugin-extra.php index a2215d5..6ef3d83 100644 --- a/includes/class.cooked-plugin-extra.php +++ b/includes/class.cooked-plugin-extra.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Widgets - * @since 1.0.0 + * @since 1.8.8 */ // Exit if accessed directly @@ -20,7 +20,7 @@ public function __construct() { * * Adds a "Upgrade to Pro" link to the plugin list page. * - * @since 1.0.0 + * @since 1.8.8 * @param array $links * @return array */ diff --git a/includes/class.cooked-rankmathseo.php b/includes/class.cooked-rankmathseo.php index e512e21..b3392a0 100644 --- a/includes/class.cooked-rankmathseo.php +++ b/includes/class.cooked-rankmathseo.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Rank Math SEO Support - * @since 1.0.0 + * @since 1.8.8 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles Rank Math SEO support. * - * @since 1.0.0 + * @since 1.8.8 */ class Cooked_RankMathSEO { diff --git a/includes/class.cooked-yoastseo.php b/includes/class.cooked-yoastseo.php index 3ebb5f5..4dcc069 100644 --- a/includes/class.cooked-yoastseo.php +++ b/includes/class.cooked-yoastseo.php @@ -4,7 +4,7 @@ * * @package Cooked * @subpackage Yoast SEO Support - * @since 1.0.0 + * @since 1.8.8 */ // Exit if accessed directly @@ -15,7 +15,7 @@ * * This class handles Yoast SEO support. * - * @since 1.0.0 + * @since 1.8.8 */ class Cooked_YoastSEO { diff --git a/includes/widgets/init.php b/includes/widgets/init.php index 7375744..90cec5c 100644 --- a/includes/widgets/init.php +++ b/includes/widgets/init.php @@ -1,4 +1,11 @@ + + PHPCS configuration for Cooked + + . + + + */vendor/* + */tests/* + */node_modules/* + + + + + + + + + + + + + + + + + + + + cooked.php + /includes/ + + + + /includes/ + + + + cooked.php + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/phpunit.xml b/phpunit.xml new file mode 100644 index 0000000..4f0e223 --- /dev/null +++ b/phpunit.xml @@ -0,0 +1,15 @@ + + + + + tests/phpunit + + + \ No newline at end of file diff --git a/readme.md b/readme.md index 8859397..545952f 100644 --- a/readme.md +++ b/readme.md @@ -68,12 +68,13 @@ This project uses [Bun](https://bun.sh) for package management and running scrip #### Development environment You can run the plugin locally with either wp-env or DDEV; pick one. -**wp-env** — Requires Node; `@wordpress/env` is in devDependencies. Config in `.wp-env.json` (PHP 7.4, port 8888, plugin mounted from repo root). +**wp-env** — Requires Node.js and Docker; `@wordpress/env` is in devDependencies. Config in `.wp-env.json` (PHP 7.4, port 8888, plugin mounted from repo root). ``` bash bun run start:wp-env # start bun run stop:wp-env # stop -bun run reset:wp-env # reset -bun run destroy:wp-env # tear down +bun run reset:wp-env # wipe all db uploads +bun run destroy:wp-env # remove containers and data +bun run shell:wp-env # run bash inside the container ``` Site at port 8888, tests at 8889. @@ -108,6 +109,21 @@ bun run bundle ``` Runs build and i18n, then creates `build/cooked.zip` ready for distribution (excludes dev files). +#### Testing + +**PHPCS** — Lint PHP files against WordPress Coding Standards and PHPCompatibility (PHP 7.4+): +``` bash +bun run lint # check for violations +bun run lint-fix # auto-fix violations +``` + +**PHPUnit** — Run the test suite (14 test classes covering CSV import, recipes, settings, SEO, and more): +``` bash +bun run test +``` + +Tests are in `tests/phpunit/` with CSV fixtures in `tests/test_data/`. Requires a running wp-env or DDEV environment. + ## Documentation Detailed documentation for Cooked can be found in the [wiki](https://github.com/XjSv/Cooked/wiki). diff --git a/readme.txt b/readme.txt index 4f35878..4b03878 100644 --- a/readme.txt +++ b/readme.txt @@ -7,6 +7,7 @@ Stable tag: 1.14.0 Requires PHP: 7.4 License: GPLv2 or later License URI: https://www.gnu.org/licenses/gpl-2.0.html +Funding: https://github.com/sponsors/XjSv Cooked is the absolute best way to create & display recipes with WordPress. SEO optimized, galleries, timers, and much more. @@ -86,13 +87,13 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.14.0 = * **NEW:** Added a new "Add Bulk Ingredients" and "Add Bulk Directions" buttons to the recipe builder. -* **FIX:** Validated style attribute for the [cooked-recipe-card] and [cooked-categories] shortcodes. +* **FIX:** Validated style attribute for the `[cooked-recipe-card]` and `[cooked-categories]` shortcodes. * **TWEAK:** Added Ingredient Substitution fields to the migration and imports features. * **TWEAK:** Updated the "Apply to All" template update feature to fix potential performance issues with large recipe collections. = 1.13.0 = -* **NEW:** Added CSV import functionality for bulk importing recipes from CSV files. Supports all recipe fields including ingredients with substitutions, directions, nutrition data, categories, and tags (thanks to @mgiannopoulos24). -* **TWEAK:** Simplified the way the [cooked-related-recipes] shortcode works. +* **NEW:** Added CSV import functionality for bulk importing recipes from CSV files. Supports all recipe fields including ingredients with substitutions, directions, nutrition data, categories, and tags (thanks to [@mgiannopoulos24](https://github.com/mgiannopoulos24)). +* **TWEAK:** Simplified the way the `[cooked-related-recipes]` shortcode works. * **FIX:** Fixed a bug with the recipe directions and ingredients not being sortable in mobile devices. * **FIX:** Fixed a bug when toggling full screen view in recipe fields that use the WP Editor. * **FIX:** Fixed bug when changing image under recipe instructions steps where the image thumbnail was not updated with the replaced photo after clicking "Use this image". @@ -103,19 +104,19 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.12.0 = * **NEW:** Added the ability to add ingredients substitutions thanks to @mgiannopoulos24. -* **NEW:** Added new [cooked-related-recipes] shortcode to display related recipes based on categories, cuisines, ingredients, and other factors. -* **NEW:** Added Polylang & WPML plugin support. -* **TWEAK:** Added [cooked-next-previous] shortcode documentation in recipe "Shortcodes" tab. +* **NEW:** Added new `[cooked-related-recipes]` shortcode to display related recipes based on categories, cuisines, ingredients, and other factors. +* **NEW:** Added [Polylang](https://polylang.pro/) & [WPML](https://wpml.org/) plugin support. +* **TWEAK:** Added `[cooked-next-previous]` shortcode documentation in recipe "Shortcodes" tab. = 1.11.4 = -* **FIX:** Addressed the CVE-2025-68586 security vulnerability. -* **FIX:** Addressed the CVE-2025-62989 security vulnerability. +* **FIX:** Addressed the [CVE-2025-68586](https://nvd.nist.gov/vuln/detail/CVE-2025-68586) security vulnerability. +* **FIX:** Addressed the [CVE-2025-62989](https://nvd.nist.gov/vuln/detail/CVE-2025-62989) security vulnerability. = 1.11.3 = -* **NEW:** Added the Patchstack Vulnerability Disclosure Program. +* **NEW:** Added the [Patchstack Vulnerability Disclosure Program](https://patchstack.com/database/vdp). = 1.11.2 = -* **NEW:** Added import for WP Recipe Maker recipes. +* **NEW:** Added import for [WP Recipe Maker](https://wordpress.org/plugins/wp-recipe-maker/) recipes. * **TWEAK:** Improved author permalink generation and rewrite rules for recipe authors. * **TWEAK:** Added transient message handling for guests to support guest recipe submissions. * **NEW:** Added developer hooks: `cooked_info_shortcode_output` filter and `cooked_ingredients_shortcode_before`/`cooked_ingredients_shortcode_after` actions for enhanced customization. @@ -132,8 +133,8 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.11.0 = * **FIX:** Fixed a bug when users have multiple roles where the WP Editor does not appear in the recipe edit screen. -* **NEW:** Added the 'cooked_format_author_name' developer filter for customizing how author names are displayed via code. View the [Cooked Documentation](https://docs.cooked.pro/docs/author-name/) for more information. -* **NEW:** Added 'Default Heading Tags' settings option to allow users to choose between H2, H3, H4, H5, H6 or Div for the Directions and Ingredients sections. +* **NEW:** Added the `cooked_format_author_name` developer filter for customizing how author names are displayed via code. View the [Cooked Documentation](https://docs.cooked.pro/docs/author-name/) for more information. +* **NEW:** Added 'Default Heading Tags' settings option to allow users to choose between `H2`, `H3`, `H4`, `H5`, `H6` or `Div` for the Directions and Ingredients sections. * **FIX:** Fixed a couple of PHP related issues. * **FIX:** Fixed a bug with the Browse Recipe filters that would not work when the Browse Recipe Page is set as the Homepage. * **FIX:** Fixed a bug with default values not loading when introducing a new settings field and the settings page was not saved yet. @@ -146,9 +147,9 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **TWEAK:** Improved SEO by preventing print pages from being indexed in search engines. * **FIX:** Fixed an issue where checkbox settings with default values couldn't be turned off in the settings panel. * **NEW:** Added support for profile photos with a new optimized image size specifically for user avatars. -* **NEW:** Added flexibility to customize heading styles for Directions and Ingredients sections - now you can choose between H2, H3, H4, H5, H6 or regular text. +* **NEW:** Added flexibility to customize heading styles for Directions and Ingredients sections - now you can choose between `H2`, `H3`, `H4`, `H5`, `H6` or regular text. * **FIX:** Improved accessibility by adding descriptive titles to recipe direction images. -* **FIX:** Fixed an issue where recipe notes weren't displaying properly when using the [cooked-notes] shortcode with show_header option. +* **FIX:** Fixed an issue where recipe notes weren't displaying properly when using the `[cooked-notes]` shortcode with `show_header` option. * **FIX:** Fixed an issue where the servings was showing as 0 in the recipe print view. * **FIX:** Improved French language support by fixing issues with the built-in translation. @@ -158,7 +159,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **TWEAK:** Fixed a memory issue when using the themes Customizer and the recipe card widget. = 1.9.5 = -* **FIX:** Resolved an issue with the preprocess_shortcode filter that was causing memory problems and plugin crashes, especially when used with Elementor. +* **FIX:** Resolved an issue with the `preprocess_shortcode` filter that was causing memory problems and plugin crashes, especially when used with Elementor. * **FIX:** Enhanced the navigation for recipe categories and tags in the admin dashboard, ensuring the Recipes menu remains expanded. * **FIX:** Addressed compatibility issues with PHP v8.1. * **FIX:** Corrected a bug affecting the pretty URLs for sorting and searching in the Browse Recipe feature. @@ -183,25 +184,25 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **NEW:** Added pretty URL's to the Browse Recipe page when searching and filtering recipes. * **FIX:** Fixed issue with the recipe search when filters are applied. * **FIX:** Fixed a couple of PHP v8.3 compatibility issues. -* **FIX:** Fixed issue with Divi Theme Builder shortcodes not loading. +* **FIX:** Fixed issue with [Divi Theme Builder](https://www.elegantthemes.com/gallery/divi/) shortcodes not loading. * **FIX:** Fixed a bug when installing the plugin for the first time and the settings were not saved yet. = 1.8.9 = -* **NEW:** Improved SEO by dynamically updating the canonical URL on the Browse Recipe page to match active category/tag filters. Supports Rank Math SEO and Yoast SEO. +* **NEW:** Improved SEO by dynamically updating the canonical URL on the Browse Recipe page to match active category/tag filters. Supports [Rank Math SEO](https://wordpress.org/plugins/seo-by-rank-math/) and [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/). * **TWEAK:** Enhanced translation handling to better support custom language files and prevent conflicts. * **FIX:** Resolved compatibility issue with Loco Translate plugin that was causing incorrect textdomain loading. = 1.8.8 = -* **NEW:** Added option to disable the recipe archive page under Cooked Settings > General > Advanced Settings. -* **NEW:** Added Rank Math SEO and Yoast SEO support. Added a custom variable called `cooked_recipe_category` that can be used in the title or description fields. -* **NEW:** Added 'hide_excerpt' Parameter to [cooked-browse] shortcode to hide the recipe excerpt. +* **NEW:** Added option to disable the recipe archive page under Cooked `Settings > General > Advanced Settings`. +* **NEW:** Added [Rank Math SEO](https://wordpress.org/plugins/seo-by-rank-math/) and [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) support. Added a custom variable called `cooked_recipe_category` that can be used in the title or description fields. +* **NEW:** Added `hide_excerpt` Parameter to `[cooked-browse]` shortcode to hide the recipe excerpt. * **NEW:** Added Recipes column in the Admin users table to show the number of recipes each user has created. * **NEW:** Added post states to the Browse Recipe page. When selected a label will appear in the page list table indicating which page is the Cooked Browse Recipes Page. * **TWEAK:** Moved WP Editor Roles option into General settings. * **TWEAK:** The Excerpt, Notes and Directions fields will display the WP Editor depending on the 'WP Editor Roles' setting. -* **TWEAK:** If the "Disable Cooked Tags" setting is enabled, the SEO Description field will be hidden in the recipe edit screen. +* **TWEAK:** If the "Disable Cooked `` Tags" setting is enabled, the SEO Description field will be hidden in the recipe edit screen. * **FIX:** Added Meta Description to Meta Tags, it uses the SEO Description, Excerpt or Title, in that order. -* **FIX:** Fixed bug when Disable Cooked Tags is turned on. +* **FIX:** Fixed bug when Disable Cooked `` Tags is turned on. * **FIX:** Fixed bug with schema output where the direction titles were duplicated as 'Step 1'. The section heading logic is removed in favor or labeling each step as Step #. * **FIX:** Fixed bug with the recipe print view not displaying the Notes section. @@ -211,23 +212,23 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.8.6 = * **FIX:** Fixed bug with recipe pagination. It now works with Plain and Custom Permalink structures. * **FIX:** Fixed bug with the recipe directions text editor not being responsive. -* **FIX:** Fixed permalink preview in Cooked Settings > Permalinks. +* **FIX:** Fixed permalink preview in Cooked `Settings > Permalinks`. = 1.8.5 = * **NEW:** Added 4 new measurement options: "Drizzle", "Clove", "Jar", and "Can" to support the Delicious Recipes plugin import. * **FIX:** Fixed bug with the recipe gallery not showing Vimeo videos thumbnails. * **FIX:** Fixed bug with Cooked settings success message showing on other admin pages. * **FIX:** Fixed undefined index error when the Browse Page is not set. -* **FIX:** Fixed bug with the recipe SEO schema not having a "name" for the recipe directions steps. +* **FIX:** Fixed bug with the recipe SEO schema not having a `name` for the recipe directions steps. = 1.8.4 = * **TWEAK:** Updated the look of the Nutrition Facts to conform with the new FDA guidelines on [Changes to the Nutrition Facts Label](https://www.fda.gov/food/food-labeling-nutrition/changes-nutrition-facts-label). -* **TWEAK:** Updated the Percent Daily Value (%DV) to conform with the new FDA guidelines on [Daily Value on the Nutrition and Supplement Facts Labels](https://www.fda.gov/food/nutrition-facts-label/daily-value-nutrition-and-supplement-facts-labels). -* **TWEAK:** Updated the recipe schema to include the nutrition information and updated the "recipeInstructions" property to use the "HowToStep" type for better SEO. +* **TWEAK:** Updated the Percent Daily Value (`%DV`) to conform with the new FDA guidelines on [Daily Value on the Nutrition and Supplement Facts Labels](https://www.fda.gov/food/nutrition-facts-label/daily-value-nutrition-and-supplement-facts-labels). +* **TWEAK:** Updated the recipe schema to include the nutrition information and updated the `recipeInstructions` property to use the `HowToStep` type for better SEO. * **NEW:** Added unique IDs to the recipe directions for the ability to link directly to a recipe step (i.e. https://www.example.com/recipe/my-recipe#cooked-single-direction-step-3). * **FIX:** Fixed a bug with the recipe nutrition information not setting values correctly in the admin area. -* **FIX:** Various bug fixes for the WP Delicious import feature thanks to @Genevsky. -* **FIX:** Fixed bug with certain links not saving correctly in the recipes Notes field thanks to @nwm2006. +* **FIX:** Various bug fixes for the [WP Delicious](https://wordpress.org/plugins/delicious-recipes/) import feature thanks to @Genevsky. +* **FIX:** Fixed bug with certain links not saving correctly in the recipes Notes field thanks to [@nwm2006](https://profiles.wordpress.org/nwm2006/). * **TWEAK:** Fixed bugs with the permissions system. * **TWEAK:** Minor improvements to settings page and other areas of the plugin. @@ -236,9 +237,9 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.8.2 = * **FIX:** Fixed bug with the recipe direction not saving correctly. -* **FIX:** Fixed bug with the tooltips such as in the Recipe Template "Save as Default" button not working when "Smash Balloon Social Photo Feed" plugin is installed. -* **NEW:** Added the Notes feature, a new field to add notes to your recipes. Use the [cooked-notes] shortcode to display them. -* **NEW:** Added the option to import recipes from the WP Delicious plugin. +* **FIX:** Fixed bug with the tooltips such as in the Recipe Template "Save as Default" button not working when [Smash Balloon Social Photo Feed](https://wordpress.org/plugins/instagram-feed/) plugin is installed. +* **NEW:** Added the **Notes** feature, a new field to add notes to your recipes. Use the `[cooked-notes]` shortcode to display them. +* **NEW:** Added the option to import recipes from the [WP Delicious](https://wordpress.org/plugins/delicious-recipes/) plugin. = 1.8.1 = * **FIX:** Persistent Cross-Site Scripting Vulnerability via the Cooked Timer. @@ -250,13 +251,13 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.8.0 = * **NEW:** Added Hands Free Cooking Mode for a more convenient cooking experience. -* **TWEAK:** Improved pretty URLs for pagination links thanks to [morvy](https://github.com/morvy). +* **TWEAK:** Improved pretty URLs for pagination links thanks to [@morvy](https://github.com/morvy). * **TWEAK:** Improved the way recipe categories and tags are handled for better site performance. * **TWEAK:** Optimized the plugin's performance by streamlining code and improving how external libraries are loaded. * **TWEAK:** Updated language files for better international support. -* **FIX:** Implemented several security enhancements to keep your site safe and secure thanks to [re-alter](https://github.com/re-alter). -* **FIX:** Fixed an issue with the recipe gallery editing feature thanks to [re-alter](https://github.com/re-alter). -* **FIX:** Improved user permissions system for better control over who can edit recipes and templates thanks to [re-alter](https://github.com/re-alter). +* **FIX:** Implemented several security enhancements to keep your site safe and secure thanks to @re-alter. +* **FIX:** Fixed an issue with the recipe gallery editing feature thanks to @re-alter. +* **FIX:** Improved user permissions system for better control over who can edit recipes and templates thanks to @re-alter. * **FIX:** Resolved various bugs related to recipe display and functionality. * **FIX:** Fixed an issue with the full-screen mode of the image viewer. * **FIX:** Corrected the display of recipe nutrition information. @@ -265,16 +266,16 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **FIX:** Serving Size Not Changing Ingredients. = 1.7.15.3 = -* **FIX:** HTML showing in front end when using the [cooked-browse] shortcode. +* **FIX:** HTML showing in front end when using the `[cooked-browse]` shortcode. = 1.7.15.2 = -* **FIX:** Composer detected issues in your platform error discovered by @ianrlp. -* **FIX:** PHP undefined variable $hours_left discovered and fixed by @addyh. -* **TWEAK:** Security improvements thanks to @addyh. +* **FIX:** Composer detected issues in your platform error discovered by [@ianrlp](https://profiles.wordpress.org/ianrlp/). +* **FIX:** PHP undefined variable `$hours_left` discovered and fixed by [@addyh](https://github.com/addyh). +* **TWEAK:** Security improvements thanks to [@addyh](https://github.com/addyh). = 1.7.15.1 = -* **FIX:** Addressed the CVE-2023-44477 security vulnerability. -* **FIX:** Added html lang attribute to html tag in print view. +* **FIX:** Addressed the [CVE-2023-44477](https://nvd.nist.gov/vuln/detail/CVE-2023-44477) security vulnerability. +* **FIX:** Added html `lang` attribute to html tag in print view. * **FIX:** Added alt text to gallery images. = 1.7.13 = @@ -297,7 +298,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.7.8.5 = * **FIX:** Fixed an issue with large spaces between the recipe template shortcodes -* **FIX:** Fixed an issue with Vimeo videos in the gallery +* **FIX:** Fixed an issue with [Vimeo](https://vimeo.com/) videos in the gallery = 1.7.8.4 = * **FIX:** Removed "Section Headings" from recipe schema output. @@ -328,18 +329,18 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **TWEAK:** Widgets with images now load thumbnail sizes instead of the larger ones. = 1.7.5.2 = -* **TWEAK:** Adds support for Cooked Pro 1.7. +* **TWEAK:** Adds support for [Cooked Pro](https://cooked.pro/) 1.7. = 1.7.4 = -* **TWEAK:** Moved Fotorama assets into plugin instead of relying on a CDN connection. +* **TWEAK:** Moved [Fotorama](https://fotorama.io/) assets into plugin instead of relying on a CDN connection. * **TWEAK:** Removed "imagesLoaded" script (no longer needed). -* **TWEAK:** Added new filter to single ingredient output (cooked_single_ingredient_html) +* **TWEAK:** Added new filter to single ingredient output (`cooked_single_ingredient_html`) = 1.7.3 = * **FIX:** A minor fix (thank you to @zorkman777) = 1.7.2 = -* **FIX:** Adds support for Cooked Pro v1.6 (redirect fixes) +* **FIX:** Adds support for [Cooked Pro](https://cooked.pro/) v1.6 (redirect fixes) = 1.7.1 = * **TWEAK:** WordPress 5.2 support @@ -350,7 +351,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **NEW:** Removed masonry javascript and let the recipe grid line up automatically with CSS. * **NEW:** Browse dropdown now includes both parent and child taxonomies. Long lists get a scrollable area. * **NEW:** Added an "exclude" property to the `[cooked-browse]` shortcode. You can now exclude specific recipes by their ID. [Learn More](https://demos.boxystudio.com/cooked/) -* **NEW:** Added a "ding" sound to the end of timers. Use the "cooked_timer_sound_mp3" filter to change the MP3 file to anything you'd like (needs to be a publically available URL). [Learn More](https://demos.boxystudio.com/cooked/) +* **NEW:** Added a "ding" sound to the end of timers. Use the `cooked_timer_sound_mp3` filter to change the MP3 file to anything you'd like (needs to be a publically available URL). [Learn More](https://demos.boxystudio.com/cooked/) * **FIX:** Fixed some missing ingredient fractions. * **FIX:** Fixed some minor PHP warnings. @@ -362,7 +363,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **TWEAK:** Added new icon for BigOven save button (Pro feature). = 1.6.2 = -* **FIX:** Added support for custom permalinks with slashes (i.e. "our-food/recipes"). +* **FIX:** Added support for custom permalinks with slashes (i.e. `our-food/recipes`). = 1.6.1 = * **FIX:** Fixed a bug with the search form when multiple searh forms are on one page. @@ -377,14 +378,14 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.5.4 = * **FIX:** Some minor "difficulty level" label fixes. -* **FIX:** Nutrition Facts now accepts "0" as an amount. +* **FIX:** Nutrition Facts now accepts `0` as an amount. * **FIX:** Fixed the annoying "Settings Page Disappearing" issue! = 1.5.3 = -* **NEW:** Elementor support. Now you can create your recipe templates with Elementor! +* **NEW:** [Elementor](https://wordpress.org/plugins/elementor/) support. Now you can create your recipe templates with [Elementor](https://wordpress.org/plugins/elementor/)! = 1.5.2 = -* **NEW:** Added a new "cooked_show_difficulty_level" filter so you can show whatever you want. +* **NEW:** Added a new `cooked_show_difficulty_level` filter so you can show whatever you want. * **FIX:** Fixed an issue with the "Save as Default" and "Apply to All" feature. * **FIX:** Added a fix to prevent Gutenberg from breaking the recipe edit screen. * **FIX:** Fixed an unintentional redirect issue with the recipe RSS feed. @@ -406,15 +407,15 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **FIX:** Fixed some issues with Recipe Schema output. = 1.4.2 = -* **NEW:** Improved support for Yoast SEO (and other SEO plugins). +* **NEW:** Improved support for [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) (and other SEO plugins). * **NEW:** French translation added. * **FIX:** Fixed an issue with some incorrect Percent Daily Values on the front-end. * **FIX:** Fixed a padding issue on the recipe grid (on smaller screens). -* **FIX:** Bug fixes for the WooCommerce Memberships' "Restrict Content" feature. +* **FIX:** Bug fixes for the [WooCommerce](https://wordpress.org/plugins/woocommerce/) Memberships' "Restrict Content" feature. = 1.4.1 = -* **NEW:** Added support for WooCommerce Memberships' "Restrict Content" feature. -* **FIX:** Fixed some issues with the [cooked-browse] shortcode. +* **NEW:** Added support for [WooCommerce](https://wordpress.org/plugins/woocommerce/) Memberships' "Restrict Content" feature. +* **FIX:** Fixed some issues with the `[cooked-browse]` shortcode. = 1.4.0.3 = * **FIX:** Fixed an issue with pagination on recipe taxonomy templates. @@ -422,7 +423,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.4.0.2 = * **NEW:** When viewing a parent category, it will now display sub-category items instead of recipes. This allows you to nicely nest your categories if desired. * **TWEAK:** Added some adjustments for "Dark Mode". -* **TWEAK:** Added some adjustments to fix a few TwentySeventeen CSS conflicts. +* **TWEAK:** Added some adjustments to fix a few [TwentySeventeen](https://wordpress.org/themes/twentyseventeen/) CSS conflicts. = 1.4.0.1 = * **FIX:** Fixed a major layout issue, sorry about that everyone! @@ -435,7 +436,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **FIX:** Fixed some styling issues with the search bar. = 1.3.05 = -* **TWEAK:** Minor adjustments to support the new Cooked Pro 1.1. +* **TWEAK:** Minor adjustments to support the new [Cooked Pro](https://cooked.pro/) 1.1. = 1.3.04 = * **FIX:** Fixed some issues with the Settings page on some servers. @@ -443,7 +444,7 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **TWEAK:** Tweaked the migration feature to support MUCH larger recipe collections. = 1.3.03 = -* **FIX:** Fixes conflicts with the Cooked Pro plugin. +* **FIX:** Fixes conflicts with the [Cooked Pro](https://cooked.pro/) plugin. * **TWEAK:** Updated the language template file. = 1.3.02 = @@ -466,14 +467,14 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **FIX:** Fixed issues with the "Apply to All" template update feature. * **FIX:** Fixed issues with the Default Template saving/loading buttons. * **FIX:** Fixed an issue where "Authors" could not edit recipes. -* **FIX:** Fixed an issue with WPML not being able to translate recipe information. +* **FIX:** Fixed an issue with [WPML](https://wpml.org/) not being able to translate recipe information. = 1.2.0 = * **NEW:** **"Cooked - Recipe Search" Widget** — Display the recipe search form. * **NEW:** `[cooked-search]` — Display the recipe search form. * **NEW:** Added REST API support to recipes and recipe categories. * **TWEAK:** Added the same "search" shortcode options to `[cooked-browse]` so you can customize the recipe search bar from that shortcode as well. See the documentation for more shortcode options. -* **TWEAK:** Added some hooks and filters to the welcome screen to add the ability to include the Cooked Pro changelog information there as well. +* **TWEAK:** Added some hooks and filters to the welcome screen to add the ability to include the [Cooked Pro](https://cooked.pro/) changelog information there as well. * **TWEAK:** Direction images are formatted much better now (inline with the text and some margin below). * **TWEAK:** Added an option to disable the "Servings Switcher". * **TWEAK:** Converted all CSS "em" values to "rem" values. @@ -482,25 +483,25 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty * **FIX:** Added some missing language strings. = 1.1.13 = -* **NEW:** Added kg (kilograms) as a measurement option. +* **NEW:** Added `kg` (kilograms) as a measurement option. * **FIX:** Fixed an issue where zeros were being removed from large numbers. * **FIX:** Recipes will now 404 if "Disable Public Recipes" is active. * **FIX:** Minor CSS adjustments throughout. = 1.1.12 = -* Adjusted some code to support the upcoming Cooked Pro features. +* Adjusted some code to support the upcoming [Cooked Pro](https://cooked.pro/) features. * Some minor text changes in the Settings panel. = 1.1.11 = * **FIX:** Fixed an issue with ingredient amounts getting rounded up to 1. -* **FIX:** Fixed some theme compatibiltiy issues. +* **FIX:** Fixed some theme compatibility issues. * **FIX:** Re-enabled structured data for recipes. Didn't mean to disable this, sorry! = 1.1.10 = * **NEW:** Ingredient amounts will now display as entered (fractions or decimals) in the number format based on your language settings. * **NEW:** Added taxonomy filter dropdowns to the admin recipe list page. * **NEW:** Added developer filters for customizing the "Percent Daily Value" calculations. -* **FIX:** Added compatibility for the "Bridge" theme. +* **FIX:** Added compatibility for the [Bridge](https://bridgetheme.com/) theme. = 1.1.9 = * **FIX:** Added "1/5" support to measurements. @@ -510,19 +511,19 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.1.8 = * **NEW:** HTML is allowed in all ingredient/direction fields. * **FIX:** Fixed some redirect issues. -* **FIX:** Some adjustments to support the upcoming Cooked Pro. +* **FIX:** Some adjustments to support the upcoming [Cooked Pro](https://cooked.pro/) plugin. = 1.1.7 = * **FIX:** Fixed an issue with the Cooked settings screens if a non-English language is enabled. -* **FIX:** Fixed an issue for when the "Browse Recipe Page" and "Single Recipe Post" slugs were the same (i.e. /recipes/). You can now use the same slug for both! +* **FIX:** Fixed an issue for when the "Browse Recipe Page" and "Single Recipe Post" slugs were the same (i.e. `/recipes/`). You can now use the same slug for both! = 1.1.6 = * **NEW:** Tested and working in WordPress 4.8! * **NEW:** Custom checkbox toggles on the Settings page. -* **FIX:** Fixed an issue with category redirects. There was a double slash being added that has now been resolved. Huge thanks to **@travelnlass** and **@kitcatsz** for finding this one! +* **FIX:** Fixed an issue with category redirects. There was a double slash being added that has now been resolved. Huge thanks to [@travelnlass](https://profiles.wordpress.org/travelnlass/) and [@kitcatsz](https://profiles.wordpress.org/kitcatsz/) for finding this one! = 1.1.5 = -* **FIX:** A lot more fixes for the [cooked-recipe] shortcode. Huge thanks to Zoe and Mariana for donating their time and websites to help me work out these issues! +* **FIX:** A lot more fixes for the `[cooked-recipe]` shortcode. Huge thanks to Zoe and Mariana for donating their time and websites to help me work out these issues! * **NEW:** Added an advanced ability to "Disable Cooked `` Tags" when needed. * **NEW:** Added an advanced ability to "Disable Public Recipes" when needed. @@ -536,18 +537,18 @@ Version 1.14.0 adds bulk ingredient and direction entry, validates shortcode sty = 1.1.2 = * **FIX:** Fixed an error on the recipe author template. -* **FIX:** More minor tweaks to support the upcoming Cooked Pro plugin. +* **FIX:** More minor tweaks to support the upcoming [Cooked Pro](https://cooked.pro/) plugin. = 1.1.1 = -* **FIX:** Compatibility improvements with the Yoast SEO plugin. -* **FIX:** Some minor tweaks to support the upcoming Cooked Pro plugin. +* **FIX:** Compatibility improvements with the [Yoast SEO](https://wordpress.org/plugins/wordpress-seo/) plugin. +* **FIX:** Some minor tweaks to support the upcoming [Cooked Pro](https://cooked.pro/) plugin. = 1.1.0 = * **NEW:** **Full-Screen Mode:** Just include "fullscreen" in the `[cooked-info]` shortcode. Really shines on mobile devices! * **NEW:** **Printable Recipes:** Just include "print" in the `[cooked-info]`shortcode. Includes some handy "a-la-carte" print options. * **FIX:** Some adjustments for layouts on smaller devices (responsive fixes). * **FIX:** Fixed an issue where quantities and amounts would not show up without a "Servings" setting. Now it works no matter what! -* **FIX:** Minor code adjustments to better support Cooked Pro. +* **FIX:** Minor code adjustments to better support [Cooked Pro](https://cooked.pro/). = 1.0.0 = * **NEW:** *Everything is new!* diff --git a/tests/phpunit/CSVImportTest.php b/tests/phpunit/CSVImportTest.php new file mode 100644 index 0000000..130cdf7 --- /dev/null +++ b/tests/phpunit/CSVImportTest.php @@ -0,0 +1,73 @@ +setAccessible( true ); + return $ref->invoke( null, ...$args ); + } + + public function test_parse_ingredient_parts_returns_false_for_empty() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'parse_ingredient_parts', [ [], $measurements ] ); + $this->assertFalse( $result ); + } + + public function test_parse_ingredient_parts_with_three_parts() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'parse_ingredient_parts', [ [ '2', 'cups', 'Flour' ], $measurements ] ); + $this->assertIsArray( $result ); + $this->assertSame( '2', $result['amount'] ); + $this->assertSame( 'Flour', $result['name'] ); + } + + public function test_parse_ingredient_parts_with_two_parts() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'parse_ingredient_parts', [ [ '3', 'Eggs' ], $measurements ] ); + $this->assertIsArray( $result ); + $this->assertSame( '3', $result['amount'] ); + $this->assertSame( 'Eggs', $result['name'] ); + $this->assertSame( '', $result['measurement'] ); + } + + public function test_parse_ingredient_parts_with_one_part() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'parse_ingredient_parts', [ [ 'Salt' ], $measurements ] ); + $this->assertIsArray( $result ); + $this->assertSame( 'Salt', $result['name'] ); + $this->assertSame( '', $result['amount'] ); + } + + public function test_match_measurement_exact_key() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'match_measurement', [ 'cup', $measurements ] ); + $this->assertSame( 'cup', $result ); + } + + public function test_match_measurement_singular() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'match_measurement', [ 'teaspoon', $measurements ] ); + $this->assertSame( 'tsp', $result ); + } + + public function test_match_measurement_plural() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'match_measurement', [ 'teaspoons', $measurements ] ); + $this->assertSame( 'tsp', $result ); + } + + public function test_match_measurement_case_insensitive() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'match_measurement', [ 'CUP', $measurements ] ); + $this->assertSame( 'cup', $result ); + } + + public function test_match_measurement_returns_false_for_unknown() { + $measurements = Cooked_Measurements::get(); + $result = self::call_private_method( 'match_measurement', [ 'furlong', $measurements ] ); + $this->assertFalse( $result ); + } +} diff --git a/tests/phpunit/EnqueuesTest.php b/tests/phpunit/EnqueuesTest.php new file mode 100644 index 0000000..e5f61fe --- /dev/null +++ b/tests/phpunit/EnqueuesTest.php @@ -0,0 +1,48 @@ +enqueues = new Cooked_Enqueues(); + } + + public function test_compress_css_removes_newlines() { + $css = ".foo {\n color: red;\n}"; + $result = $this->enqueues->compress_css($css); + $this->assertStringNotContainsString("\n", $result); + } + + public function test_compress_css_removes_tabs() { + $css = ".foo {\tcolor: red;\t}"; + $result = $this->enqueues->compress_css($css); + $this->assertStringNotContainsString("\t", $result); + } + + public function test_compress_css_removes_carriage_returns() { + $css = ".foo {\r\n color: red;\r\n}"; + $result = $this->enqueues->compress_css($css); + $this->assertStringNotContainsString("\r", $result); + } + + public function test_compress_css_removes_double_spaces() { + $css = ".foo { color: red; }"; + $result = $this->enqueues->compress_css($css); + $this->assertStringNotContainsString(' ', $result); + } + + public function test_compress_css_preserves_content() { + $css = ".foo { color: red; }"; + $result = $this->enqueues->compress_css($css); + $this->assertStringContainsString('.foo', $result); + $this->assertStringContainsString('color: red;', $result); + } + + public function test_compress_css_empty_string() { + $result = $this->enqueues->compress_css(''); + $this->assertSame('', $result); + } +} diff --git a/tests/phpunit/FunctionsExtraTest.php b/tests/phpunit/FunctionsExtraTest.php new file mode 100644 index 0000000..7341214 --- /dev/null +++ b/tests/phpunit/FunctionsExtraTest.php @@ -0,0 +1,43 @@ +assertIsString($result); + $this->assertNotEmpty($result); + } + + public function test_set_transient_message_empty_returns_null() { + $result = Cooked_Functions::set_transient_message(''); + $this->assertNull($result); + } + + public function test_get_and_delete_transient_message_returns_false_when_none() { + $result = Cooked_Functions::get_and_delete_transient_message(); + $this->assertFalse($result); + } + + public function test_parse_readme_changelog_returns_string() { + $result = Cooked_Functions::parse_readme_changelog(COOKED_DIR . 'readme.txt', 'What\'s New'); + $this->assertIsString($result); + } + + public function test_parse_readme_changelog_contains_title() { + $result = Cooked_Functions::parse_readme_changelog(COOKED_DIR . 'readme.txt', 'Custom Title'); + $this->assertStringContainsString('Custom Title', $result); + } + + public function test_sanitize_text_field_encodes_html() { + $result = Cooked_Functions::sanitize_text_field('test'); + $this->assertStringContainsString('<', $result); + $this->assertStringNotContainsString('', $result); + } + + public function test_sanitize_text_field_strips_slashes() { + $result = Cooked_Functions::sanitize_text_field("O\'Brien"); + $this->assertStringNotContainsString('\\', $result); + } +} diff --git a/tests/phpunit/FunctionsTest.php b/tests/phpunit/FunctionsTest.php new file mode 100644 index 0000000..5079a86 --- /dev/null +++ b/tests/phpunit/FunctionsTest.php @@ -0,0 +1,113 @@ +assertSame( '255,0,0', Cooked_Functions::hex2rgb( '#ff0000' ) ); + } + + /** + * Test hex2rgb converts pure green. + */ + public function test_hex2rgb_green() { + $this->assertSame( '0,255,0', Cooked_Functions::hex2rgb( '#00ff00' ) ); + } + + /** + * Test hex2rgb converts pure blue. + */ + public function test_hex2rgb_blue() { + $this->assertSame( '0,0,255', Cooked_Functions::hex2rgb( '#0000ff' ) ); + } + + /** + * Test hex2rgb converts black. + */ + public function test_hex2rgb_black() { + $this->assertSame( '0,0,0', Cooked_Functions::hex2rgb( '#000000' ) ); + } + + /** + * Test hex2rgb converts white. + */ + public function test_hex2rgb_white() { + $this->assertSame( '255,255,255', Cooked_Functions::hex2rgb( '#ffffff' ) ); + } + + /** + * Test hex2rgb converts mixed color. + */ + public function test_hex2rgb_mixed() { + $this->assertSame( '22,167,128', Cooked_Functions::hex2rgb( '#16a780' ) ); + } + + /** + * Test array_splice_assoc splices by numeric offset. + */ + public function test_array_splice_assoc_numeric_offset() { + $input = [ 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4 ]; + Cooked_Functions::array_splice_assoc( $input, 1, 1 ); + + $this->assertSame( [ 'a' => 1, 'c' => 3, 'd' => 4 ], $input ); + } + + /** + * Test array_splice_assoc splices by string key (offset is position after the key). + */ + public function test_array_splice_assoc_string_key() { + $input = [ 'first' => 1, 'second' => 2, 'third' => 3 ]; + Cooked_Functions::array_splice_assoc( $input, 'first', 1 ); + + $this->assertSame( [ 'first' => 1, 'third' => 3 ], $input ); + } + + /** + * Test array_splice_assoc replaces with new elements. + */ + public function test_array_splice_assoc_with_replacement() { + $input = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + Cooked_Functions::array_splice_assoc( $input, 1, 1, [ 'x' => 99 ] ); + + $this->assertSame( [ 'a' => 1, 'x' => 99, 'c' => 3 ], $input ); + } + + /** + * Test array_splice_assoc handles empty replacement. + */ + public function test_array_splice_assoc_empty_replacement() { + $input = [ 'a' => 1, 'b' => 2, 'c' => 3 ]; + Cooked_Functions::array_splice_assoc( $input, 0, 1, [] ); + + $this->assertSame( [ 'b' => 2, 'c' => 3 ], $input ); + } + + /** + * Test array_splice_assoc does nothing on non-array input. + */ + public function test_array_splice_assoc_non_array() { + $input = 'not an array'; + Cooked_Functions::array_splice_assoc( $input, 0, 1 ); + + $this->assertSame( 'not an array', $input ); + } + + /** + * Test sanitize_text_field strips tags and slashes. + */ + public function test_sanitize_text_field_strips_tags() { + $result = Cooked_Functions::sanitize_text_field( '' ); + + $this->assertStringNotContainsString( 'My Recipe']; + $result = Cooked_Recipe_Meta::meta_cleanup($input); + $this->assertStringNotContainsString('