diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..08db71e --- /dev/null +++ b/.gitignore @@ -0,0 +1,9 @@ +.idea/ +vendor/ +node_modules/ +composer.lock +yarn.lock +.phpunit.cache +.phpunit.result.cache +.phpactor.json +CLAUDE.md diff --git a/composer.json b/composer.json index 8cd4562..42687ff 100644 --- a/composer.json +++ b/composer.json @@ -1,7 +1,7 @@ { "name": "roelmagdaleno/markdown-to-notion-blocks", "description": "Convert markdown to Notion blocks.", - "version": "1.2.2", + "version": "1.3.0", "homepage": "https://github.com/roelmagdaleno/markdown-to-notion-blocks", "license": "MIT", "keywords": [ @@ -22,13 +22,14 @@ } ], "require": { - "php": "^8.0", - "league/commonmark": "^2.5", - "incenteev/emoji-pattern": "^1.3" + "php": "^8.1", + "league/commonmark": "^2.7", + "incenteev/emoji-pattern": "^v1.4" }, "require-dev": { - "pestphp/pest": "^1.23", - "phpstan/phpstan": "^1.12" + "pestphp/pest": "^v3.8.2", + "phpstan/phpstan": "^1.12", + "laravel/pint": "^1.22" }, "scripts": { "test": "vendor/bin/pest" diff --git a/composer.lock b/composer.lock index d661e47..f2fb3e6 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "9f8a73be68c8e684aeada714c54bcb3c", + "content-hash": "16adb37366e1f0639c24b894affccdf0", "packages": [ { "name": "dflydev/dot-access-data", @@ -131,16 +131,16 @@ }, { "name": "league/commonmark", - "version": "2.6.0", + "version": "2.7.0", "source": { "type": "git", "url": "https://github.com/thephpleague/commonmark.git", - "reference": "d150f911e0079e90ae3c106734c93137c184f932" + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/d150f911e0079e90ae3c106734c93137c184f932", - "reference": "d150f911e0079e90ae3c106734c93137c184f932", + "url": "https://api.github.com/repos/thephpleague/commonmark/zipball/6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", + "reference": "6fbb36d44824ed4091adbcf4c7d4a3923cdb3405", "shasum": "" }, "require": { @@ -177,7 +177,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "2.7-dev" + "dev-main": "2.8-dev" } }, "autoload": { @@ -234,7 +234,7 @@ "type": "tidelift" } ], - "time": "2024-12-07T15:34:16+00:00" + "time": "2025-05-05T12:20:28+00:00" }, { "name": "league/config", @@ -382,16 +382,16 @@ }, { "name": "nette/utils", - "version": "v4.0.5", + "version": "v4.0.7", "source": { "type": "git", "url": "https://github.com/nette/utils.git", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96" + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nette/utils/zipball/736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", - "reference": "736c567e257dbe0fcf6ce81b4d6dbe05c6899f96", + "url": "https://api.github.com/repos/nette/utils/zipball/e67c4061eb40b9c113b218214e42cb5a0dda28f2", + "reference": "e67c4061eb40b9c113b218214e42cb5a0dda28f2", "shasum": "" }, "require": { @@ -462,9 +462,9 @@ ], "support": { "issues": "https://github.com/nette/utils/issues", - "source": "https://github.com/nette/utils/tree/v4.0.5" + "source": "https://github.com/nette/utils/tree/v4.0.7" }, - "time": "2024-08-07T15:39:19+00:00" + "time": "2025-06-03T04:55:08+00:00" }, { "name": "psr/event-dispatcher", @@ -518,16 +518,16 @@ }, { "name": "symfony/deprecation-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/deprecation-contracts.git", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6" + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", - "reference": "74c71c939a79f7d5bf3c1ce9f5ea37ba0114c6f6", + "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62", + "reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62", "shasum": "" }, "require": { @@ -535,12 +535,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -565,7 +565,7 @@ "description": "A generic function and convention to trigger deprecation notices", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/deprecation-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0" }, "funding": [ { @@ -581,20 +581,20 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2024-09-25T14:21:43+00:00" }, { "name": "symfony/polyfill-php80", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-php80.git", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8" + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", - "reference": "60328e362d4c2c802a54fcbf04f9d3fb892b4cf8", + "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608", + "reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608", "shasum": "" }, "require": { @@ -645,7 +645,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-php80/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-php80/tree/v1.32.0" }, "funding": [ { @@ -661,41 +661,61 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2025-01-02T08:10:11+00:00" } ], "packages-dev": [ { - "name": "doctrine/instantiator", - "version": "2.0.0", + "name": "brianium/paratest", + "version": "v7.8.3", "source": { "type": "git", - "url": "https://github.com/doctrine/instantiator.git", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0" + "url": "https://github.com/paratestphp/paratest.git", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/doctrine/instantiator/zipball/c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", - "reference": "c6222283fa3f4ac679f8b9ced9a4e23f163e80d0", + "url": "https://api.github.com/repos/paratestphp/paratest/zipball/a585c346ddf1bec22e51e20b5387607905604a71", + "reference": "a585c346ddf1bec22e51e20b5387607905604a71", "shasum": "" }, "require": { - "php": "^8.1" + "ext-dom": "*", + "ext-pcre": "*", + "ext-reflection": "*", + "ext-simplexml": "*", + "fidry/cpu-core-counter": "^1.2.0", + "jean85/pretty-package-versions": "^2.1.0", + "php": "~8.2.0 || ~8.3.0 || ~8.4.0", + "phpunit/php-code-coverage": "^11.0.9 || ^12.0.4", + "phpunit/php-file-iterator": "^5.1.0 || ^6", + "phpunit/php-timer": "^7.0.1 || ^8", + "phpunit/phpunit": "^11.5.11 || ^12.0.6", + "sebastian/environment": "^7.2.0 || ^8", + "symfony/console": "^6.4.17 || ^7.2.1", + "symfony/process": "^6.4.19 || ^7.2.4" }, "require-dev": { - "doctrine/coding-standard": "^11", - "ext-pdo": "*", - "ext-phar": "*", - "phpbench/phpbench": "^1.2", - "phpstan/phpstan": "^1.9.4", - "phpstan/phpstan-phpunit": "^1.3", - "phpunit/phpunit": "^9.5.27", - "vimeo/psalm": "^5.4" + "doctrine/coding-standard": "^12.0.0", + "ext-pcov": "*", + "ext-posix": "*", + "phpstan/phpstan": "^2.1.6", + "phpstan/phpstan-deprecation-rules": "^2.0.1", + "phpstan/phpstan-phpunit": "^2.0.4", + "phpstan/phpstan-strict-rules": "^2.0.3", + "squizlabs/php_codesniffer": "^3.11.3", + "symfony/filesystem": "^6.4.13 || ^7.2.0" }, + "bin": [ + "bin/paratest", + "bin/paratest_for_phpstorm" + ], "type": "library", "autoload": { "psr-4": { - "Doctrine\\Instantiator\\": "src/Doctrine/Instantiator/" + "ParaTest\\": [ + "src/" + ] } }, "notification-url": "https://packagist.org/downloads/", @@ -704,49 +724,161 @@ ], "authors": [ { - "name": "Marco Pivetta", - "email": "ocramius@gmail.com", - "homepage": "https://ocramius.github.io/" + "name": "Brian Scaturro", + "email": "scaturrob@gmail.com", + "role": "Developer" + }, + { + "name": "Filippo Tessarotto", + "email": "zoeslam@gmail.com", + "role": "Developer" } ], - "description": "A small, lightweight utility to instantiate objects in PHP without invoking their constructors", - "homepage": "https://www.doctrine-project.org/projects/instantiator.html", + "description": "Parallel testing for PHP", + "homepage": "https://github.com/paratestphp/paratest", "keywords": [ - "constructor", - "instantiate" + "concurrent", + "parallel", + "phpunit", + "testing" ], "support": { - "issues": "https://github.com/doctrine/instantiator/issues", - "source": "https://github.com/doctrine/instantiator/tree/2.0.0" + "issues": "https://github.com/paratestphp/paratest/issues", + "source": "https://github.com/paratestphp/paratest/tree/v7.8.3" }, "funding": [ { - "url": "https://www.doctrine-project.org/sponsorship.html", - "type": "custom" + "url": "https://github.com/sponsors/Slamdunk", + "type": "github" }, { - "url": "https://www.patreon.com/phpdoctrine", - "type": "patreon" - }, + "url": "https://paypal.me/filippotessarotto", + "type": "paypal" + } + ], + "time": "2025-03-05T08:29:11+00:00" + }, + { + "name": "doctrine/deprecations", + "version": "1.1.5", + "source": { + "type": "git", + "url": "https://github.com/doctrine/deprecations.git", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/doctrine/deprecations/zipball/459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "reference": "459c2f5dd3d6a4633d3b5f46ee2b1c40f57d3f38", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "phpunit/phpunit": "<=7.5 || >=13" + }, + "require-dev": { + "doctrine/coding-standard": "^9 || ^12 || ^13", + "phpstan/phpstan": "1.4.10 || 2.1.11", + "phpstan/phpstan-phpunit": "^1.0 || ^2", + "phpunit/phpunit": "^7.5 || ^8.5 || ^9.6 || ^10.5 || ^11.5 || ^12", + "psr/log": "^1 || ^2 || ^3" + }, + "suggest": { + "psr/log": "Allows logging deprecations via PSR-3 logger implementation" + }, + "type": "library", + "autoload": { + "psr-4": { + "Doctrine\\Deprecations\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "A small layer on top of trigger_error(E_USER_DEPRECATED) or PSR-3 logging with options to disable all deprecations or selectively for packages.", + "homepage": "https://www.doctrine-project.org/", + "support": { + "issues": "https://github.com/doctrine/deprecations/issues", + "source": "https://github.com/doctrine/deprecations/tree/1.1.5" + }, + "time": "2025-04-07T20:06:18+00:00" + }, + { + "name": "fidry/cpu-core-counter", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/theofidry/cpu-core-counter.git", + "reference": "8520451a140d3f46ac33042715115e290cf5785f" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theofidry/cpu-core-counter/zipball/8520451a140d3f46ac33042715115e290cf5785f", + "reference": "8520451a140d3f46ac33042715115e290cf5785f", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "require-dev": { + "fidry/makefile": "^0.2.0", + "fidry/php-cs-fixer-config": "^1.1.2", + "phpstan/extension-installer": "^1.2.0", + "phpstan/phpstan": "^1.9.2", + "phpstan/phpstan-deprecation-rules": "^1.0.0", + "phpstan/phpstan-phpunit": "^1.2.2", + "phpstan/phpstan-strict-rules": "^1.4.4", + "phpunit/phpunit": "^8.5.31 || ^9.5.26", + "webmozarts/strict-phpunit": "^7.5" + }, + "type": "library", + "autoload": { + "psr-4": { + "Fidry\\CpuCoreCounter\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ { - "url": "https://tidelift.com/funding/github/packagist/doctrine%2Finstantiator", - "type": "tidelift" + "name": "Théo FIDRY", + "email": "theo.fidry@gmail.com" + } + ], + "description": "Tiny utility to get the number of CPU cores.", + "keywords": [ + "CPU", + "core" + ], + "support": { + "issues": "https://github.com/theofidry/cpu-core-counter/issues", + "source": "https://github.com/theofidry/cpu-core-counter/tree/1.2.0" + }, + "funding": [ + { + "url": "https://github.com/theofidry", + "type": "github" } ], - "time": "2022-12-30T00:23:10+00:00" + "time": "2024-08-06T10:04:20+00:00" }, { "name": "filp/whoops", - "version": "2.16.0", + "version": "2.18.3", "source": { "type": "git", "url": "https://github.com/filp/whoops.git", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2" + "reference": "59a123a3d459c5a23055802237cb317f609867e5" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/filp/whoops/zipball/befcdc0e5dce67252aa6322d82424be928214fa2", - "reference": "befcdc0e5dce67252aa6322d82424be928214fa2", + "url": "https://api.github.com/repos/filp/whoops/zipball/59a123a3d459c5a23055802237cb317f609867e5", + "reference": "59a123a3d459c5a23055802237cb317f609867e5", "shasum": "" }, "require": { @@ -796,7 +928,7 @@ ], "support": { "issues": "https://github.com/filp/whoops/issues", - "source": "https://github.com/filp/whoops/tree/2.16.0" + "source": "https://github.com/filp/whoops/tree/2.18.3" }, "funding": [ { @@ -804,20 +936,146 @@ "type": "github" } ], - "time": "2024-09-25T12:00:00+00:00" + "time": "2025-06-16T00:02:10+00:00" + }, + { + "name": "jean85/pretty-package-versions", + "version": "2.1.1", + "source": { + "type": "git", + "url": "https://github.com/Jean85/pretty-package-versions.git", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/Jean85/pretty-package-versions/zipball/4d7aa5dab42e2a76d99559706022885de0e18e1a", + "reference": "4d7aa5dab42e2a76d99559706022885de0e18e1a", + "shasum": "" + }, + "require": { + "composer-runtime-api": "^2.1.0", + "php": "^7.4|^8.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.2", + "jean85/composer-provided-replaced-stub-package": "^1.0", + "phpstan/phpstan": "^2.0", + "phpunit/phpunit": "^7.5|^8.5|^9.6", + "rector/rector": "^2.0", + "vimeo/psalm": "^4.3 || ^5.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "Jean85\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Alessandro Lai", + "email": "alessandro.lai85@gmail.com" + } + ], + "description": "A library to get pretty versions strings of installed dependencies", + "keywords": [ + "composer", + "package", + "release", + "versions" + ], + "support": { + "issues": "https://github.com/Jean85/pretty-package-versions/issues", + "source": "https://github.com/Jean85/pretty-package-versions/tree/2.1.1" + }, + "time": "2025-03-19T14:43:43+00:00" + }, + { + "name": "laravel/pint", + "version": "v1.22.1", + "source": { + "type": "git", + "url": "https://github.com/laravel/pint.git", + "reference": "941d1927c5ca420c22710e98420287169c7bcaf7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/laravel/pint/zipball/941d1927c5ca420c22710e98420287169c7bcaf7", + "reference": "941d1927c5ca420c22710e98420287169c7bcaf7", + "shasum": "" + }, + "require": { + "ext-json": "*", + "ext-mbstring": "*", + "ext-tokenizer": "*", + "ext-xml": "*", + "php": "^8.2.0" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "^3.75.0", + "illuminate/view": "^11.44.7", + "larastan/larastan": "^3.4.0", + "laravel-zero/framework": "^11.36.1", + "mockery/mockery": "^1.6.12", + "nunomaduro/termwind": "^2.3.1", + "pestphp/pest": "^2.36.0" + }, + "bin": [ + "builds/pint" + ], + "type": "project", + "autoload": { + "psr-4": { + "App\\": "app/", + "Database\\Seeders\\": "database/seeders/", + "Database\\Factories\\": "database/factories/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "An opinionated code formatter for PHP.", + "homepage": "https://laravel.com", + "keywords": [ + "format", + "formatter", + "lint", + "linter", + "php" + ], + "support": { + "issues": "https://github.com/laravel/pint/issues", + "source": "https://github.com/laravel/pint" + }, + "time": "2025-05-08T08:38:12+00:00" }, { "name": "myclabs/deep-copy", - "version": "1.12.1", + "version": "1.13.1", "source": { "type": "git", "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845" + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/123267b2c49fbf30d78a7b2d333f6be754b94845", - "reference": "123267b2c49fbf30d78a7b2d333f6be754b94845", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/1720ddd719e16cf0db4eb1c6eca108031636d46c", + "reference": "1720ddd719e16cf0db4eb1c6eca108031636d46c", "shasum": "" }, "require": { @@ -856,7 +1114,7 @@ ], "support": { "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.12.1" + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.1" }, "funding": [ { @@ -864,20 +1122,20 @@ "type": "tidelift" } ], - "time": "2024-11-08T17:47:46+00:00" + "time": "2025-04-29T12:36:36+00:00" }, { "name": "nikic/php-parser", - "version": "v5.3.1", + "version": "v5.5.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b" + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/8eea230464783aa9671db8eea6f8c6ac5285794b", - "reference": "8eea230464783aa9671db8eea6f8c6ac5285794b", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/ae59794362fe85e051a58ad36b289443f57be7a9", + "reference": "ae59794362fe85e051a58ad36b289443f57be7a9", "shasum": "" }, "require": { @@ -920,51 +1178,61 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.3.1" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.5.0" }, - "time": "2024-10-08T18:51:32+00:00" + "time": "2025-05-31T08:24:38+00:00" }, { "name": "nunomaduro/collision", - "version": "v6.4.0", + "version": "v8.8.2", "source": { "type": "git", "url": "https://github.com/nunomaduro/collision.git", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015" + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nunomaduro/collision/zipball/f05978827b9343cba381ca05b8c7deee346b6015", - "reference": "f05978827b9343cba381ca05b8c7deee346b6015", + "url": "https://api.github.com/repos/nunomaduro/collision/zipball/60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", + "reference": "60207965f9b7b7a4ce15a0f75d57f9dadb105bdb", "shasum": "" }, "require": { - "filp/whoops": "^2.14.5", - "php": "^8.0.0", - "symfony/console": "^6.0.2" + "filp/whoops": "^2.18.1", + "nunomaduro/termwind": "^2.3.1", + "php": "^8.2.0", + "symfony/console": "^7.3.0" + }, + "conflict": { + "laravel/framework": "<11.44.2 || >=13.0.0", + "phpunit/phpunit": "<11.5.15 || >=13.0.0" }, "require-dev": { - "brianium/paratest": "^6.4.1", - "laravel/framework": "^9.26.1", - "laravel/pint": "^1.1.1", - "nunomaduro/larastan": "^1.0.3", - "nunomaduro/mock-final-classes": "^1.1.0", - "orchestra/testbench": "^7.7", - "phpunit/phpunit": "^9.5.23", - "spatie/ignition": "^1.4.1" + "brianium/paratest": "^7.8.3", + "larastan/larastan": "^3.4.2", + "laravel/framework": "^11.44.2 || ^12.18", + "laravel/pint": "^1.22.1", + "laravel/sail": "^1.43.1", + "laravel/sanctum": "^4.1.1", + "laravel/tinker": "^2.10.1", + "orchestra/testbench-core": "^9.12.0 || ^10.4", + "pestphp/pest": "^3.8.2", + "sebastian/environment": "^7.2.1 || ^8.0" }, "type": "library", "extra": { - "branch-alias": { - "dev-develop": "6.x-dev" - }, "laravel": { "providers": [ "NunoMaduro\\Collision\\Adapters\\Laravel\\CollisionServiceProvider" ] + }, + "branch-alias": { + "dev-8.x": "8.x-dev" } }, "autoload": { + "files": [ + "./src/Adapters/Phpunit/Autoload.php" + ], "psr-4": { "NunoMaduro\\Collision\\": "src/" } @@ -979,26 +1247,296 @@ "email": "enunomaduro@gmail.com" } ], - "description": "Cli error handling for console/command-line PHP applications.", + "description": "Cli error handling for console/command-line PHP applications.", + "keywords": [ + "artisan", + "cli", + "command-line", + "console", + "dev", + "error", + "handling", + "laravel", + "laravel-zero", + "php", + "symfony" + ], + "support": { + "issues": "https://github.com/nunomaduro/collision/issues", + "source": "https://github.com/nunomaduro/collision" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://www.patreon.com/nunomaduro", + "type": "patreon" + } + ], + "time": "2025-06-25T02:12:12+00:00" + }, + { + "name": "nunomaduro/termwind", + "version": "v2.3.1", + "source": { + "type": "git", + "url": "https://github.com/nunomaduro/termwind.git", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nunomaduro/termwind/zipball/dfa08f390e509967a15c22493dc0bac5733d9123", + "reference": "dfa08f390e509967a15c22493dc0bac5733d9123", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": "^8.2", + "symfony/console": "^7.2.6" + }, + "require-dev": { + "illuminate/console": "^11.44.7", + "laravel/pint": "^1.22.0", + "mockery/mockery": "^1.6.12", + "pestphp/pest": "^2.36.0 || ^3.8.2", + "phpstan/phpstan": "^1.12.25", + "phpstan/phpstan-strict-rules": "^1.6.2", + "symfony/var-dumper": "^7.2.6", + "thecodingmachine/phpstan-strict-rules": "^1.0.0" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Termwind\\Laravel\\TermwindServiceProvider" + ] + }, + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "files": [ + "src/Functions.php" + ], + "psr-4": { + "Termwind\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Its like Tailwind CSS, but for the console.", + "keywords": [ + "cli", + "console", + "css", + "package", + "php", + "style" + ], + "support": { + "issues": "https://github.com/nunomaduro/termwind/issues", + "source": "https://github.com/nunomaduro/termwind/tree/v2.3.1" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + }, + { + "url": "https://github.com/xiCO2k", + "type": "github" + } + ], + "time": "2025-05-08T08:14:37+00:00" + }, + { + "name": "pestphp/pest", + "version": "v3.8.2", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest.git", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest/zipball/c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "reference": "c6244a8712968dbac88eb998e7ff3b5caa556b0d", + "shasum": "" + }, + "require": { + "brianium/paratest": "^7.8.3", + "nunomaduro/collision": "^8.8.0", + "nunomaduro/termwind": "^2.3.0", + "pestphp/pest-plugin": "^3.0.0", + "pestphp/pest-plugin-arch": "^3.1.0", + "pestphp/pest-plugin-mutate": "^3.0.5", + "php": "^8.2.0", + "phpunit/phpunit": "^11.5.15" + }, + "conflict": { + "filp/whoops": "<2.16.0", + "phpunit/phpunit": ">11.5.15", + "sebastian/exporter": "<6.0.0", + "webmozart/assert": "<1.11.0" + }, + "require-dev": { + "pestphp/pest-dev-tools": "^3.4.0", + "pestphp/pest-plugin-type-coverage": "^3.5.0", + "symfony/process": "^7.2.5" + }, + "bin": [ + "bin/pest" + ], + "type": "library", + "extra": { + "pest": { + "plugins": [ + "Pest\\Mutate\\Plugins\\Mutate", + "Pest\\Plugins\\Configuration", + "Pest\\Plugins\\Bail", + "Pest\\Plugins\\Cache", + "Pest\\Plugins\\Coverage", + "Pest\\Plugins\\Init", + "Pest\\Plugins\\Environment", + "Pest\\Plugins\\Help", + "Pest\\Plugins\\Memory", + "Pest\\Plugins\\Only", + "Pest\\Plugins\\Printer", + "Pest\\Plugins\\ProcessIsolation", + "Pest\\Plugins\\Profile", + "Pest\\Plugins\\Retry", + "Pest\\Plugins\\Snapshot", + "Pest\\Plugins\\Verbose", + "Pest\\Plugins\\Version", + "Pest\\Plugins\\Parallel" + ] + }, + "phpstan": { + "includes": [ + "extension.neon" + ] + } + }, + "autoload": { + "files": [ + "src/Functions.php", + "src/Pest.php" + ], + "psr-4": { + "Pest\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "The elegant PHP Testing Framework.", + "keywords": [ + "framework", + "pest", + "php", + "test", + "testing", + "unit" + ], + "support": { + "issues": "https://github.com/pestphp/pest/issues", + "source": "https://github.com/pestphp/pest/tree/v3.8.2" + }, + "funding": [ + { + "url": "https://www.paypal.com/paypalme/enunomaduro", + "type": "custom" + }, + { + "url": "https://github.com/nunomaduro", + "type": "github" + } + ], + "time": "2025-04-17T10:53:02+00:00" + }, + { + "name": "pestphp/pest-plugin", + "version": "v3.0.0", + "source": { + "type": "git", + "url": "https://github.com/pestphp/pest-plugin.git", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/e79b26c65bc11c41093b10150c1341cc5cdbea83", + "reference": "e79b26c65bc11c41093b10150c1341cc5cdbea83", + "shasum": "" + }, + "require": { + "composer-plugin-api": "^2.0.0", + "composer-runtime-api": "^2.2.2", + "php": "^8.2" + }, + "conflict": { + "pestphp/pest": "<3.0.0" + }, + "require-dev": { + "composer/composer": "^2.7.9", + "pestphp/pest": "^3.0.0", + "pestphp/pest-dev-tools": "^3.0.0" + }, + "type": "composer-plugin", + "extra": { + "class": "Pest\\Plugin\\Manager" + }, + "autoload": { + "psr-4": { + "Pest\\Plugin\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "The Pest plugin manager", "keywords": [ - "artisan", - "cli", - "command-line", - "console", - "error", - "handling", - "laravel", - "laravel-zero", + "framework", + "manager", + "pest", "php", - "symfony" + "plugin", + "test", + "testing", + "unit" ], "support": { - "issues": "https://github.com/nunomaduro/collision/issues", - "source": "https://github.com/nunomaduro/collision" + "source": "https://github.com/pestphp/pest-plugin/tree/v3.0.0" }, "funding": [ { - "url": "https://www.paypal.com/paypalme/enunomaduro", + "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", "type": "custom" }, { @@ -1010,88 +1548,65 @@ "type": "patreon" } ], - "time": "2023-01-03T12:54:54+00:00" + "time": "2024-09-08T23:21:41+00:00" }, { - "name": "pestphp/pest", - "version": "v1.23.1", + "name": "pestphp/pest-plugin-arch", + "version": "v3.1.1", "source": { "type": "git", - "url": "https://github.com/pestphp/pest.git", - "reference": "5c56ad8772b89611c72a07e23f6e30aa29dc677a" + "url": "https://github.com/pestphp/pest-plugin-arch.git", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest/zipball/5c56ad8772b89611c72a07e23f6e30aa29dc677a", - "reference": "5c56ad8772b89611c72a07e23f6e30aa29dc677a", + "url": "https://api.github.com/repos/pestphp/pest-plugin-arch/zipball/db7bd9cb1612b223e16618d85475c6f63b9c8daa", + "reference": "db7bd9cb1612b223e16618d85475c6f63b9c8daa", "shasum": "" }, "require": { - "nunomaduro/collision": "^5.11.0|^6.4.0", - "pestphp/pest-plugin": "^1.1.0", - "php": "^7.3 || ^8.0", - "phpunit/phpunit": "^9.6.10" + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "ta-tikoma/phpunit-architecture-test": "^0.8.4" }, "require-dev": { - "illuminate/console": "^8.83.27", - "illuminate/support": "^8.83.27", - "laravel/dusk": "^6.25.2", - "pestphp/pest-dev-tools": "^1.0.0", - "pestphp/pest-plugin-parallel": "^1.2.1" + "pestphp/pest": "^3.8.1", + "pestphp/pest-dev-tools": "^3.4.0" }, - "bin": [ - "bin/pest" - ], "type": "library", "extra": { "pest": { "plugins": [ - "Pest\\Plugins\\Coverage", - "Pest\\Plugins\\Init", - "Pest\\Plugins\\Version", - "Pest\\Plugins\\Environment" - ] - }, - "laravel": { - "providers": [ - "Pest\\Laravel\\PestServiceProvider" + "Pest\\Arch\\Plugin" ] - }, - "branch-alias": { - "dev-1.x": "1.x-dev" } }, "autoload": { "files": [ - "src/Functions.php", - "src/Pest.php" + "src/Autoload.php" ], "psr-4": { - "Pest\\": "src/" + "Pest\\Arch\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "authors": [ - { - "name": "Nuno Maduro", - "email": "enunomaduro@gmail.com" - } - ], - "description": "An elegant PHP Testing Framework.", + "description": "The Arch plugin for Pest PHP.", "keywords": [ + "arch", + "architecture", "framework", "pest", "php", + "plugin", "test", "testing", "unit" ], "support": { - "issues": "https://github.com/pestphp/pest/issues", - "source": "https://github.com/pestphp/pest/tree/v1.23.1" + "source": "https://github.com/pestphp/pest-plugin-arch/tree/v3.1.1" }, "funding": [ { @@ -1103,54 +1618,54 @@ "type": "github" } ], - "time": "2023-07-12T19:42:47+00:00" + "time": "2025-04-16T22:59:48+00:00" }, { - "name": "pestphp/pest-plugin", - "version": "v1.1.0", + "name": "pestphp/pest-plugin-mutate", + "version": "v3.0.5", "source": { "type": "git", - "url": "https://github.com/pestphp/pest-plugin.git", - "reference": "606c5f79c6a339b49838ffbee0151ca519efe378" + "url": "https://github.com/pestphp/pest-plugin-mutate.git", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/pestphp/pest-plugin/zipball/606c5f79c6a339b49838ffbee0151ca519efe378", - "reference": "606c5f79c6a339b49838ffbee0151ca519efe378", + "url": "https://api.github.com/repos/pestphp/pest-plugin-mutate/zipball/e10dbdc98c9e2f3890095b4fe2144f63a5717e08", + "reference": "e10dbdc98c9e2f3890095b4fe2144f63a5717e08", "shasum": "" }, "require": { - "composer-plugin-api": "^1.1.0 || ^2.0.0", - "php": "^7.3 || ^8.0" - }, - "conflict": { - "pestphp/pest": "<1.0" + "nikic/php-parser": "^5.2.0", + "pestphp/pest-plugin": "^3.0.0", + "php": "^8.2", + "psr/simple-cache": "^3.0.0" }, "require-dev": { - "composer/composer": "^2.4.2", - "pestphp/pest": "^1.22.1", - "pestphp/pest-dev-tools": "^1.0.0" - }, - "type": "composer-plugin", - "extra": { - "branch-alias": { - "dev-master": "1.x-dev" - }, - "class": "Pest\\Plugin\\Manager" + "pestphp/pest": "^3.0.8", + "pestphp/pest-dev-tools": "^3.0.0", + "pestphp/pest-plugin-type-coverage": "^3.0.0" }, + "type": "library", "autoload": { "psr-4": { - "Pest\\Plugin\\": "src/" + "Pest\\Mutate\\": "src/" } }, "notification-url": "https://packagist.org/downloads/", "license": [ "MIT" ], - "description": "The Pest plugin manager", + "authors": [ + { + "name": "Sandro Gehri", + "email": "sandrogehri@gmail.com" + } + ], + "description": "Mutates your code to find untested cases", "keywords": [ "framework", - "manager", + "mutate", + "mutation", "pest", "php", "plugin", @@ -1159,23 +1674,23 @@ "unit" ], "support": { - "source": "https://github.com/pestphp/pest-plugin/tree/v1.1.0" + "source": "https://github.com/pestphp/pest-plugin-mutate/tree/v3.0.5" }, "funding": [ { - "url": "https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=66BYDWAT92N6L", + "url": "https://www.paypal.com/paypalme/enunomaduro", "type": "custom" }, { - "url": "https://github.com/nunomaduro", + "url": "https://github.com/gehrisandro", "type": "github" }, { - "url": "https://www.patreon.com/nunomaduro", - "type": "patreon" + "url": "https://github.com/nunomaduro", + "type": "github" } ], - "time": "2022-09-18T13:18:17+00:00" + "time": "2024-09-22T07:54:40+00:00" }, { "name": "phar-io/manifest", @@ -1295,18 +1810,240 @@ }, "time": "2022-02-21T01:04:05+00:00" }, + { + "name": "phpdocumentor/reflection-common", + "version": "2.2.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionCommon.git", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionCommon/zipball/1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "reference": "1d01c49d4ed62f25aa84a747ad35d5a16924662b", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-2.x": "2.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "Common reflection classes used by phpdocumentor to reflect the code structure", + "homepage": "http://www.phpdoc.org", + "keywords": [ + "FQSEN", + "phpDocumentor", + "phpdoc", + "reflection", + "static analysis" + ], + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionCommon/issues", + "source": "https://github.com/phpDocumentor/ReflectionCommon/tree/2.x" + }, + "time": "2020-06-27T09:03:43+00:00" + }, + { + "name": "phpdocumentor/reflection-docblock", + "version": "5.6.2", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/ReflectionDocBlock.git", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/ReflectionDocBlock/zipball/92dde6a5919e34835c506ac8c523ef095a95ed62", + "reference": "92dde6a5919e34835c506ac8c523ef095a95ed62", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.1", + "ext-filter": "*", + "php": "^7.4 || ^8.0", + "phpdocumentor/reflection-common": "^2.2", + "phpdocumentor/type-resolver": "^1.7", + "phpstan/phpdoc-parser": "^1.7|^2.0", + "webmozart/assert": "^1.9.1" + }, + "require-dev": { + "mockery/mockery": "~1.3.5 || ~1.6.0", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-mockery": "^1.1", + "phpstan/phpstan-webmozart-assert": "^1.2", + "phpunit/phpunit": "^9.5", + "psalm/phar": "^5.26" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + }, + { + "name": "Jaap van Otterdijk", + "email": "opensource@ijaap.nl" + } + ], + "description": "With this component, a library can provide support for annotations via DocBlocks or otherwise retrieve information that is embedded in a DocBlock.", + "support": { + "issues": "https://github.com/phpDocumentor/ReflectionDocBlock/issues", + "source": "https://github.com/phpDocumentor/ReflectionDocBlock/tree/5.6.2" + }, + "time": "2025-04-13T19:20:35+00:00" + }, + { + "name": "phpdocumentor/type-resolver", + "version": "1.10.0", + "source": { + "type": "git", + "url": "https://github.com/phpDocumentor/TypeResolver.git", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "reference": "679e3ce485b99e84c775d28e2e96fade9a7fb50a", + "shasum": "" + }, + "require": { + "doctrine/deprecations": "^1.0", + "php": "^7.3 || ^8.0", + "phpdocumentor/reflection-common": "^2.0", + "phpstan/phpdoc-parser": "^1.18|^2.0" + }, + "require-dev": { + "ext-tokenizer": "*", + "phpbench/phpbench": "^1.2", + "phpstan/extension-installer": "^1.1", + "phpstan/phpstan": "^1.8", + "phpstan/phpstan-phpunit": "^1.1", + "phpunit/phpunit": "^9.5", + "rector/rector": "^0.13.9", + "vimeo/psalm": "^4.25" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-1.x": "1.x-dev" + } + }, + "autoload": { + "psr-4": { + "phpDocumentor\\Reflection\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Mike van Riel", + "email": "me@mikevanriel.com" + } + ], + "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", + "support": { + "issues": "https://github.com/phpDocumentor/TypeResolver/issues", + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.10.0" + }, + "time": "2024-11-09T15:12:26+00:00" + }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.1.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "reference": "9b30d6fd026b2c132b3985ce6b23bec09ab3aa68", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.1.0" + }, + "time": "2025-02-19T13:28:12+00:00" + }, { "name": "phpstan/phpstan", - "version": "1.12.12", + "version": "1.12.27", "source": { "type": "git", "url": "https://github.com/phpstan/phpstan.git", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0" + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", - "reference": "b5ae1b88f471d3fd4ba1aa0046234b5ca3776dd0", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/3a6e423c076ab39dfedc307e2ac627ef579db162", + "reference": "3a6e423c076ab39dfedc307e2ac627ef579db162", "shasum": "" }, "require": { @@ -1351,39 +2088,39 @@ "type": "github" } ], - "time": "2024-11-28T22:13:23+00:00" + "time": "2025-05-21T20:51:45+00:00" }, { "name": "phpunit/php-code-coverage", - "version": "9.2.32", + "version": "11.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-code-coverage.git", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5" + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5", - "reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/1a800a7446add2d79cc6b3c01c45381810367d76", + "reference": "1a800a7446add2d79cc6b3c01c45381810367d76", "shasum": "" }, "require": { "ext-dom": "*", "ext-libxml": "*", "ext-xmlwriter": "*", - "nikic/php-parser": "^4.19.1 || ^5.1.0", - "php": ">=7.3", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-text-template": "^2.0.4", - "sebastian/code-unit-reverse-lookup": "^2.0.3", - "sebastian/complexity": "^2.0.3", - "sebastian/environment": "^5.1.5", - "sebastian/lines-of-code": "^1.0.4", - "sebastian/version": "^3.0.2", + "nikic/php-parser": "^5.4.0", + "php": ">=8.2", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-text-template": "^4.0.1", + "sebastian/code-unit-reverse-lookup": "^4.0.1", + "sebastian/complexity": "^4.0.1", + "sebastian/environment": "^7.2.0", + "sebastian/lines-of-code": "^3.0.1", + "sebastian/version": "^5.0.2", "theseer/tokenizer": "^1.2.3" }, "require-dev": { - "phpunit/phpunit": "^9.6" + "phpunit/phpunit": "^11.5.2" }, "suggest": { "ext-pcov": "PHP extension that provides line coverage", @@ -1392,7 +2129,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-main": "9.2.x-dev" + "dev-main": "11.0.x-dev" } }, "autoload": { @@ -1421,40 +2158,52 @@ "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/9.2.32" + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/show" }, "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": "2024-08-22T04:23:01+00:00" + "time": "2025-06-18T08:56:18+00:00" }, { "name": "phpunit/php-file-iterator", - "version": "3.0.6", + "version": "5.1.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-file-iterator.git", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf" + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", - "reference": "cf1c2e7c203ac650e352f4cc675a7021e7d1b3cf", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/118cfaaa8bc5aef3287bf315b6060b1174754af6", + "reference": "118cfaaa8bc5aef3287bf315b6060b1174754af6", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.0-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1481,7 +2230,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", - "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/3.0.6" + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/5.1.0" }, "funding": [ { @@ -1489,28 +2239,28 @@ "type": "github" } ], - "time": "2021-12-02T12:48:52+00:00" + "time": "2024-08-27T05:02:59+00:00" }, { "name": "phpunit/php-invoker", - "version": "3.1.1", + "version": "5.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-invoker.git", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67" + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/5a10147d0aaf65b58940a0b72f71c9ac0423cc67", - "reference": "5a10147d0aaf65b58940a0b72f71c9ac0423cc67", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/c1ca3814734c07492b3d4c5f794f4b0995333da2", + "reference": "c1ca3814734c07492b3d4c5f794f4b0995333da2", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { "ext-pcntl": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "suggest": { "ext-pcntl": "*" @@ -1518,7 +2268,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "3.1-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -1544,7 +2294,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-invoker/issues", - "source": "https://github.com/sebastianbergmann/php-invoker/tree/3.1.1" + "security": "https://github.com/sebastianbergmann/php-invoker/security/policy", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/5.0.1" }, "funding": [ { @@ -1552,32 +2303,32 @@ "type": "github" } ], - "time": "2020-09-28T05:58:55+00:00" + "time": "2024-07-03T05:07:44+00:00" }, { "name": "phpunit/php-text-template", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-text-template.git", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28" + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", - "reference": "5da5f67fc95621df9ff4c4e5a84d6a8a2acf7c28", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/3e0404dc6b300e6bf56415467ebcb3fe4f33e964", + "reference": "3e0404dc6b300e6bf56415467ebcb3fe4f33e964", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -1603,7 +2354,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-text-template/issues", - "source": "https://github.com/sebastianbergmann/php-text-template/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/4.0.1" }, "funding": [ { @@ -1611,32 +2363,32 @@ "type": "github" } ], - "time": "2020-10-26T05:33:50+00:00" + "time": "2024-07-03T05:08:43+00:00" }, { "name": "phpunit/php-timer", - "version": "5.0.3", + "version": "7.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/php-timer.git", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2" + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", - "reference": "5a63ce20ed1b5bf577850e2c4e87f4aa902afbd2", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", + "reference": "3b415def83fbcb41f991d9ebf16ae4ad8b7837b3", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -1662,7 +2414,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/php-timer/issues", - "source": "https://github.com/sebastianbergmann/php-timer/tree/5.0.3" + "security": "https://github.com/sebastianbergmann/php-timer/security/policy", + "source": "https://github.com/sebastianbergmann/php-timer/tree/7.0.1" }, "funding": [ { @@ -1670,54 +2423,52 @@ "type": "github" } ], - "time": "2020-10-26T13:16:10+00:00" + "time": "2024-07-03T05:09:35+00:00" }, { "name": "phpunit/phpunit", - "version": "9.6.22", + "version": "11.5.15", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c" + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/f80235cb4d3caa59ae09be3adf1ded27521d1a9c", - "reference": "f80235cb4d3caa59ae09be3adf1ded27521d1a9c", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", + "reference": "4b6a4ee654e5e0c5e1f17e2f83c0f4c91dee1f9c", "shasum": "" }, "require": { - "doctrine/instantiator": "^1.5.0 || ^2", "ext-dom": "*", "ext-json": "*", "ext-libxml": "*", "ext-mbstring": "*", "ext-xml": "*", "ext-xmlwriter": "*", - "myclabs/deep-copy": "^1.12.1", + "myclabs/deep-copy": "^1.13.0", "phar-io/manifest": "^2.0.4", "phar-io/version": "^3.2.1", - "php": ">=7.3", - "phpunit/php-code-coverage": "^9.2.32", - "phpunit/php-file-iterator": "^3.0.6", - "phpunit/php-invoker": "^3.1.1", - "phpunit/php-text-template": "^2.0.4", - "phpunit/php-timer": "^5.0.3", - "sebastian/cli-parser": "^1.0.2", - "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.8", - "sebastian/diff": "^4.0.6", - "sebastian/environment": "^5.1.5", - "sebastian/exporter": "^4.0.6", - "sebastian/global-state": "^5.0.7", - "sebastian/object-enumerator": "^4.0.4", - "sebastian/resource-operations": "^3.0.4", - "sebastian/type": "^3.2.1", - "sebastian/version": "^3.0.2" + "php": ">=8.2", + "phpunit/php-code-coverage": "^11.0.9", + "phpunit/php-file-iterator": "^5.1.0", + "phpunit/php-invoker": "^5.0.1", + "phpunit/php-text-template": "^4.0.1", + "phpunit/php-timer": "^7.0.1", + "sebastian/cli-parser": "^3.0.2", + "sebastian/code-unit": "^3.0.3", + "sebastian/comparator": "^6.3.1", + "sebastian/diff": "^6.0.2", + "sebastian/environment": "^7.2.0", + "sebastian/exporter": "^6.3.0", + "sebastian/global-state": "^7.0.2", + "sebastian/object-enumerator": "^6.0.1", + "sebastian/type": "^5.1.2", + "sebastian/version": "^5.0.2", + "staabm/side-effects-detector": "^1.0.5" }, "suggest": { - "ext-soap": "To be able to generate mocks based on WSDL files", - "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + "ext-soap": "To be able to generate mocks based on WSDL files" }, "bin": [ "phpunit" @@ -1725,7 +2476,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "9.6-dev" + "dev-main": "11.5-dev" } }, "autoload": { @@ -1757,7 +2508,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.22" + "source": "https://github.com/sebastianbergmann/phpunit/tree/11.5.15" }, "funding": [ { @@ -1773,7 +2524,7 @@ "type": "tidelift" } ], - "time": "2024-12-05T13:48:26+00:00" + "time": "2025-03-23T16:02:11+00:00" }, { "name": "psr/container", @@ -1878,30 +2629,81 @@ }, "time": "2024-09-11T13:17:53+00:00" }, + { + "name": "psr/simple-cache", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/php-fig/simple-cache.git", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/php-fig/simple-cache/zipball/764e0b3939f5ca87cb904f570ef9be2d78a07865", + "reference": "764e0b3939f5ca87cb904f570ef9be2d78a07865", + "shasum": "" + }, + "require": { + "php": ">=8.0.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "3.0.x-dev" + } + }, + "autoload": { + "psr-4": { + "Psr\\SimpleCache\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "PHP-FIG", + "homepage": "https://www.php-fig.org/" + } + ], + "description": "Common interfaces for simple caching", + "keywords": [ + "cache", + "caching", + "psr", + "psr-16", + "simple-cache" + ], + "support": { + "source": "https://github.com/php-fig/simple-cache/tree/3.0.0" + }, + "time": "2021-10-29T13:26:27+00:00" + }, { "name": "sebastian/cli-parser", - "version": "1.0.2", + "version": "3.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/cli-parser.git", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b" + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/2b56bea83a09de3ac06bb18b92f068e60cc6f50b", - "reference": "2b56bea83a09de3ac06bb18b92f068e60cc6f50b", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/15c5dd40dc4f38794d383bb95465193f5e0ae180", + "reference": "15c5dd40dc4f38794d383bb95465193f5e0ae180", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1924,7 +2726,8 @@ "homepage": "https://github.com/sebastianbergmann/cli-parser", "support": { "issues": "https://github.com/sebastianbergmann/cli-parser/issues", - "source": "https://github.com/sebastianbergmann/cli-parser/tree/1.0.2" + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/3.0.2" }, "funding": [ { @@ -1932,32 +2735,32 @@ "type": "github" } ], - "time": "2024-03-02T06:27:43+00:00" + "time": "2024-07-03T04:41:36+00:00" }, { "name": "sebastian/code-unit", - "version": "1.0.8", + "version": "3.0.3", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit.git", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120" + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/1fc9f64c0927627ef78ba436c9b17d967e68e120", - "reference": "1fc9f64c0927627ef78ba436c9b17d967e68e120", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/54391c61e4af8078e5b276ab082b6d3c54c9ad64", + "reference": "54391c61e4af8078e5b276ab082b6d3c54c9ad64", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -1980,7 +2783,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit", "support": { "issues": "https://github.com/sebastianbergmann/code-unit/issues", - "source": "https://github.com/sebastianbergmann/code-unit/tree/1.0.8" + "security": "https://github.com/sebastianbergmann/code-unit/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit/tree/3.0.3" }, "funding": [ { @@ -1988,32 +2792,32 @@ "type": "github" } ], - "time": "2020-10-26T13:08:54+00:00" + "time": "2025-03-19T07:56:08+00:00" }, { "name": "sebastian/code-unit-reverse-lookup", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5" + "reference": "183a9b2632194febd219bb9246eee421dad8d45e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", - "reference": "ac91f01ccec49fb77bdc6fd1e548bc70f7faa3e5", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/183a9b2632194febd219bb9246eee421dad8d45e", + "reference": "183a9b2632194febd219bb9246eee421dad8d45e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2035,7 +2839,8 @@ "homepage": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/", "support": { "issues": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/issues", - "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/security/policy", + "source": "https://github.com/sebastianbergmann/code-unit-reverse-lookup/tree/4.0.1" }, "funding": [ { @@ -2043,34 +2848,39 @@ "type": "github" } ], - "time": "2020-09-28T05:30:19+00:00" + "time": "2024-07-03T04:45:54+00:00" }, { "name": "sebastian/comparator", - "version": "4.0.8", + "version": "6.3.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a" + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/fa0f136dd2334583309d32b62544682ee972b51a", - "reference": "fa0f136dd2334583309d32b62544682ee972b51a", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/24b8fbc2c8e201bb1308e7b05148d6ab393b6959", + "reference": "24b8fbc2c8e201bb1308e7b05148d6ab393b6959", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/diff": "^4.0", - "sebastian/exporter": "^4.0" + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/diff": "^6.0", + "sebastian/exporter": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.4" + }, + "suggest": { + "ext-bcmath": "For comparing BcMath\\Number objects" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.3-dev" } }, "autoload": { @@ -2109,7 +2919,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.8" + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/6.3.1" }, "funding": [ { @@ -2117,33 +2928,33 @@ "type": "github" } ], - "time": "2022-09-14T12:41:17+00:00" + "time": "2025-03-07T06:57:01+00:00" }, { "name": "sebastian/complexity", - "version": "2.0.3", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/complexity.git", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a" + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/25f207c40d62b8b7aa32f5ab026c53561964053a", - "reference": "25f207c40d62b8b7aa32f5ab026c53561964053a", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/ee41d384ab1906c68852636b6de493846e13e5a0", + "reference": "ee41d384ab1906c68852636b6de493846e13e5a0", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2166,7 +2977,8 @@ "homepage": "https://github.com/sebastianbergmann/complexity", "support": { "issues": "https://github.com/sebastianbergmann/complexity/issues", - "source": "https://github.com/sebastianbergmann/complexity/tree/2.0.3" + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/4.0.1" }, "funding": [ { @@ -2174,33 +2986,33 @@ "type": "github" } ], - "time": "2023-12-22T06:19:30+00:00" + "time": "2024-07-03T04:49:50+00:00" }, { "name": "sebastian/diff", - "version": "4.0.6", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/diff.git", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc" + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/ba01945089c3a293b01ba9badc29ad55b106b0bc", - "reference": "ba01945089c3a293b01ba9badc29ad55b106b0bc", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/b4ccd857127db5d41a5b676f24b51371d76d8544", + "reference": "b4ccd857127db5d41a5b676f24b51371d76d8544", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3", + "phpunit/phpunit": "^11.0", "symfony/process": "^4.2 || ^5" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2232,7 +3044,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/diff/issues", - "source": "https://github.com/sebastianbergmann/diff/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/6.0.2" }, "funding": [ { @@ -2240,27 +3053,27 @@ "type": "github" } ], - "time": "2024-03-02T06:30:58+00:00" + "time": "2024-07-03T04:53:05+00:00" }, { "name": "sebastian/environment", - "version": "5.1.5", + "version": "7.2.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/environment.git", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed" + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", - "reference": "830c43a844f1f8d5b7a1f6d6076b784454d8b7ed", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/a5c75038693ad2e8d4b6c15ba2403532647830c4", + "reference": "a5c75038693ad2e8d4b6c15ba2403532647830c4", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "suggest": { "ext-posix": "*" @@ -2268,7 +3081,7 @@ "type": "library", "extra": { "branch-alias": { - "dev-master": "5.1-dev" + "dev-main": "7.2-dev" } }, "autoload": { @@ -2287,7 +3100,7 @@ } ], "description": "Provides functionality to handle HHVM/PHP environments", - "homepage": "http://www.github.com/sebastianbergmann/environment", + "homepage": "https://github.com/sebastianbergmann/environment", "keywords": [ "Xdebug", "environment", @@ -2295,42 +3108,55 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/environment/issues", - "source": "https://github.com/sebastianbergmann/environment/tree/5.1.5" + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/7.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/environment", + "type": "tidelift" } ], - "time": "2023-02-03T06:03:51+00:00" + "time": "2025-05-21T11:55:47+00:00" }, { "name": "sebastian/exporter", - "version": "4.0.6", + "version": "6.3.0", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/exporter.git", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72" + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/78c00df8f170e02473b682df15bfcdacc3d32d72", - "reference": "78c00df8f170e02473b682df15bfcdacc3d32d72", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/3473f61172093b2da7de1fb5782e1f24cc036dc3", + "reference": "3473f61172093b2da7de1fb5782e1f24cc036dc3", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/recursion-context": "^4.0" + "ext-mbstring": "*", + "php": ">=8.2", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "ext-mbstring": "*", - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.1-dev" } }, "autoload": { @@ -2372,7 +3198,8 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/exporter/issues", - "source": "https://github.com/sebastianbergmann/exporter/tree/4.0.6" + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/6.3.0" }, "funding": [ { @@ -2380,38 +3207,35 @@ "type": "github" } ], - "time": "2024-03-02T06:33:00+00:00" + "time": "2024-12-05T09:17:50+00:00" }, { "name": "sebastian/global-state", - "version": "5.0.7", + "version": "7.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/global-state.git", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9" + "reference": "3be331570a721f9a4b5917f4209773de17f747d7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", - "reference": "bca7df1f32ee6fe93b4d4a9abbf69e13a4ada2c9", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/3be331570a721f9a4b5917f4209773de17f747d7", + "reference": "3be331570a721f9a4b5917f4209773de17f747d7", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { "ext-dom": "*", - "phpunit/phpunit": "^9.3" - }, - "suggest": { - "ext-uopz": "*" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "5.0-dev" + "dev-main": "7.0-dev" } }, "autoload": { @@ -2430,13 +3254,14 @@ } ], "description": "Snapshotting of global state", - "homepage": "http://www.github.com/sebastianbergmann/global-state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", "keywords": [ "global state" ], "support": { "issues": "https://github.com/sebastianbergmann/global-state/issues", - "source": "https://github.com/sebastianbergmann/global-state/tree/5.0.7" + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/7.0.2" }, "funding": [ { @@ -2444,33 +3269,33 @@ "type": "github" } ], - "time": "2024-03-02T06:35:11+00:00" + "time": "2024-07-03T04:57:36+00:00" }, { "name": "sebastian/lines-of-code", - "version": "1.0.4", + "version": "3.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/lines-of-code.git", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5" + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/e1e4a170560925c26d424b6a03aed157e7dcc5c5", - "reference": "e1e4a170560925c26d424b6a03aed157e7dcc5c5", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/d36ad0d782e5756913e42ad87cb2890f4ffe467a", + "reference": "d36ad0d782e5756913e42ad87cb2890f4ffe467a", "shasum": "" }, "require": { - "nikic/php-parser": "^4.18 || ^5.0", - "php": ">=7.3" + "nikic/php-parser": "^5.0", + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "1.0-dev" + "dev-main": "3.0-dev" } }, "autoload": { @@ -2493,7 +3318,8 @@ "homepage": "https://github.com/sebastianbergmann/lines-of-code", "support": { "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", - "source": "https://github.com/sebastianbergmann/lines-of-code/tree/1.0.4" + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/3.0.1" }, "funding": [ { @@ -2501,34 +3327,34 @@ "type": "github" } ], - "time": "2023-12-22T06:20:34+00:00" + "time": "2024-07-03T04:58:38+00:00" }, { "name": "sebastian/object-enumerator", - "version": "4.0.4", + "version": "6.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-enumerator.git", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71" + "reference": "f5b498e631a74204185071eb41f33f38d64608aa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/5c9eeac41b290a3712d88851518825ad78f45c71", - "reference": "5c9eeac41b290a3712d88851518825ad78f45c71", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/f5b498e631a74204185071eb41f33f38d64608aa", + "reference": "f5b498e631a74204185071eb41f33f38d64608aa", "shasum": "" }, "require": { - "php": ">=7.3", - "sebastian/object-reflector": "^2.0", - "sebastian/recursion-context": "^4.0" + "php": ">=8.2", + "sebastian/object-reflector": "^4.0", + "sebastian/recursion-context": "^6.0" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2550,7 +3376,8 @@ "homepage": "https://github.com/sebastianbergmann/object-enumerator/", "support": { "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", - "source": "https://github.com/sebastianbergmann/object-enumerator/tree/4.0.4" + "security": "https://github.com/sebastianbergmann/object-enumerator/security/policy", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/6.0.1" }, "funding": [ { @@ -2558,32 +3385,32 @@ "type": "github" } ], - "time": "2020-10-26T13:12:34+00:00" + "time": "2024-07-03T05:00:13+00:00" }, { "name": "sebastian/object-reflector", - "version": "2.0.4", + "version": "4.0.1", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/object-reflector.git", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7" + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", - "reference": "b4f479ebdbf63ac605d183ece17d8d7fe49c15c7", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/6e1a43b411b2ad34146dee7524cb13a068bb35f9", + "reference": "6e1a43b411b2ad34146dee7524cb13a068bb35f9", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "2.0-dev" + "dev-main": "4.0-dev" } }, "autoload": { @@ -2605,7 +3432,8 @@ "homepage": "https://github.com/sebastianbergmann/object-reflector/", "support": { "issues": "https://github.com/sebastianbergmann/object-reflector/issues", - "source": "https://github.com/sebastianbergmann/object-reflector/tree/2.0.4" + "security": "https://github.com/sebastianbergmann/object-reflector/security/policy", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/4.0.1" }, "funding": [ { @@ -2613,32 +3441,32 @@ "type": "github" } ], - "time": "2020-10-26T13:14:26+00:00" + "time": "2024-07-03T05:01:32+00:00" }, { "name": "sebastian/recursion-context", - "version": "4.0.5", + "version": "6.0.2", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/recursion-context.git", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1" + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", - "reference": "e75bd0f07204fec2a0af9b0f3cfe97d05f92efc1", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/694d156164372abbd149a4b85ccda2e4670c0e16", + "reference": "694d156164372abbd149a4b85ccda2e4670c0e16", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.3" + "phpunit/phpunit": "^11.0" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-main": "6.0-dev" } }, "autoload": { @@ -2668,7 +3496,8 @@ "homepage": "https://github.com/sebastianbergmann/recursion-context", "support": { "issues": "https://github.com/sebastianbergmann/recursion-context/issues", - "source": "https://github.com/sebastianbergmann/recursion-context/tree/4.0.5" + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/6.0.2" }, "funding": [ { @@ -2676,32 +3505,32 @@ "type": "github" } ], - "time": "2023-02-03T06:07:39+00:00" + "time": "2024-07-03T05:10:34+00:00" }, { - "name": "sebastian/resource-operations", - "version": "3.0.4", + "name": "sebastian/type", + "version": "5.1.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/resource-operations.git", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e" + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/resource-operations/zipball/05d5692a7993ecccd56a03e40cd7e5b09b1d404e", - "reference": "05d5692a7993ecccd56a03e40cd7e5b09b1d404e", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", + "reference": "a8a7e30534b0eb0c77cd9d07e82de1a114389f5e", "shasum": "" }, "require": { - "php": ">=7.3" + "php": ">=8.2" }, "require-dev": { - "phpunit/phpunit": "^9.0" + "phpunit/phpunit": "^11.3" }, "type": "library", "extra": { "branch-alias": { - "dev-main": "3.0-dev" + "dev-main": "5.1-dev" } }, "autoload": { @@ -2716,13 +3545,16 @@ "authors": [ { "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de" + "email": "sebastian@phpunit.de", + "role": "lead" } ], - "description": "Provides a list of PHP built-in functions that operate on resources", - "homepage": "https://www.github.com/sebastianbergmann/resource-operations", + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", "support": { - "source": "https://github.com/sebastianbergmann/resource-operations/tree/3.0.4" + "issues": "https://github.com/sebastianbergmann/type/issues", + "security": "https://github.com/sebastianbergmann/type/security/policy", + "source": "https://github.com/sebastianbergmann/type/tree/5.1.2" }, "funding": [ { @@ -2730,32 +3562,29 @@ "type": "github" } ], - "time": "2024-03-14T16:00:52+00:00" + "time": "2025-03-18T13:35:50+00:00" }, { - "name": "sebastian/type", - "version": "3.2.1", + "name": "sebastian/version", + "version": "5.0.2", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/type.git", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7" + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", - "reference": "75e2c2a32f5e0b3aef905b9ed0b179b953b3d7c7", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c687e3387b99f5b03b6caa64c74b63e2936ff874", + "reference": "c687e3387b99f5b03b6caa64c74b63e2936ff874", "shasum": "" }, "require": { - "php": ">=7.3" - }, - "require-dev": { - "phpunit/phpunit": "^9.5" + "php": ">=8.2" }, "type": "library", "extra": { "branch-alias": { - "dev-master": "3.2-dev" + "dev-main": "5.0-dev" } }, "autoload": { @@ -2774,11 +3603,12 @@ "role": "lead" } ], - "description": "Collection of value objects that represent the types of the PHP type system", - "homepage": "https://github.com/sebastianbergmann/type", + "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/type/issues", - "source": "https://github.com/sebastianbergmann/type/tree/3.2.1" + "issues": "https://github.com/sebastianbergmann/version/issues", + "security": "https://github.com/sebastianbergmann/version/security/policy", + "source": "https://github.com/sebastianbergmann/version/tree/5.0.2" }, "funding": [ { @@ -2786,104 +3616,103 @@ "type": "github" } ], - "time": "2023-02-03T06:13:03+00:00" + "time": "2024-10-09T05:16:32+00:00" }, { - "name": "sebastian/version", - "version": "3.0.2", + "name": "staabm/side-effects-detector", + "version": "1.0.5", "source": { "type": "git", - "url": "https://github.com/sebastianbergmann/version.git", - "reference": "c6c1022351a901512170118436c764e473f6de8c" + "url": "https://github.com/staabm/side-effects-detector.git", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c6c1022351a901512170118436c764e473f6de8c", - "reference": "c6c1022351a901512170118436c764e473f6de8c", + "url": "https://api.github.com/repos/staabm/side-effects-detector/zipball/d8334211a140ce329c13726d4a715adbddd0a163", + "reference": "d8334211a140ce329c13726d4a715adbddd0a163", "shasum": "" }, "require": { - "php": ">=7.3" + "ext-tokenizer": "*", + "php": "^7.4 || ^8.0" }, - "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.0-dev" - } + "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": [ - "src/" + "lib/" ] }, "notification-url": "https://packagist.org/downloads/", "license": [ - "BSD-3-Clause" + "MIT" ], - "authors": [ - { - "name": "Sebastian Bergmann", - "email": "sebastian@phpunit.de", - "role": "lead" - } + "description": "A static analysis tool to detect side effects in PHP code", + "keywords": [ + "static analysis" ], - "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", - "source": "https://github.com/sebastianbergmann/version/tree/3.0.2" + "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/sebastianbergmann", + "url": "https://github.com/staabm", "type": "github" } ], - "time": "2020-09-28T06:39:44+00:00" + "time": "2024-10-20T05:08:20+00:00" }, { "name": "symfony/console", - "version": "v6.4.15", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd" + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", - "reference": "f1fc6f47283e27336e7cebb9e8946c8de7bff9bd", + "url": "https://api.github.com/repos/symfony/console/zipball/66c1440edf6f339fd82ed6c7caa76cb006211b44", + "reference": "66c1440edf6f339fd82ed6c7caa76cb006211b44", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/deprecation-contracts": "^2.5|^3", "symfony/polyfill-mbstring": "~1.0", "symfony/service-contracts": "^2.5|^3", - "symfony/string": "^5.4|^6.0|^7.0" + "symfony/string": "^7.2" }, "conflict": { - "symfony/dependency-injection": "<5.4", - "symfony/dotenv": "<5.4", - "symfony/event-dispatcher": "<5.4", - "symfony/lock": "<5.4", - "symfony/process": "<5.4" + "symfony/dependency-injection": "<6.4", + "symfony/dotenv": "<6.4", + "symfony/event-dispatcher": "<6.4", + "symfony/lock": "<6.4", + "symfony/process": "<6.4" }, "provide": { "psr/log-implementation": "1.0|2.0|3.0" }, "require-dev": { "psr/log": "^1|^2|^3", - "symfony/config": "^5.4|^6.0|^7.0", - "symfony/dependency-injection": "^5.4|^6.0|^7.0", - "symfony/event-dispatcher": "^5.4|^6.0|^7.0", + "symfony/config": "^6.4|^7.0", + "symfony/dependency-injection": "^6.4|^7.0", + "symfony/event-dispatcher": "^6.4|^7.0", "symfony/http-foundation": "^6.4|^7.0", "symfony/http-kernel": "^6.4|^7.0", - "symfony/lock": "^5.4|^6.0|^7.0", - "symfony/messenger": "^5.4|^6.0|^7.0", - "symfony/process": "^5.4|^6.0|^7.0", - "symfony/stopwatch": "^5.4|^6.0|^7.0", - "symfony/var-dumper": "^5.4|^6.0|^7.0" + "symfony/lock": "^6.4|^7.0", + "symfony/messenger": "^6.4|^7.0", + "symfony/process": "^6.4|^7.0", + "symfony/stopwatch": "^6.4|^7.0", + "symfony/var-dumper": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -2917,7 +3746,71 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.15" + "source": "https://github.com/symfony/console/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-05-24T10:34:04+00:00" + }, + { + "name": "symfony/finder", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/finder.git", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/finder/zipball/ec2344cf77a48253bbca6939aa3d2477773ea63d", + "reference": "ec2344cf77a48253bbca6939aa3d2477773ea63d", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "require-dev": { + "symfony/filesystem": "^6.4|^7.0" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Finder\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Finds files and directories via an intuitive fluent interface", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/finder/tree/v7.3.0" }, "funding": [ { @@ -2933,11 +3826,11 @@ "type": "tidelift" } ], - "time": "2024-11-06T14:19:14+00:00" + "time": "2024-12-30T19:00:26+00:00" }, { "name": "symfony/polyfill-ctype", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-ctype.git", @@ -2996,7 +3889,7 @@ "portable" ], "support": { - "source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-ctype/tree/v1.32.0" }, "funding": [ { @@ -3016,7 +3909,7 @@ }, { "name": "symfony/polyfill-intl-grapheme", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-grapheme.git", @@ -3074,7 +3967,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.32.0" }, "funding": [ { @@ -3094,7 +3987,7 @@ }, { "name": "symfony/polyfill-intl-normalizer", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-intl-normalizer.git", @@ -3155,7 +4048,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.32.0" }, "funding": [ { @@ -3175,19 +4068,20 @@ }, { "name": "symfony/polyfill-mbstring", - "version": "v1.31.0", + "version": "v1.32.0", "source": { "type": "git", "url": "https://github.com/symfony/polyfill-mbstring.git", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341" + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341", - "reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341", + "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/6d857f4d76bd4b343eac26d6b539585d2bc56493", + "reference": "6d857f4d76bd4b343eac26d6b539585d2bc56493", "shasum": "" }, "require": { + "ext-iconv": "*", "php": ">=7.2" }, "provide": { @@ -3235,7 +4129,7 @@ "shim" ], "support": { - "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0" + "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.32.0" }, "funding": [ { @@ -3251,20 +4145,81 @@ "type": "tidelift" } ], - "time": "2024-09-09T11:45:10+00:00" + "time": "2024-12-23T08:48:59+00:00" + }, + { + "name": "symfony/process", + "version": "v7.3.0", + "source": { + "type": "git", + "url": "https://github.com/symfony/process.git", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/symfony/process/zipball/40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "reference": "40c295f2deb408d5e9d2d32b8ba1dd61e36f05af", + "shasum": "" + }, + "require": { + "php": ">=8.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Symfony\\Component\\Process\\": "" + }, + "exclude-from-classmap": [ + "/Tests/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Fabien Potencier", + "email": "fabien@symfony.com" + }, + { + "name": "Symfony Community", + "homepage": "https://symfony.com/contributors" + } + ], + "description": "Executes commands in sub-processes", + "homepage": "https://symfony.com", + "support": { + "source": "https://github.com/symfony/process/tree/v7.3.0" + }, + "funding": [ + { + "url": "https://symfony.com/sponsor", + "type": "custom" + }, + { + "url": "https://github.com/fabpot", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/symfony/symfony", + "type": "tidelift" + } + ], + "time": "2025-04-17T09:11:12+00:00" }, { "name": "symfony/service-contracts", - "version": "v3.5.1", + "version": "v3.6.0", "source": { "type": "git", "url": "https://github.com/symfony/service-contracts.git", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0" + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/service-contracts/zipball/e53260aabf78fb3d63f8d79d69ece59f80d5eda0", - "reference": "e53260aabf78fb3d63f8d79d69ece59f80d5eda0", + "url": "https://api.github.com/repos/symfony/service-contracts/zipball/f021b05a130d35510bd6b25fe9053c2a8a15d5d4", + "reference": "f021b05a130d35510bd6b25fe9053c2a8a15d5d4", "shasum": "" }, "require": { @@ -3277,12 +4232,12 @@ }, "type": "library", "extra": { - "branch-alias": { - "dev-main": "3.5-dev" - }, "thanks": { - "name": "symfony/contracts", - "url": "https://github.com/symfony/contracts" + "url": "https://github.com/symfony/contracts", + "name": "symfony/contracts" + }, + "branch-alias": { + "dev-main": "3.6-dev" } }, "autoload": { @@ -3318,7 +4273,7 @@ "standards" ], "support": { - "source": "https://github.com/symfony/service-contracts/tree/v3.5.1" + "source": "https://github.com/symfony/service-contracts/tree/v3.6.0" }, "funding": [ { @@ -3334,24 +4289,24 @@ "type": "tidelift" } ], - "time": "2024-09-25T14:20:29+00:00" + "time": "2025-04-25T09:37:31+00:00" }, { "name": "symfony/string", - "version": "v6.4.15", + "version": "v7.3.0", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f" + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", - "reference": "73a5e66ea2e1677c98d4449177c5a9cf9d8b4c6f", + "url": "https://api.github.com/repos/symfony/string/zipball/f3570b8c61ca887a9e2938e85cb6458515d2b125", + "reference": "f3570b8c61ca887a9e2938e85cb6458515d2b125", "shasum": "" }, "require": { - "php": ">=8.1", + "php": ">=8.2", "symfony/polyfill-ctype": "~1.8", "symfony/polyfill-intl-grapheme": "~1.0", "symfony/polyfill-intl-normalizer": "~1.0", @@ -3361,11 +4316,12 @@ "symfony/translation-contracts": "<2.5" }, "require-dev": { - "symfony/error-handler": "^5.4|^6.0|^7.0", - "symfony/http-client": "^5.4|^6.0|^7.0", - "symfony/intl": "^6.2|^7.0", + "symfony/emoji": "^7.1", + "symfony/error-handler": "^6.4|^7.0", + "symfony/http-client": "^6.4|^7.0", + "symfony/intl": "^6.4|^7.0", "symfony/translation-contracts": "^2.5|^3.0", - "symfony/var-exporter": "^5.4|^6.0|^7.0" + "symfony/var-exporter": "^6.4|^7.0" }, "type": "library", "autoload": { @@ -3404,7 +4360,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.15" + "source": "https://github.com/symfony/string/tree/v7.3.0" }, "funding": [ { @@ -3420,7 +4376,66 @@ "type": "tidelift" } ], - "time": "2024-11-13T13:31:12+00:00" + "time": "2025-04-20T20:19:01+00:00" + }, + { + "name": "ta-tikoma/phpunit-architecture-test", + "version": "0.8.5", + "source": { + "type": "git", + "url": "https://github.com/ta-tikoma/phpunit-architecture-test.git", + "reference": "cf6fb197b676ba716837c886baca842e4db29005" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/ta-tikoma/phpunit-architecture-test/zipball/cf6fb197b676ba716837c886baca842e4db29005", + "reference": "cf6fb197b676ba716837c886baca842e4db29005", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18.0 || ^5.0.0", + "php": "^8.1.0", + "phpdocumentor/reflection-docblock": "^5.3.0", + "phpunit/phpunit": "^10.5.5 || ^11.0.0 || ^12.0.0", + "symfony/finder": "^6.4.0 || ^7.0.0" + }, + "require-dev": { + "laravel/pint": "^1.13.7", + "phpstan/phpstan": "^1.10.52" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPUnit\\Architecture\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Ni Shi", + "email": "futik0ma011@gmail.com" + }, + { + "name": "Nuno Maduro", + "email": "enunomaduro@gmail.com" + } + ], + "description": "Methods for testing application architecture", + "keywords": [ + "architecture", + "phpunit", + "stucture", + "test", + "testing" + ], + "support": { + "issues": "https://github.com/ta-tikoma/phpunit-architecture-test/issues", + "source": "https://github.com/ta-tikoma/phpunit-architecture-test/tree/0.8.5" + }, + "time": "2025-04-20T20:23:40+00:00" }, { "name": "theseer/tokenizer", @@ -3471,16 +4486,74 @@ } ], "time": "2024-03-03T12:36:25+00:00" + }, + { + "name": "webmozart/assert", + "version": "1.11.0", + "source": { + "type": "git", + "url": "https://github.com/webmozarts/assert.git", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/webmozarts/assert/zipball/11cb2199493b2f8a3b53e7f19068fc6aac760991", + "reference": "11cb2199493b2f8a3b53e7f19068fc6aac760991", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "php": "^7.2 || ^8.0" + }, + "conflict": { + "phpstan/phpstan": "<0.12.20", + "vimeo/psalm": "<4.6.1 || 4.6.2" + }, + "require-dev": { + "phpunit/phpunit": "^8.5.13" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "1.10-dev" + } + }, + "autoload": { + "psr-4": { + "Webmozart\\Assert\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Assertions to validate method input/output with nice error messages.", + "keywords": [ + "assert", + "check", + "validate" + ], + "support": { + "issues": "https://github.com/webmozarts/assert/issues", + "source": "https://github.com/webmozarts/assert/tree/1.11.0" + }, + "time": "2022-06-03T18:03:27+00:00" } ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": {}, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "^8.0" }, - "platform-dev": [], + "platform-dev": {}, "plugin-api-version": "2.6.0" } diff --git a/phpunit.xml b/phpunit.xml index 8f4b58c..eead5c3 100644 --- a/phpunit.xml +++ b/phpunit.xml @@ -1,18 +1,14 @@ - - - - ./tests - - - - - ./app - ./src - - + + + + ./tests + + + + + ./app + ./src + + diff --git a/pint.json b/pint.json new file mode 100644 index 0000000..2b469f7 --- /dev/null +++ b/pint.json @@ -0,0 +1,62 @@ +{ + "preset": "laravel", + "notPath": [ + "tests/TestCase.php" + ], + "rules": { + "array_push": true, + "backtick_to_shell_exec": true, + "date_time_immutable": true, + "declare_strict_types": true, + "lowercase_keywords": true, + "lowercase_static_reference": true, + "final_class": true, + "final_internal_class": true, + "final_public_method_for_abstract_class": true, + "fully_qualified_strict_types": true, + "global_namespace_import": { + "import_classes": true, + "import_constants": true, + "import_functions": true + }, + "mb_str_functions": true, + "modernize_types_casting": true, + "new_with_parentheses": false, + "no_superfluous_elseif": true, + "no_useless_else": true, + "no_multiple_statements_per_line": true, + "not_operator_with_successor_space": false, + "ordered_class_elements": { + "order": [ + "use_trait", + "case", + "constant", + "constant_public", + "constant_protected", + "constant_private", + "property_public", + "property_protected", + "property_private", + "construct", + "destruct", + "magic", + "phpunit", + "method_abstract", + "method_public_static", + "method_public", + "method_protected_static", + "method_protected", + "method_private_static", + "method_private" + ], + "sort_algorithm": "none" + }, + "ordered_interfaces": true, + "ordered_traits": true, + "protected_to_private": true, + "self_accessor": true, + "self_static_accessor": true, + "strict_comparison": true, + "visibility_required": true + } +} diff --git a/src/Converter/MarkdownImageProcessor.php b/src/Converter/MarkdownImageProcessor.php new file mode 100644 index 0000000..be405f2 --- /dev/null +++ b/src/Converter/MarkdownImageProcessor.php @@ -0,0 +1,145 @@ +isImageLink($node)) { + $altText = $this->getImageAltText($node); + $images[] = new ImageLikeLink($node, $altText); + } + + if ($node->hasChildren()) { + foreach ($node->children() as $child) { + $images = array_merge($images, $this->extractImages($child)); + } + } + + return $images; + } + + /** + * Check if a paragraph node contains only images (and whitespace). + */ + public function containsOnlyImages(Node $node): bool + { + if (!$node->hasChildren()) { + return false; + } + + $hasImages = false; + $textContent = ''; + + foreach ($node->children() as $child) { + if ($child instanceof Image) { + $hasImages = true; + + continue; + } + + if ($child instanceof Link && $this->isImageLink($child)) { + $hasImages = true; + + continue; + } + + if ($child instanceof Text) { + $text = $child->getLiteral(); + // Remove the "!" that precedes image links + $cleanText = preg_replace('/!\s*$/', '', $text); + $textContent .= $cleanText; + + continue; + } + + // Check if this child has meaningful text content + $text = $this->getTextContent($child); + $textContent .= $text; + } + + return $hasImages && mb_trim($textContent) === ''; + } + + /** + * Check if a Link node is actually an image (preceded by ! in markdown). + */ + private function isImageLink(Link $link): bool + { + // Check if the parent paragraph contains a Text node with "!" before this link + $parent = $link->parent(); + if (!$parent) { + return false; + } + + $children = iterator_to_array($parent->children()); + $linkIndex = array_search($link, $children, true); + + if ($linkIndex <= 0) { + return false; + } + + $previousNode = $children[$linkIndex - 1]; + if (!$previousNode instanceof Text) { + return false; + } + + $text = $previousNode->getLiteral(); + + return str_ends_with($text, '!'); + } + + /** + * Get the alt text for an image link. + */ + private function getImageAltText(Link $link): string + { + $altText = ''; + foreach ($link->children() as $child) { + if ($child instanceof Text) { + $altText .= $child->getLiteral(); + } + } + + return mb_trim($altText); + } + + /** + * Get text content from a node, recursively. + */ + private function getTextContent(Node $node): string + { + $text = ''; + + if (method_exists($node, 'getLiteral')) { + $text .= $node->getLiteral(); + } + + if ($node->hasChildren()) { + foreach ($node->children() as $child) { + $text .= $this->getTextContent($child); + } + } + + return $text; + } +} diff --git a/src/Converter/MarkdownToNotionBlocksConverter.php b/src/Converter/MarkdownToNotionBlocksConverter.php index 461233b..5152108 100644 --- a/src/Converter/MarkdownToNotionBlocksConverter.php +++ b/src/Converter/MarkdownToNotionBlocksConverter.php @@ -1,5 +1,7 @@ parser = new MarkdownParser($environment); - $this->renderer = new NotionBlocksRenderer(); + $this->renderer = new NotionBlocksRenderer; } /** @@ -32,12 +37,14 @@ public function __construct(EnvironmentInterface $environment) { * * @since 1.0.0 * - * @param string $input Markdown content. + * @param string $input Markdown content. * @return RenderedContentInterface The Notion blocks from parsed markdown. + * * @throws CommonMarkException * @throws ReflectionException */ - public function convert(string $input): RenderedContentInterface { + public function convert(string $input): RenderedContentInterface + { return $this->renderer->renderDocument($this->parser->parse($input)); } } diff --git a/src/Converter/NotionBlocksRenderer.php b/src/Converter/NotionBlocksRenderer.php index 0feba86..635fb95 100644 --- a/src/Converter/NotionBlocksRenderer.php +++ b/src/Converter/NotionBlocksRenderer.php @@ -1,28 +1,46 @@ children() as $node) { + // Check for images within this node first (including links that are actually images) + $images = $this->imageProcessor->extractImages($node); + foreach ($images as $image) { + $imageBlock = new Image($image); + $json[] = $imageBlock->object(); + } + $shortNameClass = (new ReflectionClass($node))->getShortName(); // Run the block renderers dynamically. - $class = 'RoelMR\\MarkdownToNotionBlocks\\NotionBlocks\\' . $shortNameClass; + $class = 'RoelMR\\MarkdownToNotionBlocks\\NotionBlocks\\'.$shortNameClass; if (!class_exists($class)) { continue; @@ -31,6 +49,11 @@ public function renderDocument(Document $document): NotionRenderedContent { /* @var $class NotionBlock */ $object = (new $class($node))->object(); + // Skip paragraphs that only contain images (we've already processed them above) + if ($shortNameClass === 'Paragraph' && $this->imageProcessor->containsOnlyImages($node)) { + continue; + } + $type = $object['type'] ?? ''; /** @@ -39,22 +62,24 @@ public function renderDocument(Document $document): NotionRenderedContent { * The Notion API only accepts 100 rich text objects per block. * * @since 1.2.0 - * * @see https://developers.notion.com/reference/request-limits#limits-for-property-values */ - if (isset($object[$type]['rich_text']) && count($object[$type]['rich_text']) > 100) { - $richText = $object[$type]['rich_text']; + if (!isset($object[$type]['rich_text']) || count($object[$type]['rich_text']) <= 100) { + $json[] = $object; + + continue; + } - while (count($richText) > 100) { - $object[$type]['rich_text'] = array_slice($richText, 0, 100); - $json[] = $object; + $richText = $object[$type]['rich_text']; - $richText = array_slice($richText, 100); - } + while (count($richText) > 100) { + $object[$type]['rich_text'] = array_slice($richText, 0, 100); + $json[] = $object; - $object[$type]['rich_text'] = $richText; + $richText = array_slice($richText, 100); } + $object[$type]['rich_text'] = $richText; $json[] = $object; } @@ -97,15 +122,17 @@ public function renderDocument(Document $document): NotionRenderedContent { * * @since 1.0.0 * - * @param array $array Array to flatten. + * @param array $array Array to flatten. * @return array Flattened array. */ - protected function flattenSpecificArray(array $array): array { + private function flattenSpecificArray(array $array): array + { $result = []; foreach ($array as $element) { if (!is_array($element[0] ?? null)) { $result[] = $element; + continue; } diff --git a/src/Converter/NotionRenderedContent.php b/src/Converter/NotionRenderedContent.php index 2cbb04a..da01607 100644 --- a/src/Converter/NotionRenderedContent.php +++ b/src/Converter/NotionRenderedContent.php @@ -1,8 +1,9 @@ buildInvalidBlock($url); + } + + return [ + 'object' => 'block', + 'type' => 'image', + 'image' => [ + 'type' => $this->value, + $this->value => [ + 'url' => $url, + ], + 'caption' => $this->caption($title), + ], + ]; + } + + /** + * Return array structure for invalid image. + * + * @param string $url The invalid image URL + * @return array Paragraph block for invalid image + */ + private function buildInvalidBlock(string $url): array + { + $richTextObject = array_replace_recursive( + RichText::defaultObject(), + [ + 'text' => ['content' => '[Invalid image: '.$url.']'], + 'annotations' => ['italic' => true, 'color' => 'gray'], + ] + ); + + return [ + 'object' => 'block', + 'type' => 'paragraph', + 'paragraph' => [ + 'rich_text' => [$richTextObject], + ], + ]; + } + + /** + * Build caption array from title string. + * + * @param string|null $title The image title + * @return array The caption array for Notion + */ + private function caption(?string $title): array + { + if (empty($title)) { + return []; + } + + $richTextObject = array_replace_recursive( + RichText::defaultObject(), + ['text' => ['content' => $title]] + ); + + return [$richTextObject]; + } +} diff --git a/src/MarkdownToNotionBlocks.php b/src/MarkdownToNotionBlocks.php index 0b195e0..b5d2439 100644 --- a/src/MarkdownToNotionBlocks.php +++ b/src/MarkdownToNotionBlocks.php @@ -1,5 +1,7 @@ getContent(); } /** * Convert markdown to Notion blocks in array format. * - * @param string $markdown Markdown content. - * + * @param string $markdown Markdown content. * @return array The Notion blocks from parsed markdown in array format. + * * @throws CommonMarkException|ReflectionException */ - public static function array(string $markdown): array { + public static function array(string $markdown): array + { $notion_blocks = json_decode(self::convert($markdown)->getContent(), true); return json_last_error() !== JSON_ERROR_NONE - ? [ 'error' => json_last_error_msg() ] + ? ['error' => json_last_error_msg()] : $notion_blocks; } @@ -44,18 +49,18 @@ public static function array(string $markdown): array { * * @since 1.0.0 * - * @param string $markdown Markdown content. + * @param string $markdown Markdown content. * @return RenderedContentInterface The Notion blocks from parsed markdown. + * * @throws CommonMarkException * @throws ReflectionException */ - public static function convert(string $markdown): RenderedContentInterface { - $environment = new Environment(); - $environment->addExtension(new CommonMarkCoreExtension()); - $environment->addExtension(new GithubFlavoredMarkdownExtension()); - - $converter = new MarkdownToNotionBlocksConverter($environment); + public static function convert(string $markdown): RenderedContentInterface + { + $environment = new Environment; + $environment->addExtension(new CommonMarkCoreExtension); + $environment->addExtension(new GithubFlavoredMarkdownExtension); - return $converter->convert($markdown); + return (new MarkdownToNotionBlocksConverter($environment))->convert($markdown); } } diff --git a/src/NotionBlocks/BlockQuote.php b/src/NotionBlocks/BlockQuote.php index 7afaf95..5148e4e 100644 --- a/src/NotionBlocks/BlockQuote.php +++ b/src/NotionBlocks/BlockQuote.php @@ -1,5 +1,7 @@ realNode = !$node->hasChildren() ? false : $node->children()[0]; } /** - * @inheritDoc + * {@inheritDoc} */ - public function object(): array { + public function object(): array + { return $this->isCallout() ? (new Callout($this->node))->object() - : array( + : [ 'object' => 'block', 'type' => 'quote', - 'quote' => array( + 'quote' => [ 'rich_text' => $this->richText($this->realNode), 'color' => $this->color(), - ), - ); + ], + ]; } /** @@ -56,7 +61,8 @@ public function object(): array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { return 'default'; } @@ -70,7 +76,8 @@ protected function color(): string { * * @return bool True if the block quote is a callout, false otherwise. */ - protected function isCallout(): bool { + protected function isCallout(): bool + { if (!$this->realNode) { return false; } @@ -91,9 +98,9 @@ protected function isCallout(): bool { return false; } - $textContent = strtolower($firstChild->getLiteral()); - $callout = array_filter($types, fn ($type) => str_contains($textContent, strtolower($type))); + $textContent = mb_strtolower($firstChild->getLiteral()); + $callout = array_filter($types, fn ($type) => str_contains($textContent, mb_strtolower($type))); - return ! empty( $callout ); + return !empty($callout); } } diff --git a/src/NotionBlocks/Callout.php b/src/NotionBlocks/Callout.php index dd48450..0630c23 100644 --- a/src/NotionBlocks/Callout.php +++ b/src/NotionBlocks/Callout.php @@ -1,5 +1,7 @@ realNode = !$node->hasChildren() ? false : $node->children()[0]; if ($this->realNode) { @@ -58,24 +62,26 @@ public function __construct(public CommonMarkBlockQuote $node) { } /** - * @inheritDoc + * {@inheritDoc} */ - public function object(): array { - return array( + public function object(): array + { + return [ 'object' => 'block', 'type' => 'callout', - 'callout' => array( + 'callout' => [ 'rich_text' => $this->richText($this->realNode), 'icon' => $this->icon(), 'color' => $this->color(), - ), - ); + ], + ]; } /** - * @inheritDoc + * {@inheritDoc} */ - protected function richText(Node|bool $node): array { + protected function richText(Node|bool $node): array + { $richText = (new RichText($node))->toArray(); // Remove the entire object if the rich text first object is a line break. @@ -93,8 +99,9 @@ protected function richText(Node|bool $node): array { * * @return null|array The icon of the block. */ - protected function icon(): null|array { - return empty ($this->emoji) ? null : [ + protected function icon(): ?array + { + return empty($this->emoji) ? null : [ 'type' => 'emoji', 'emoji' => $this->emoji, ]; @@ -107,7 +114,8 @@ protected function icon(): null|array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { $backgroundColors = [ 'CAUTION' => 'yellow_background', 'DANGER' => 'red_background', @@ -128,18 +136,17 @@ protected function color(): string { * We're just setting the callout type and emoji. * * @since 1.0.0 - * - * @return void */ - protected function setProperties(): void { + protected function setProperties(): void + { $firstChild = $this->realNode->firstChild(); if (!$firstChild instanceof AbstractStringContainer) { return; } - $textContent = trim( $firstChild->getLiteral()); - $pattern = '/\[!(\w+)](?:\s(' . EmojiPattern::getEmojiPattern() . '))?/mu'; + $textContent = mb_trim($firstChild->getLiteral()); + $pattern = '/\[!(\w+)](?:\s('.EmojiPattern::getEmojiPattern().'))?/mu'; /** * Extract the callout type and emoji. @@ -157,10 +164,11 @@ protected function setProperties(): void { $this->emoji = empty($matches) ? $this->emoji : $matches[2] ?? ''; // We don't need the callout type and emoji in the text content anymore. - $textContent = trim(preg_replace($pattern, '', $textContent)); + $textContent = mb_trim(preg_replace($pattern, '', $textContent)); if ($textContent === '') { $firstChild->detach(); + return; } diff --git a/src/NotionBlocks/FencedCode.php b/src/NotionBlocks/FencedCode.php index 669589e..c4fe5f5 100644 --- a/src/NotionBlocks/FencedCode.php +++ b/src/NotionBlocks/FencedCode.php @@ -1,5 +1,7 @@ 'block', 'type' => 'code', - 'code' => array( + 'code' => [ 'caption' => [], 'rich_text' => $this->richText($this->node), 'language' => $this->language(), - ), - ); + ], + ]; } /** - * @inheritDoc + * {@inheritDoc} */ - protected function richText(Node|bool $node): array { + protected function richText(Node|bool $node): array + { $richText = (new RichText($node))->toArray(); /** @@ -48,7 +53,9 @@ protected function richText(Node|bool $node): array { */ foreach ($richText as $key => $object) { if ($object['type'] === 'text') { - $richText[$key]['text']['content'] = trim($object['text']['content']); + $richText[$key]['text']['content'] = mb_trim($object['text']['content']); + // code blocks should not have annotations otherwise it will not be syntax highlighted correctly + unset($richText[$key]['annotations']); } } @@ -61,12 +68,12 @@ protected function richText(Node|bool $node): array { * Check the available languages in the Notion API documentation. * * @since 1.0.0 - * * @see https://developers.notion.com/reference/block#code * * @return string The language of the code block. */ - protected function language(): string { + protected function language(): string + { $default = 'plain text'; $language = $this->node->getInfo() ?: $default; @@ -88,80 +95,81 @@ protected function language(): string { * * @return array[] The available languages in the Notion API. */ - protected function notionLanguages(): array { + protected function notionLanguages(): array + { return [ - 'abap' => ['abap'], - 'arduino' => ['arduino'], - 'bash' => ['bash', 'sh', 'shell'], - 'basic' => ['basic'], - 'c' => ['c'], - 'clojure' => ['clojure'], - 'coffeescript' => ['coffeescript', 'coffee'], - 'c++' => ['c++', 'cpp'], - 'c#' => ['c#', 'csharp'], - 'css' => ['css'], - 'dart' => ['dart'], - 'diff' => ['diff'], - 'docker' => ['docker'], - 'elixir' => ['elixir'], - 'elm' => ['elm'], - 'erlang' => ['erlang'], - 'flow' => ['flow'], - 'fortran' => ['fortran'], - 'fsharp' => ['f#', 'fsharp'], - 'gherkin' => ['gherkin'], - 'glsl' => ['glsl'], - 'go' => ['go', 'golang'], - 'graphql' => ['graphql'], - 'groovy' => ['groovy'], - 'haskell' => ['haskell'], - 'html' => ['html'], - 'java' => ['java'], - 'javascript' => ['javascript', 'js'], - 'json' => ['json'], - 'julia' => ['julia'], - 'kotlin' => ['kotlin'], - 'latex' => ['latex'], - 'less' => ['less'], - 'lisp' => ['lisp'], - 'livescript' => ['livescript'], - 'lua' => ['lua'], - 'makefile' => ['makefile'], - 'markdown' => ['markdown', 'md'], - 'markup' => ['markup'], - 'matlab' => ['matlab'], - 'mermaid' => ['mermaid'], - 'nix' => ['nix'], - 'objective-c' => ['objective-c', 'objc'], - 'ocaml' => ['ocaml'], - 'pascal' => ['pascal'], - 'perl' => ['perl'], - 'php' => ['php'], - 'plain text' => ['plain text', 'plaintext', 'text'], - 'powershell' => ['powershell', 'ps1'], - 'prolog' => ['prolog'], - 'protobuf' => ['protobuf'], - 'python' => ['python', 'py'], - 'r' => ['r'], - 'reason' => ['reason'], - 'ruby' => ['ruby', 'rb'], - 'rust' => ['rust'], - 'sass' => ['sass'], - 'scala' => ['scala'], - 'scheme' => ['scheme'], - 'scss' => ['scss'], - 'shell' => ['shell', 'bash'], - 'sql' => ['sql'], - 'swift' => ['swift'], - 'typescript' => ['typescript', 'ts'], - 'vb.net' => ['vb.net', 'vbnet'], - 'verilog' => ['verilog'], - 'vhdl' => ['vhdl'], - 'visual basic' => ['visual basic', 'vb'], - 'webassembly' => ['webassembly', 'wasm'], - 'xml' => ['xml'], - 'yaml' => ['yaml', 'yml'], - 'java/c/c++/c#' => ['java', 'c', 'c++', 'c#'] + 'abap' => ['abap'], + 'arduino' => ['arduino'], + 'bash' => ['bash', 'sh', 'shell'], + 'basic' => ['basic'], + 'c' => ['c'], + 'clojure' => ['clojure'], + 'coffeescript' => ['coffeescript', 'coffee'], + 'c++' => ['c++', 'cpp'], + 'c#' => ['c#', 'csharp'], + 'css' => ['css'], + 'dart' => ['dart'], + 'diff' => ['diff'], + 'docker' => ['docker'], + 'elixir' => ['elixir'], + 'elm' => ['elm'], + 'erlang' => ['erlang'], + 'flow' => ['flow'], + 'fortran' => ['fortran'], + 'fsharp' => ['f#', 'fsharp'], + 'gherkin' => ['gherkin'], + 'glsl' => ['glsl'], + 'go' => ['go', 'golang'], + 'graphql' => ['graphql'], + 'groovy' => ['groovy'], + 'haskell' => ['haskell'], + 'html' => ['html'], + 'java' => ['java'], + 'javascript' => ['javascript', 'js'], + 'json' => ['json'], + 'julia' => ['julia'], + 'kotlin' => ['kotlin'], + 'latex' => ['latex'], + 'less' => ['less'], + 'lisp' => ['lisp'], + 'livescript' => ['livescript'], + 'lua' => ['lua'], + 'makefile' => ['makefile'], + 'markdown' => ['markdown', 'md'], + 'markup' => ['markup'], + 'matlab' => ['matlab'], + 'mermaid' => ['mermaid'], + 'nix' => ['nix'], + 'objective-c' => ['objective-c', 'objc'], + 'ocaml' => ['ocaml'], + 'pascal' => ['pascal'], + 'perl' => ['perl'], + 'php' => ['php'], + 'plain text' => ['plain text', 'plaintext', 'text'], + 'powershell' => ['powershell', 'ps1'], + 'prolog' => ['prolog'], + 'protobuf' => ['protobuf'], + 'python' => ['python', 'py'], + 'r' => ['r'], + 'reason' => ['reason'], + 'ruby' => ['ruby', 'rb'], + 'rust' => ['rust'], + 'sass' => ['sass'], + 'scala' => ['scala'], + 'scheme' => ['scheme'], + 'scss' => ['scss'], + 'shell' => ['shell', 'bash'], + 'sql' => ['sql'], + 'swift' => ['swift'], + 'typescript' => ['typescript', 'ts'], + 'vb.net' => ['vb.net', 'vbnet'], + 'verilog' => ['verilog'], + 'vhdl' => ['vhdl'], + 'visual basic' => ['visual basic', 'vb'], + 'webassembly' => ['webassembly', 'wasm'], + 'xml' => ['xml'], + 'yaml' => ['yaml', 'yml'], + 'java/c/c++/c#' => ['java', 'c', 'c++', 'c#'], ]; } } diff --git a/src/NotionBlocks/Heading.php b/src/NotionBlocks/Heading.php index 8589ae7..a8037a1 100644 --- a/src/NotionBlocks/Heading.php +++ b/src/NotionBlocks/Heading.php @@ -1,11 +1,14 @@ node->getLevel(); $this->level = $level >= 4 ? 3 : $level; // Notion only supports up to h3. } /** - * @inheritDoc + * {@inheritDoc} */ - public function object(): array { + public function object(): array + { $type = "heading_$this->level"; - return array( + return [ 'object' => 'block', 'type' => $type, - $type => array( + $type => [ 'rich_text' => $this->richText($this->node), 'color' => $this->color(), 'is_toggleable' => $this->isToggleable(), - ), - ); + ], + ]; } /** @@ -51,7 +56,8 @@ public function object(): array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { return 'default'; } @@ -62,7 +68,8 @@ protected function color(): string { * * @return bool Whether the block is toggleable or not. */ - protected function isToggleable(): bool { + protected function isToggleable(): bool + { return false; } } diff --git a/src/NotionBlocks/Image.php b/src/NotionBlocks/Image.php new file mode 100644 index 0000000..0f73981 --- /dev/null +++ b/src/NotionBlocks/Image.php @@ -0,0 +1,62 @@ +validator = new ImageValidator; + } + + /** + * {@inheritDoc} + */ + public function object(): array + { + $url = $this->node->getUrl(); + + if (!$this->isValid()) { + return ImageType::INVALID->get($url); + } + + $title = $this->node->getTitle(); + + if ($this->validator->isExternalUrl($url)) { + return ImageType::EXTERNAL->get($url, $title); + } + + return ImageType::FILE->get($url, $title); + } + + /** + * Check if the image is valid for Notion. + * + * @since 1.0.0 + * + * @return bool True if the image is valid, false otherwise. + */ + private function isValid(): bool + { + return $this->validator->isValidNotionImage($this->node->getUrl()); + } +} diff --git a/src/NotionBlocks/ListBlock.php b/src/NotionBlocks/ListBlock.php index 78f7a05..5328175 100644 --- a/src/NotionBlocks/ListBlock.php +++ b/src/NotionBlocks/ListBlock.php @@ -1,5 +1,7 @@ node->getListData()->type === 'ordered' ? 'numbered_list_item' : 'bulleted_list_item'; @@ -36,14 +40,14 @@ public function object(): array { foreach ($this->node->children() as $listItem) { $isToDo = $this->isToDo($listItem); - $objects[] = $isToDo ? (new TodoBlock($listItem))->object() : array( + $objects[] = $isToDo ? (new TodoBlock($listItem))->object() : [ 'object' => 'block', 'type' => $type, $type => [ 'rich_text' => $this->richText($listItem->children()[0]), 'color' => $this->color(), ], - ); + ]; } return $objects; @@ -56,7 +60,8 @@ public function object(): array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { return 'default'; } @@ -65,10 +70,11 @@ protected function color(): string { * * @since 1.0.0 * - * @param Node $listItem The list item node. + * @param Node $listItem The list item node. * @return bool Whether the list item is a to-do item. */ - protected function isToDo(Node $listItem): bool { + protected function isToDo(Node $listItem): bool + { return $listItem->firstChild()?->firstChild() instanceof TaskListItemMarker; } } diff --git a/src/NotionBlocks/Paragraph.php b/src/NotionBlocks/Paragraph.php index 2b05ced..32b32a9 100644 --- a/src/NotionBlocks/Paragraph.php +++ b/src/NotionBlocks/Paragraph.php @@ -1,34 +1,38 @@ 'block', 'type' => 'paragraph', - 'paragraph' => array( + 'paragraph' => [ 'rich_text' => $this->richText($this->node), 'color' => $this->color(), - ), - ); + ], + ]; } /** @@ -38,7 +42,8 @@ public function object(): array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { return 'default'; } } diff --git a/src/NotionBlocks/Table.php b/src/NotionBlocks/Table.php new file mode 100644 index 0000000..4e99658 --- /dev/null +++ b/src/NotionBlocks/Table.php @@ -0,0 +1,124 @@ +hasHeader(); + + // Process table sections + foreach ($this->node->children() as $section) { + if (!$section instanceof TableSection) { + continue; + } + + foreach ($section->children() as $row) { + if ($row instanceof CommonMarkTableRow) { + $cells = $this->getRowCells($row); + $tableWidth = max($tableWidth, count($cells)); + $tableRows[] = [ + 'type' => 'table_row', + 'table_row' => ['cells' => $cells], + ]; + } + } + } + + // Ensure minimum requirements for Notion table + if (empty($tableRows)) { + $tableWidth = 1; + $tableRows[] = [ + 'type' => 'table_row', + 'table_row' => ['cells' => [[]]], + ]; + } + + return [ + [ + 'type' => 'table', + 'table' => [ + 'table_width' => $tableWidth, + 'has_column_header' => $hasHeader, + 'has_row_header' => false, + 'children' => $tableRows, + ], + ], + ]; + } + + /** + * Check if the table has a header row by checking for separators. + * + * @return bool True if table has headers + */ + private function hasHeader(): bool + { + // Check if there are at least 2 sections (header + body) + $sections = []; + foreach ($this->node->children() as $section) { + if ($section instanceof TableSection) { + $sections[] = $section; + } + } + + return count($sections) > 1; + } + + /** + * Extract cells from a table row. + * + * @param CommonMarkTableRow $row The table row node. + * @return array The cells as rich text arrays. + */ + private function getRowCells(CommonMarkTableRow $row): array + { + $cells = []; + + foreach ($row->children() as $cell) { + $richTextContent = $this->richText($cell); + + // If the cell is empty (no content), add empty array + if (empty($richTextContent)) { + $cells[] = []; + + continue; + } + + // Check for single empty text content + if (count($richTextContent) === 1 && + isset($richTextContent[0]['text']['content']) && + mb_trim($richTextContent[0]['text']['content']) === '') { + $cells[] = []; + + continue; + } + + $cells[] = $richTextContent; + } + + return $cells; + } +} diff --git a/src/NotionBlocks/TodoBlock.php b/src/NotionBlocks/TodoBlock.php index e020f40..03d9e68 100644 --- a/src/NotionBlocks/TodoBlock.php +++ b/src/NotionBlocks/TodoBlock.php @@ -1,5 +1,7 @@ 'block', 'type' => 'to_do', @@ -49,7 +53,7 @@ public function object(): array { * @var Text $rawText */ $rawText = $this->node->children()[0]?->children()[1]; - $rawText->setLiteral(trim($rawText->getLiteral())); + $rawText->setLiteral(mb_trim($rawText->getLiteral())); // Detach the `TaskListItemMarker` node from the list item to avoid empty rich text results. $taskListItem->detach(); @@ -66,7 +70,8 @@ public function object(): array { * * @return string The color of the block. */ - protected function color(): string { + protected function color(): string + { return 'default'; } } diff --git a/src/Objects/ImageLikeLink.php b/src/Objects/ImageLikeLink.php new file mode 100644 index 0000000..a1f0619 --- /dev/null +++ b/src/Objects/ImageLikeLink.php @@ -0,0 +1,25 @@ +link->getUrl(); + } + + public function getTitle(): ?string + { + return $this->altText ?: $this->link->getTitle(); + } +} diff --git a/src/Objects/NotionBlock.php b/src/Objects/NotionBlock.php index 226900e..790334c 100644 --- a/src/Objects/NotionBlock.php +++ b/src/Objects/NotionBlock.php @@ -1,10 +1,13 @@ toArray() : []; } } diff --git a/src/Objects/RichText.php b/src/Objects/RichText.php index 2b7f165..afa81bc 100644 --- a/src/Objects/RichText.php +++ b/src/Objects/RichText.php @@ -1,5 +1,7 @@ childNodes = $this->node->hasChildren() ? $this->node->children() : [$this->node]; } + /** + * Get the default object. + * The default object is a Notion object. + * + * @since 1.0.0 + * + * @return array The default object. + */ + public static function defaultObject(): array + { + return [ + 'type' => 'text', + 'text' => [ + 'content' => '', + 'link' => null, + ], + 'annotations' => [ + 'bold' => false, + 'italic' => false, + 'strikethrough' => false, + 'underline' => false, + 'code' => false, + 'color' => 'default', + ], + ]; + } + + /** + * Convert the rich text to an array. + * + * @since 1.0.0 + * + * @return array The rich text. + */ + public function toArray(): array + { + return !empty($this->childNodes) ? $this->objects() : []; + } + /** * Get the rich text objects. * These are the objects that a rich text can have: text and annotations. * * @since 1.0.0 - * * @see https://developers.notion.com/reference/rich-text * * @return array The rich text objects. */ - protected function objects(): array { + private function objects(): array + { $objects = []; foreach ($this->childNodes as $node) { - $object = $this->defaultObject(); + $object = self::defaultObject(); $object['text']['content'] = $this->getTextContent($node); - // If `$object['text']['content']` length is more than 2000 characters, split it into multiple objects. - if (strlen($object['text']['content']) > 2000) { + // If `$object['text']['content']` length is more than 1950 characters, split it into multiple objects. + if (mb_strlen($object['text']['content']) > 1950) { $content = $object['text']['content']; - while (strlen($content) > 2000) { - $object['text']['content'] = substr($content, 0, 2000); - $content = substr($content, 2000); + while (mb_strlen($content) > 1950) { + $object['text']['content'] = mb_substr($content, 0, 1950); + $content = mb_substr($content, 1950); $objects[] = $object; - $object = $this->defaultObject(); + $object = self::defaultObject(); } $object['text']['content'] = $content; @@ -73,12 +116,18 @@ protected function objects(): array { $object['text']['link'] = ['url' => $link]; } - // If `$object['text']['link']['url']` length is more than 2000 characters, set a `null` value. - if (is_array($object['text']['link']) && strlen($object['text']['link']['url']) > 2000) { + // If `$object['text']['link']['url']` length is more than 1950 characters, set a `null` value. + if (is_array($object['text']['link']) && mb_strlen($object['text']['link']['url']) > 1950) { $object['text']['link'] = null; } - $object['annotations'] = array_merge($object['annotations'], $this->getAnnotations($node)); + $annotations = $this->getAnnotations($node); + $object['annotations'] = array_merge($object['annotations'], $annotations); + + // Set color for code annotations + if (isset($annotations['code']) && $annotations['code']) { + $object['annotations']['color'] = 'red'; + } $objects[] = $object; } @@ -91,10 +140,11 @@ protected function objects(): array { * * @since 1.0.0 * - * @param Node $node The node. + * @param Node $node The node. * @return string The text content. */ - protected function getTextContent(Node $node): string { + private function getTextContent(Node $node): string + { if ($node instanceof StringContainerInterface) { return $node->getLiteral(); } @@ -117,10 +167,11 @@ protected function getTextContent(Node $node): string { * * @since 1.0.0 * - * @param Node $node The node. + * @param Node $node The node. * @return string The link. */ - protected function getLink(Node $node): string { + private function getLink(Node $node): string + { if ($node instanceof AbstractWebResource) { return $node->getUrl(); } @@ -144,10 +195,11 @@ protected function getLink(Node $node): string { * * @since 1.0.0 * - * @param Node $node The node. + * @param Node $node The node. * @return array The annotations. */ - protected function getAnnotations(Node $node): array { + private function getAnnotations(Node $node): array + { $annotations = []; // If `$node` is a strong node, set bold annotation to true. @@ -165,7 +217,7 @@ protected function getAnnotations(Node $node): array { $annotations['strikethrough'] = true; } - // If `$node` is a strikethrough node, set strikethrough annotation to true. + // If `$node` is a code node, set code annotation to true. if ($node instanceof Code) { $annotations['code'] = true; } @@ -188,41 +240,4 @@ protected function getAnnotations(Node $node): array { return $annotations; } - - /** - * Get the default object. - * The default object is a Notion object. - * - * @since 1.0.0 - * - * @return array The default object. - */ - protected function defaultObject(): array { - return [ - 'type' => 'text', - 'text' => [ - 'content' => '', - 'link' => null, - ], - 'annotations' => [ - 'bold' => false, - 'italic' => false, - 'strikethrough' => false, - 'underline' => false, - 'code' => false, - 'color' => 'default', - ], - ]; - } - - /** - * Convert the rich text to an array. - * - * @since 1.0.0 - * - * @return array The rich text. - */ - public function toArray(): array { - return !empty($this->childNodes) ? $this->objects() : []; - } } diff --git a/src/Validators/ImageValidator.php b/src/Validators/ImageValidator.php new file mode 100644 index 0000000..20b8648 --- /dev/null +++ b/src/Validators/ImageValidator.php @@ -0,0 +1,89 @@ +isExternalUrl($url)) { + return true; + } + + // Check if URL has a supported file extension + return $this->hasSupportedExtension($url); + } + + /** + * Get list of supported image extensions. + * + * @since 1.0.0 + * + * @return array Array of supported extensions. + */ + public function getSupportedExtensions(): array + { + return self::SUPPORTED_EXTENSIONS; + } + + /** + * Check if the URL is an external URL. + * + * @since 1.0.0 + * + * @param string $url The URL to check. + * @return bool True if external URL, false otherwise. + */ + public function isExternalUrl(string $url): bool + { + return filter_var($url, FILTER_VALIDATE_URL) !== false; + } + + /** + * Check if the URL has a supported file extension. + * + * @since 1.0.0 + * + * @param string $url The URL to check. + * @return bool True if has supported extension, false otherwise. + */ + private function hasSupportedExtension(string $url): bool + { + $parsedUrl = parse_url($url); + if ($parsedUrl === false || !isset($parsedUrl['path'])) { + return false; + } + + $extension = mb_strtolower(pathinfo($parsedUrl['path'], PATHINFO_EXTENSION)); + + return in_array($extension, self::SUPPORTED_EXTENSIONS, true); + } +} diff --git a/tests/BlockAnnotationsTest.php b/tests/BlockAnnotationsTest.php index f46d8ca..3c93cb4 100644 --- a/tests/BlockAnnotationsTest.php +++ b/tests/BlockAnnotationsTest.php @@ -1,7 +1,9 @@ false, 'underline' => false, 'code' => true, - 'color' => 'default', + 'color' => 'red', ], ], [ @@ -127,7 +129,7 @@ }); test('a paragraph has a strikethrough annotation', function () { - $markdown = << A simple block quote. MD; @@ -34,7 +36,7 @@ }); test('a block quote has some annotations', function () { - $markdown = << This is my *text* with ~~some~~ **annotations**. > My second block **quote**. MD; @@ -218,7 +220,7 @@ }); test('a block quote has empty content', function () { - $markdown = << MD; @@ -235,7 +237,7 @@ }); test('a block quote has empty content in two lines', function () { - $markdown = << > MD; diff --git a/tests/CalloutTest.php b/tests/CalloutTest.php index 6212342..b0c8443 100644 --- a/tests/CalloutTest.php +++ b/tests/CalloutTest.php @@ -1,7 +1,9 @@ [!NOTE] This is a callout. MD; @@ -35,7 +37,7 @@ }); test('a simple callout with emoji', function () { - $markdown = << [!NOTE] ⛏️ This is a callout. MD; @@ -72,7 +74,7 @@ }); test('a callout with title and body', function () { - $markdown = << [!NOTE] This is a callout title. > This is a callout body. MD; @@ -137,7 +139,7 @@ }); test('a callout with title, body, and emoji', function () { - $markdown = << [!NOTE] ⛏️ A callout title. > This is a callout. MD; @@ -205,7 +207,7 @@ }); test('a callout with title, body, emoji, and annotations', function () { - $markdown = << [!NOTE] ⛏️ A **callout** title. > This is a *callout*. MD; @@ -333,7 +335,7 @@ }); test('an empty callout', function () { - $markdown = << [!NOTE] MD; @@ -367,7 +369,7 @@ }); test('the first string in a callout with emoji is an annotation', function () { - $markdown = << [!NOTE] ⛏️ **This** is a callout. MD; @@ -419,7 +421,7 @@ }); test('the first string in a callout without emoji is an annotation', function () { - $markdown = << [!NOTE] **This** is a callout. MD; diff --git a/tests/FencedCodeBlockTest.php b/tests/FencedCodeBlockTest.php index b540dbc..18a63f5 100644 --- a/tests/FencedCodeBlockTest.php +++ b/tests/FencedCodeBlockTest.php @@ -1,7 +1,9 @@ "echo 'Hello, World!';", 'link' => null, ], - 'annotations' => [ - 'bold' => false, - 'italic' => false, - 'strikethrough' => false, - 'underline' => false, - 'code' => false, - 'color' => 'default', - ], ], ], 'language' => 'php', @@ -37,7 +31,7 @@ }); test('a fenced code has an invalid language', function () { - $markdown = << "echo 'Hello, World!';", 'link' => null, ], - 'annotations' => [ - 'bold' => false, - 'italic' => false, - 'strikethrough' => false, - 'underline' => false, - 'code' => false, - 'color' => 'default', - ], ], ], 'language' => 'plain text', @@ -71,3 +57,84 @@ expect(convert($markdown))->toBe(expectedJson($expected)); }); + +test('a fenced code block with more than 1950 characters is split into multiple rich text objects', function () { + // Generate a code string that's over 1950 characters + $longCode = str_repeat('echo "This is a very long line of code that will be repeated many times to exceed the 1950 character limit. ";'.PHP_EOL, 30); + + $markdown = <<toBeArray(); + expect($converted)->not->toBeEmpty(); + + // The result is wrapped in double arrays + $blocks = $converted[0]; + expect($blocks)->toBeArray(); + expect($blocks)->not->toBeEmpty(); + + // Verify it's a code block + expect($blocks[0]['type'])->toBe('code'); + + // Get the rich text array + $richText = $blocks[0]['code']['rich_text']; + + // Verify we have multiple rich text objects + expect(count($richText))->toBeGreaterThan(1); + + // Verify each rich text object has content <= 1950 characters + foreach ($richText as $textObject) { + expect(mb_strlen($textObject['text']['content']))->toBeLessThanOrEqual(1950); + } + + // Verify the total content matches the original (minus any trimming) + $totalContent = ''; + foreach ($richText as $textObject) { + $totalContent .= $textObject['text']['content']; + } + expect(mb_trim($totalContent))->toBe(mb_trim($longCode)); +}); + +test('a fenced code block with exactly 1951 characters is split correctly', function () { + // Create a string that's exactly 1951 characters + $codeContent = str_repeat('a', 1951); + + $markdown = << 'block', + 'type' => 'code', + 'code' => [ + 'caption' => [], + 'rich_text' => [ + [ + 'type' => 'text', + 'text' => [ + 'content' => str_repeat('a', 1950), + 'link' => null, + ], + ], + [ + 'type' => 'text', + 'text' => [ + 'content' => 'a', + 'link' => null, + ], + ], + ], + 'language' => 'python', + ], + ]; + + expect(convert($markdown))->toBe(expectedJson($expected)); +}); diff --git a/tests/HeadingBlockTest.php b/tests/HeadingBlockTest.php index 86726e4..6e1c372 100644 --- a/tests/HeadingBlockTest.php +++ b/tests/HeadingBlockTest.php @@ -1,7 +1,9 @@ getUrl())->toBe('https://example.com/image.jpg'); +}); + +test('ImageLikeLink returns alt text as title when provided', function () { + $link = new Link('https://example.com/image.jpg', 'Link Title'); + $imageLikeLink = new ImageLikeLink($link, 'Alt Text'); + + expect($imageLikeLink->getTitle())->toBe('Alt Text'); +}); + +test('ImageLikeLink falls back to link title when alt text is empty', function () { + $link = new Link('https://example.com/image.jpg'); + $link->setTitle('Link Title'); + $imageLikeLink = new ImageLikeLink($link, ''); + + expect($imageLikeLink->getTitle())->toBe('Link Title'); +}); + +test('ImageLikeLink falls back to link title when alt text is whitespace', function () { + $link = new Link('https://example.com/image.jpg'); + $link->setTitle('Link Title'); + $imageLikeLink = new ImageLikeLink($link, ' '); + + expect($imageLikeLink->getTitle())->toBe(' '); +}); + +test('ImageLikeLink handles link without title', function () { + $link = new Link('https://example.com/image.jpg'); + $imageLikeLink = new ImageLikeLink($link, 'Alt Text'); + + expect($imageLikeLink->getUrl())->toBe('https://example.com/image.jpg') + ->and($imageLikeLink->getTitle())->toBe('Alt Text'); +}); + +test('ImageLikeLink handles empty alt text and no link title', function () { + $link = new Link('https://example.com/image.jpg'); + $imageLikeLink = new ImageLikeLink($link, ''); + + expect($imageLikeLink->getUrl())->toBe('https://example.com/image.jpg') + ->and($imageLikeLink->getTitle())->toBeNull(); +}); + +test('ImageLikeLink preserves original link URL formatting', function () { + $url = 'https://example.com/path/to/image.jpg?param=value&other=test'; + $link = new Link($url, 'Title'); + $imageLikeLink = new ImageLikeLink($link, 'Alt'); + + expect($imageLikeLink->getUrl())->toBe($url); +}); + +test('ImageLikeLink handles relative URLs', function () { + $link = new Link('./images/local-image.jpg', 'Local Image'); + $imageLikeLink = new ImageLikeLink($link, 'Local Alt Text'); + + expect($imageLikeLink->getUrl())->toBe('./images/local-image.jpg') + ->and($imageLikeLink->getTitle())->toBe('Local Alt Text'); +}); + +test('ImageLikeLink handles special characters in alt text', function () { + $link = new Link('https://example.com/image.jpg'); + $altText = 'Image with "quotes" & special chars '; + $imageLikeLink = new ImageLikeLink($link, $altText); + + expect($imageLikeLink->getTitle())->toBe($altText); +}); + +test('ImageLikeLink handles unicode characters in alt text', function () { + $link = new Link('https://example.com/image.jpg'); + $altText = 'Image with unicode: 🖼️ 中文 العربية'; + $imageLikeLink = new ImageLikeLink($link, $altText); + + expect($imageLikeLink->getTitle())->toBe($altText); +}); diff --git a/tests/ImageTest.php b/tests/ImageTest.php new file mode 100644 index 0000000..a61d1bc --- /dev/null +++ b/tests/ImageTest.php @@ -0,0 +1,216 @@ +setTitle('Alt text'); + $imageBlock = new Image($imageNode); + + $expected = [ + 'object' => 'block', + 'type' => 'image', + 'image' => [ + 'type' => 'external', + 'external' => [ + 'url' => 'https://example.com/image.jpg', + ], + 'caption' => [ + [ + 'type' => 'text', + 'text' => [ + 'content' => 'Alt text', + 'link' => null, + ], + 'annotations' => [ + 'bold' => false, + 'italic' => false, + 'strikethrough' => false, + 'underline' => false, + 'code' => false, + 'color' => 'default', + ], + ], + ], + ], + ]; + + expect($imageBlock->object())->toBe($expected); +}); + +test('Image block with relative path generates file type', function () { + $imageNode = new CommonMarkImage('./images/local-image.jpg'); + $imageNode->setTitle('Local image'); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['image']['type'])->toBe('file') + ->and($result['image']['file']['url'])->toBe('./images/local-image.jpg'); +}); + +test('Image block without title generates empty caption', function () { + $imageNode = new CommonMarkImage('https://example.com/image.jpg'); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['image']['caption'])->toBe([]); +}); + +test('Image block with empty title generates empty caption', function () { + $imageNode = new CommonMarkImage('https://example.com/image.jpg', ''); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['image']['caption'])->toBe([]); +}); + +test('Image block with ImageLikeLink generates correct block', function () { + $link = new Link('https://example.com/image.jpg', 'Link Title'); + $imageLikeLink = new ImageLikeLink($link, 'Alt Text'); + $imageBlock = new Image($imageLikeLink); + + $expected = [ + 'object' => 'block', + 'type' => 'image', + 'image' => [ + 'type' => 'external', + 'external' => [ + 'url' => 'https://example.com/image.jpg', + ], + 'caption' => [ + [ + 'type' => 'text', + 'text' => [ + 'content' => 'Alt Text', + 'link' => null, + ], + 'annotations' => [ + 'bold' => false, + 'italic' => false, + 'strikethrough' => false, + 'underline' => false, + 'code' => false, + 'color' => 'default', + ], + ], + ], + ], + ]; + + expect($imageBlock->object())->toBe($expected); +}); + +test('Image block differentiates between external and file URLs', function () { + // Test external URL + $externalImage = new CommonMarkImage('https://example.com/image.jpg', 'External'); + $externalBlock = new Image($externalImage); + $externalResult = $externalBlock->object(); + + expect($externalResult['image']['type'])->toBe('external') + ->and($externalResult['image'])->toHaveKey('external') + ->and($externalResult['image'])->not->toHaveKey('file'); + + // Test file path + $fileImage = new CommonMarkImage('local-image.jpg', 'Local'); + $fileBlock = new Image($fileImage); + $fileResult = $fileBlock->object(); + + expect($fileResult['image']['type'])->toBe('file') + ->and($fileResult['image'])->toHaveKey('file') + ->and($fileResult['image'])->not->toHaveKey('external'); +}); + +test('Image block handles special characters in caption', function () { + $title = 'Image with "quotes" & special chars '; + $imageNode = new CommonMarkImage('https://example.com/image.jpg'); + $imageNode->setTitle($title); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['image']['caption'][0]['text']['content'])->toBe($title); +}); + +test('Image block handles unicode characters in caption', function () { + $title = 'Image with unicode: 🖼️ 中文 العربية'; + $imageNode = new CommonMarkImage('https://example.com/image.jpg'); + $imageNode->setTitle($title); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['image']['caption'][0]['text']['content'])->toBe($title); +}); + +test('Image block caption has correct structure', function () { + $imageNode = new CommonMarkImage('https://example.com/image.jpg'); + $imageNode->setTitle('Test caption'); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + $caption = $result['image']['caption'][0]; + + expect($caption)->toHaveKeys(['type', 'text', 'annotations']) + ->and($caption['type'])->toBe('text') + ->and($caption['text'])->toHaveKeys(['content', 'link']) + ->and($caption['text']['link'])->toBeNull() + ->and($caption['annotations'])->toHaveKeys([ + 'bold', + 'italic', + 'strikethrough', + 'underline', + 'code', + 'color', + ]); +}); + +test('Image block from full markdown integration', function () { + $markdown = '![Alt text](https://example.com/image.jpg)'; + + $expected = [ + 'object' => 'block', + 'type' => 'image', + 'image' => [ + 'type' => 'external', + 'external' => [ + 'url' => 'https://example.com/image.jpg', + ], + 'caption' => [], + ], + ]; + + expect(convert($markdown))->toBe(expectedJson($expected)); +}); + +test('Image block handles various URL formats', function () { + $urls = [ + 'https://example.com/image.jpg', + 'http://example.com/image.png', + 'https://cdn.example.com/path/to/image.gif?v=1&size=large', + './images/local.jpg', + '../assets/image.png', + '/absolute/path/image.svg', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', + ]; + + foreach ($urls as $url) { + $imageNode = new CommonMarkImage($url); + $imageNode->setTitle('Test'); + $imageBlock = new Image($imageNode); + $result = $imageBlock->object(); + + $isExternal = filter_var($url, FILTER_VALIDATE_URL) !== false; + $expectedType = $isExternal ? 'external' : 'file'; + + expect($result['image']['type'])->toBe($expectedType) + ->and($result['image'][$expectedType]['url'])->toBe($url); + } +}); diff --git a/tests/ImageValidatorTest.php b/tests/ImageValidatorTest.php new file mode 100644 index 0000000..edf7380 --- /dev/null +++ b/tests/ImageValidatorTest.php @@ -0,0 +1,145 @@ +isValidNotionImage($url))->toBeTrue("URL $url should be valid"); + } +}); + +test('ImageValidator rejects unsupported image extensions', function () { + $validator = new ImageValidator; + $invalidUrls = [ + 'https://example.com/image.webp', + 'https://example.com/image.avif', + 'https://example.com/image.ico', + 'https://example.com/document.pdf', + 'https://example.com/video.mp4', + 'https://example.com/audio.mp3', + 'https://example.com/no-extension', + ]; + + foreach ($invalidUrls as $url) { + expect($validator->isValidNotionImage($url))->toBeFalse("URL $url should be invalid"); + } +}); + +test('ImageValidator handles URLs with query parameters', function () { + $validator = new ImageValidator; + expect($validator->isValidNotionImage('https://example.com/image.jpg?v=1&size=large'))->toBeTrue() + ->and($validator->isValidNotionImage('https://example.com/image.webp?v=1&size=large'))->toBeFalse(); +}); + +test('ImageValidator handles URLs with fragments', function () { + $validator = new ImageValidator; + expect($validator->isValidNotionImage('https://example.com/image.png#section'))->toBeTrue() + ->and($validator->isValidNotionImage('https://example.com/image.webp#section'))->toBeFalse(); +}); + +test('ImageValidator handles case insensitive extensions', function () { + $validator = new ImageValidator; + expect($validator->isValidNotionImage('https://example.com/image.JPG'))->toBeTrue() + ->and($validator->isValidNotionImage('https://example.com/image.PNG'))->toBeTrue() + ->and($validator->isValidNotionImage('https://example.com/image.WEBP'))->toBeFalse(); +}); + +test('ImageValidator allows non-external URLs', function () { + $validator = new ImageValidator; + $nonExternalUrls = [ + './images/local.jpg', + '../assets/image.png', + '/absolute/path/image.svg', + 'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mZ8/5+hHgAHggJ/PchI7wAAAABJRU5ErkJggg==', + ]; + + foreach ($nonExternalUrls as $url) { + expect($validator->isValidNotionImage($url))->toBeTrue("URL $url should be valid"); + } +}); + +test('ImageValidator returns supported extensions list', function () { + $validator = new ImageValidator; + $extensions = $validator->getSupportedExtensions(); + + expect($extensions)->toBeArray() + ->and($extensions)->toContain('jpg', 'jpeg', 'png', 'gif', 'svg', 'bmp', 'tif', 'tiff', 'heic'); +}); + +test('Image block converts invalid images to paragraph blocks', function () { + $imageNode = new CommonMarkImage('https://example.com/image.webp', 'Invalid image'); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['type'])->toBe('paragraph') + ->and($result['paragraph']['rich_text'][0]['text']['content'])->toBe('[Invalid image: https://example.com/image.webp]') + ->and($result['paragraph']['rich_text'][0]['annotations']['italic'])->toBeTrue() + ->and($result['paragraph']['rich_text'][0]['annotations']['color'])->toBe('gray'); +}); + +test('Image block preserves valid images', function () { + $imageNode = new CommonMarkImage('https://example.com/image.jpg', 'Valid image'); + $imageBlock = new Image($imageNode); + + $result = $imageBlock->object(); + + expect($result['type'])->toBe('image') + ->and($result['image']['external']['url'])->toBe('https://example.com/image.jpg'); +}); + +test('Image block handles mixed valid and invalid images in markdown', function () { + $validMarkdown = '![Valid](https://example.com/image.jpg)'; + $invalidMarkdown = '![Invalid](https://example.com/image.webp)'; + + $validResult = json_decode(convert($validMarkdown), true); + $invalidResult = json_decode(convert($invalidMarkdown), true); + + expect($validResult[0][0]['type'])->toBe('image') + ->and($invalidResult[0][0]['type'])->toBe('paragraph') + ->and($invalidResult[0][0]['paragraph']['rich_text'][0]['text']['content'])->toContain('[Invalid image:'); +}); + +test('ImageValidator handles malformed URLs gracefully', function () { + $validator = new ImageValidator; + $malformedUrls = [ + 'not-a-url', + 'http://', + 'https://', + 'ftp://example.com/image.jpg', + '', + ]; + + foreach ($malformedUrls as $url) { + expect($validator->isValidNotionImage($url))->toBeTrue("Malformed URL $url should be treated as non-external"); + } +}); + +test('ImageValidator handles URLs without file extensions', function () { + $validator = new ImageValidator; + expect($validator->isValidNotionImage('https://example.com/image'))->toBeFalse() + ->and($validator->isValidNotionImage('https://example.com/path/to/image'))->toBeFalse(); +}); + +test('ImageValidator handles URLs with multiple dots', function () { + $validator = new ImageValidator; + expect($validator->isValidNotionImage('https://sub.example.com/image.file.jpg'))->toBeTrue() + ->and($validator->isValidNotionImage('https://sub.example.com/image.file.webp'))->toBeFalse(); +}); diff --git a/tests/ListBlockTest.php b/tests/ListBlockTest.php index f381273..5d02d90 100644 --- a/tests/ListBlockTest.php +++ b/tests/ListBlockTest.php @@ -1,7 +1,9 @@ addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = '![Alt text](https://example.com/image.jpg)'; + $document = $parser->parse($markdown); + + $images = $processor->extractImages($document); + + expect($images)->toHaveCount(1) + ->and($images[0])->toBeInstanceOf(Image::class) + ->and($images[0]->getUrl())->toBe('https://example.com/image.jpg'); +}); + +test('extractImages extracts image-like links from document', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = '![Alt text](https://example.com/image.jpg "Title")'; + $document = $parser->parse($markdown); + + $images = $processor->extractImages($document); + + expect($images)->toHaveCount(1) + ->and($images[0])->toBeInstanceOf(Image::class); +}); + +test('extractImages handles mixed content with images and text', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = 'Some text ![Image 1](https://example.com/1.jpg) more text ![Image 2](https://example.com/2.jpg)'; + $document = $parser->parse($markdown); + + $images = $processor->extractImages($document); + + expect($images)->toHaveCount(2) + ->and($images[0])->toBeInstanceOf(Image::class) + ->and($images[1])->toBeInstanceOf(Image::class) + ->and($images[0]->getUrl())->toBe('https://example.com/1.jpg') + ->and($images[1]->getUrl())->toBe('https://example.com/2.jpg'); +}); + +test('extractImages handles nested nodes', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = <<<'MD' +# Heading + +Some paragraph with ![image](https://example.com/image.jpg). + +> Quote with ![another image](https://example.com/image2.jpg) +MD; + + $document = $parser->parse($markdown); + + $images = $processor->extractImages($document); + + expect($images)->toHaveCount(2) + ->and($images[0]->getUrl())->toBe('https://example.com/image.jpg') + ->and($images[1]->getUrl())->toBe('https://example.com/image2.jpg'); +}); + +test('containsOnlyImages returns true for paragraph with only images', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = '![Image](https://example.com/image.jpg)'; + $document = $parser->parse($markdown); + + // Get the first paragraph + $paragraph = $document->firstChild(); + + expect($paragraph)->toBeInstanceOf(Paragraph::class) + ->and($processor->containsOnlyImages($paragraph))->toBeTrue(); +}); + +test('containsOnlyImages returns false for paragraph with text and images', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = 'Some text ![Image](https://example.com/image.jpg) more text'; + $document = $parser->parse($markdown); + + // Get the first paragraph + $paragraph = $document->firstChild(); + + expect($paragraph)->toBeInstanceOf(Paragraph::class) + ->and($processor->containsOnlyImages($paragraph))->toBeFalse(); +}); + +test('containsOnlyImages returns false for paragraph with only text', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = 'Just some text without images'; + $document = $parser->parse($markdown); + + // Get the first paragraph + $paragraph = $document->firstChild(); + + expect($paragraph)->toBeInstanceOf(Paragraph::class) + ->and($processor->containsOnlyImages($paragraph))->toBeFalse(); +}); + +test('containsOnlyImages handles multiple images with whitespace', function () { + $processor = new MarkdownImageProcessor(); + + $environment = new Environment(); + $environment->addExtension(new CommonMarkCoreExtension()); + $parser = new MarkdownParser($environment); + + $markdown = '![Image 1](https://example.com/1.jpg) ![Image 2](https://example.com/2.jpg)'; + $document = $parser->parse($markdown); + + // Get the first paragraph + $paragraph = $document->firstChild(); + + expect($paragraph)->toBeInstanceOf(Paragraph::class) + ->and($processor->containsOnlyImages($paragraph))->toBeTrue(); +}); + +test('containsOnlyImages returns false for empty paragraph', function () { + $processor = new MarkdownImageProcessor(); + + // Create an empty paragraph manually + $paragraph = new Paragraph(); + + expect($processor->containsOnlyImages($paragraph))->toBeFalse(); +}); diff --git a/tests/NotionBlockTest.php b/tests/NotionBlockTest.php index 7a2e8f0..0696390 100644 --- a/tests/NotionBlockTest.php +++ b/tests/NotionBlockTest.php @@ -1,13 +1,11 @@ 'text', 'text' => [ - 'content' => 'Artificial Intelligence (AI) has evolved from a theoretical concept to a tangible force that is reshaping numerous industries worldwide. AI refers to the development of systems that can perform tasks typically requiring human intelligence, such as decision-making, problem-solving, and pattern recognition. While AI has been around for decades, the explosion of data, advances in computational power, and improvements in machine learning algorithms have accelerated its adoption across various sectors. One of the key areas where AI is making a significant impact is healthcare. The healthcare industry has always been data-rich, but the ability to analyze that data effectively has been limited by human capacity. With AI, healthcare professionals can now process vast amounts of data at unprecedented speeds, leading to more accurate diagnoses and personalized treatment plans. AI-powered diagnostic tools, such as image recognition software, are now being used to detect diseases like cancer, often at earlier stages than traditional methods. In addition, AI is being employed in drug discovery, helping researchers identify potential drug candidates more efficiently, speeding up the process of bringing new treatments to market. Beyond healthcare, AI is transforming the financial industry. Banks and financial institutions are increasingly utilizing AI to enhance customer service, improve risk management, and detect fraudulent activities. AI-driven chatbots provide customers with immediate assistance, streamlining the service process and improving customer satisfaction. In terms of risk management, AI algorithms can analyze market trends and predict financial risks with higher accuracy than human analysts, helping institutions make more informed investment decisions. Moreover, AI has revolutionized the fight against financial crime by detecting anomalies in transaction patterns, flagging potential fraud in real-time. The retail industry is also benefiting greatly from the adoption ', + 'content' => 'Artificial Intelligence (AI) has evolved from a theoretical concept to a tangible force that is reshaping numerous industries worldwide. AI refers to the development of systems that can perform tasks typically requiring human intelligence, such as decision-making, problem-solving, and pattern recognition. While AI has been around for decades, the explosion of data, advances in computational power, and improvements in machine learning algorithms have accelerated its adoption across various sectors. One of the key areas where AI is making a significant impact is healthcare. The healthcare industry has always been data-rich, but the ability to analyze that data effectively has been limited by human capacity. With AI, healthcare professionals can now process vast amounts of data at unprecedented speeds, leading to more accurate diagnoses and personalized treatment plans. AI-powered diagnostic tools, such as image recognition software, are now being used to detect diseases like cancer, often at earlier stages than traditional methods. In addition, AI is being employed in drug discovery, helping researchers identify potential drug candidates more efficiently, speeding up the process of bringing new treatments to market. Beyond healthcare, AI is transforming the financial industry. Banks and financial institutions are increasingly utilizing AI to enhance customer service, improve risk management, and detect fraudulent activities. AI-driven chatbots provide customers with immediate assistance, streamlining the service process and improving customer satisfaction. In terms of risk management, AI algorithms can analyze market trends and predict financial risks with higher accuracy than human analysts, helping institutions make more informed investment decisions. Moreover, AI has revolutionized the fight against financial crime by detecting anomalies in transaction patterns, flagging potential fraud in real-time. The retail indu', 'link' => null, ], 'annotations' => [ @@ -28,7 +30,7 @@ [ 'type' => 'text', 'text' => [ - 'content' => 'of AI. E-commerce giants like Amazon and Alibaba have pioneered the use of AI in understanding consumer behavior, improving supply chain logistics, and optimizing pricing strategies. By analyzing customer preferences and shopping habits, AI algorithms can create personalized shopping experiences, recommending products that align with individual tastes. AI also powers dynamic pricing, adjusting prices based on demand, competition, and market conditions in real-time, ensuring that retailers stay competitive while maximizing profits. On the supply chain front, AI tools can predict demand fluctuations, optimizing inventory levels and reducing wastage. In the realm of manufacturing, AI is ushering in a new era of efficiency through automation and predictive maintenance. Industrial robots powered by AI can perform repetitive tasks with a level of precision that is difficult to achieve through manual labor. These robots are not only enhancing productivity but also improving worker safety by taking on hazardous tasks. Furthermore, AI-based predictive maintenance systems monitor the health of equipment, detecting issues before they lead to costly breakdowns. This not only reduces downtime but also extends the lifespan of machinery, saving manufacturers significant amounts in repair and replacement costs. AI’s influence is also being felt in the education sector, where it is changing the way students learn and teachers teach. AI-powered tutoring systems can provide personalized learning experiences for students, adapting to their individual learning paces and styles. This helps address the varying needs of students, ensuring that no one is left behind. AI tools are also assisting teachers by automating administrative tasks like grading, freeing up more time for lesson planning and student interaction. Additionally, AI-driven analytics can provide educators with insights into student performance, helping them identify areas where students may need additional support. Another', + 'content' => "stry is also benefiting greatly from the adoption of AI. E-commerce giants like Amazon and Alibaba have pioneered the use of AI in understanding consumer behavior, improving supply chain logistics, and optimizing pricing strategies. By analyzing customer preferences and shopping habits, AI algorithms can create personalized shopping experiences, recommending products that align with individual tastes. AI also powers dynamic pricing, adjusting prices based on demand, competition, and market conditions in real-time, ensuring that retailers stay competitive while maximizing profits. On the supply chain front, AI tools can predict demand fluctuations, optimizing inventory levels and reducing wastage. In the realm of manufacturing, AI is ushering in a new era of efficiency through automation and predictive maintenance. Industrial robots powered by AI can perform repetitive tasks with a level of precision that is difficult to achieve through manual labor. These robots are not only enhancing productivity but also improving worker safety by taking on hazardous tasks. Furthermore, AI-based predictive maintenance systems monitor the health of equipment, detecting issues before they lead to costly breakdowns. This not only reduces downtime but also extends the lifespan of machinery, saving manufacturers significant amounts in repair and replacement costs. AI's influence is also being felt in the education sector, where it is changing the way students learn and teachers teach. AI-powered tutoring systems can provide personalized learning experiences for students, adapting to their individual learning paces and styles. This helps address the varying needs of students, ensuring that no one is left behind. AI tools are also assisting teachers by automating administrative tasks like grading, freeing up more time for lesson planning and student interaction. Additionally, AI-driven analytics can provide educators with insights into st", 'link' => null, ], 'annotations' => [ @@ -43,7 +45,7 @@ [ 'type' => 'text', 'text' => [ - 'content' => ' industry where AI is making waves is transportation. The development of autonomous vehicles, powered by AI, promises to revolutionize the way we move goods and people. Companies like Tesla, Waymo, and Uber are at the forefront of developing self-driving cars, which have the potential to reduce traffic accidents, decrease fuel consumption, and improve transportation efficiency. In the logistics sector, AI is optimizing route planning, reducing delivery times, and cutting costs. AI-powered drones and robots are also being explored for last-mile delivery solutions, particularly in areas where traditional transportation methods face challenges. Entertainment and media are not immune to the AI revolution. AI is increasingly being used to create personalized content recommendations, whether it\'s movies on Netflix or songs on Spotify. By analyzing user preferences and consumption patterns, AI can suggest content that aligns with individual tastes, keeping users engaged for longer periods. In addition, AI-generated content, such as music, artwork, and even articles, is becoming more prevalent. AI algorithms are being trained to compose music, design logos, and write news stories, showcasing the creative potential of machines. While the benefits of AI are vast, there are also challenges and ethical considerations that come with its widespread adoption. One of the primary concerns is job displacement. As AI systems take over tasks previously performed by humans, there is a fear that many jobs will become obsolete. This is particularly relevant in industries like manufacturing and retail, where automation is reducing the need for manual labor. However, proponents of AI argue that while some jobs may be lost, new opportunities will emerge, particularly in fields related to AI development, data science, and AI ethics. Another significant concern is the issue of bias in AI systems. AI algorithms are only as good as the data they are trained on. If the data is biased, the AI syst', + 'content' => "udent performance, helping them identify areas where students may need additional support. Another industry where AI is making waves is transportation. The development of autonomous vehicles, powered by AI, promises to revolutionize the way we move goods and people. Companies like Tesla, Waymo, and Uber are at the forefront of developing self-driving cars, which have the potential to reduce traffic accidents, decrease fuel consumption, and improve transportation efficiency. In the logistics sector, AI is optimizing route planning, reducing delivery times, and cutting costs. AI-powered drones and robots are also being explored for last-mile delivery solutions, particularly in areas where traditional transportation methods face challenges. Entertainment and media are not immune to the AI revolution. AI is increasingly being used to create personalized content recommendations, whether it's movies on Netflix or songs on Spotify. By analyzing user preferences and consumption patterns, AI can suggest content that aligns with individual tastes, keeping users engaged for longer periods. In addition, AI-generated content, such as music, artwork, and even articles, is becoming more prevalent. AI algorithms are being trained to compose music, design logos, and write news stories, showcasing the creative potential of machines. While the benefits of AI are vast, there are also challenges and ethical considerations that come with its widespread adoption. One of the primary concerns is job displacement. As AI systems take over tasks previously performed by humans, there is a fear that many jobs will become obsolete. This is particularly relevant in industries like manufacturing and retail, where automation is reducing the need for manual labor. However, proponents of AI argue that while some jobs may be lost, new opportunities will emerge, particularly in fields related to AI development, data science, and AI ethics. Another signif", 'link' => null, ], 'annotations' => [ @@ -58,7 +60,7 @@ [ 'type' => 'text', 'text' => [ - 'content' => 'em can perpetuate and even amplify those biases, leading to unfair outcomes. For example, biased AI algorithms in hiring processes could disadvantage certain demographic groups, reinforcing existing inequalities. As AI continues to permeate various aspects of society, there is a growing need for transparency and accountability in how these systems are developed and deployed. In conclusion, AI is transforming industries at an unprecedented pace, offering countless benefits in terms of efficiency, cost savings, and innovation. From healthcare and finance to education and entertainment, the applications of AI are vast and varied. However, as we continue to embrace this powerful technology, it is crucial to address the ethical challenges and ensure that AI is used responsibly, with a focus on fairness and inclusivity. The future of AI holds immense potential, and how we navigate its development will shape the world for generations to come.', + 'content' => 'icant concern is the issue of bias in AI systems. AI algorithms are only as good as the data they are trained on. If the data is biased, the AI system can perpetuate and even amplify those biases, leading to unfair outcomes. For example, biased AI algorithms in hiring processes could disadvantage certain demographic groups, reinforcing existing inequalities. As AI continues to permeate various aspects of society, there is a growing need for transparency and accountability in how these systems are developed and deployed. In conclusion, AI is transforming industries at an unprecedented pace, offering countless benefits in terms of efficiency, cost savings, and innovation. From healthcare and finance to education and entertainment, the applications of AI are vast and varied. However, as we continue to embrace this powerful technology, it is crucial to address the ethical challenges and ensure that AI is used responsibly, with a focus on fairness and inclusivity. The future of AI holds immense potential, and how we navigate its development will shape the world for generations to come.', 'link' => null, ], 'annotations' => [ @@ -78,8 +80,8 @@ expect(convert($markdown))->toBe(expectedJson($expected)); }); -test('a link returns null value because has more than 2000 characters', function () { - $markdown = <<toHaveCount(1) + ->and($blocks[0])->toHaveCount(1); + + $table = $blocks[0][0]; + expect($table['type'])->toBe('table') + ->and($table['table']['table_width'])->toBe(3) + ->and($table['table']['has_column_header'])->toBe(true) + ->and($table['table']['has_row_header'])->toBe(false) + ->and($table['table']['children'])->toHaveCount(3); + // 1 header + 2 data rows +}); + +it('converts table with header to notion blocks with correct structure', function () { + $markdown = '| Name | Age | Occupation | +|------|-----|------------| +| Alice | 28 | Software Engineer | +| Bob | 34 | Designer | +| Charlie | 25 | Product Manager |'; + + $result = convert($markdown); + $blocks = json_decode($result, true); + + $table = $blocks[0][0]; + $children = $table['table']['children']; + + // Check header row + expect($children[0]['type'])->toBe('table_row') + ->and($children[0]['table_row']['cells'])->toHaveCount(3) + ->and($children[0]['table_row']['cells'][0][0]['text']['content'])->toBe('Name') + ->and($children[0]['table_row']['cells'][1][0]['text']['content'])->toBe('Age') + ->and($children[0]['table_row']['cells'][2][0]['text']['content'])->toBe('Occupation') + ->and($children[1]['table_row']['cells'][0][0]['text']['content'])->toBe('Alice') + ->and($children[1]['table_row']['cells'][1][0]['text']['content'])->toBe('28') + ->and($children[1]['table_row']['cells'][2][0]['text']['content'])->toBe('Software Engineer') + ->and($children[2]['table_row']['cells'][0][0]['text']['content'])->toBe('Bob') + ->and($children[2]['table_row']['cells'][1][0]['text']['content'])->toBe('34') + ->and($children[2]['table_row']['cells'][2][0]['text']['content'])->toBe('Designer') + ->and($children[3]['table_row']['cells'][0][0]['text']['content'])->toBe('Charlie') + ->and($children[3]['table_row']['cells'][1][0]['text']['content'])->toBe('25') + ->and($children[3]['table_row']['cells'][2][0]['text']['content'])->toBe('Product Manager'); +}); + +it('converts table with empty headers to notion blocks', function () { + $markdown = '| | | | +|---|---|---| +| Alice | 28 | Engineer | +| Bob | 34 | Designer |'; + + $result = convert($markdown); + $blocks = json_decode($result, true); + + $table = $blocks[0][0]; + expect($table['table']['has_column_header'])->toBe(true) + ->and($table['table']['children'])->toHaveCount(3); // CommonMark always has headers + // 1 empty header + 2 data rows +}); + +it('handles empty table cells', function () { + $markdown = '| Name | Age | Occupation | +|------|-----|------------| +| Alice | | Engineer | +| | 34 | |'; + + $result = convert($markdown); + $blocks = json_decode($result, true); + + $table = $blocks[0][0]; + $children = $table['table']['children']; + + expect($children[1]['table_row']['cells'][1])->toBeArray() + ->and($children[1]['table_row']['cells'][1])->toBeEmpty() + ->and($children[2]['table_row']['cells'][0])->toBeArray() + ->and($children[2]['table_row']['cells'][0])->toBeEmpty() + ->and($children[2]['table_row']['cells'][2])->toBeArray() + ->and($children[2]['table_row']['cells'][2])->toBeEmpty(); +}); + +it('handles table with different column widths', function () { + $markdown = '| A | B | C | D | +|---|---|---|---| +| 1 | 2 | +| X | Y | Z |'; + + $result = convert($markdown); + $blocks = json_decode($result, true); + + $table = $blocks[0][0]; + expect($table['table']['table_width'])->toBe(4); +}); diff --git a/tests/ToDoBlockTest.php b/tests/ToDoBlockTest.php index 63f40b2..9746ea6 100644 --- a/tests/ToDoBlockTest.php +++ b/tests/ToDoBlockTest.php @@ -1,7 +1,9 @@