diff --git a/.github/workflows/phpPackage.yml b/.github/workflows/phpPackage.yml new file mode 100644 index 0000000..640cd41 --- /dev/null +++ b/.github/workflows/phpPackage.yml @@ -0,0 +1,51 @@ +name: Publish PHP Package + +on: + workflow_dispatch: + inputs: + version: + description: "Version to publish (e.g. 1.0.0)" + required: true + type: string + +jobs: + test-and-publish: + runs-on: ubuntu-latest + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + + - name: Set up PHP + uses: shivammathur/setup-php@v2 + with: + php-version: "8.1" + tools: composer + + - name: Install Dependencies + working-directory: php + run: composer install + + - name: Run Tests + working-directory: php + run: ./vendor/bin/phpunit + + - name: Tag Release + run: | + git config user.name "GitHub Actions" + git config user.email "actions@github.com" + git tag -a "php-v${{ github.event.inputs.version }}" -m "PHP Package v${{ github.event.inputs.version }}" + git push origin "php-v${{ github.event.inputs.version }}" + + - name: Notify Packagist + run: | + STATUS=$(curl -s -o /dev/null -w "%{http_code}" -XPOST \ + -H "content-type: application/json" \ + "https://packagist.org/api/update-package?username=${{ secrets.PACKAGIST_USERNAME }}&apiToken=${{ secrets.PACKAGIST_TOKEN }}" \ + -d "{\"repository\":{\"url\":\"https://github.com/${{ github.repository }}\"}}") + echo "Packagist API response: $STATUS" + if [[ "$STATUS" -lt 200 || "$STATUS" -ge 300 ]]; then + echo "Packagist notification failed with status $STATUS" + exit 1 + fi diff --git a/.gitignore b/.gitignore index 9b26ed0..1174072 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,4 @@ node_modules -lib \ No newline at end of file +lib +php/vendor/ +php/.phpunit.result.cache \ No newline at end of file diff --git a/.npmignore b/.npmignore index 156c059..55912dd 100644 --- a/.npmignore +++ b/.npmignore @@ -1,3 +1,4 @@ node_modules src -formatter.kt \ No newline at end of file +formatter.kt +php \ No newline at end of file diff --git a/package.json b/package.json index 4eeb0a7..0eee0de 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,8 @@ "mustache": "^4.2.0" }, "files": [ - "*" + "*", + "!php" ], "packageManager": "yarn@1.22.19+sha512.ff4579ab459bb25aa7c0ff75b62acebe576f6084b36aa842971cf250a5d8c6cd3bc9420b22ce63c7f93a0857bc6ef29291db39c3e7a23aab5adfd5a4dd6c5d71" } diff --git a/php/composer.json b/php/composer.json new file mode 100644 index 0000000..a187143 --- /dev/null +++ b/php/composer.json @@ -0,0 +1,21 @@ +{ + "name": "vestaboard/vbml", + "description": "The Vestaboard Markup Language for PHP", + "type": "library", + "license": "GNU", + "authors": [{"name": "Vestaboard"}], + "require": { + "php": "^8.1", + "mustache/mustache": "^2.14" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "autoload": { + "psr-4": {"Vestaboard\\Vbml\\": "src/"} + }, + "autoload-dev": { + "psr-4": {"Vestaboard\\Vbml\\Tests\\": "tests/"} + }, + "minimum-stability": "stable" +} diff --git a/php/composer.lock b/php/composer.lock new file mode 100644 index 0000000..2d1e671 --- /dev/null +++ b/php/composer.lock @@ -0,0 +1,1741 @@ +{ + "_readme": [ + "This file locks the dependencies of your project to a known state", + "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", + "This file is @generated automatically" + ], + "content-hash": "3cc9411722dfa9154fa89b35b62bb259", + "packages": [ + { + "name": "mustache/mustache", + "version": "v2.14.2", + "source": { + "type": "git", + "url": "https://github.com/bobthecow/mustache.php.git", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/e62b7c3849d22ec55f3ec425507bf7968193a6cb", + "reference": "e62b7c3849d22ec55f3ec425507bf7968193a6cb", + "shasum": "" + }, + "require": { + "php": ">=5.2.4" + }, + "require-dev": { + "friendsofphp/php-cs-fixer": "~1.11", + "phpunit/phpunit": "~3.7|~4.0|~5.0" + }, + "type": "library", + "autoload": { + "psr-0": { + "Mustache": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Justin Hileman", + "email": "justin@justinhileman.info", + "homepage": "http://justinhileman.com" + } + ], + "description": "A Mustache implementation in PHP.", + "homepage": "https://github.com/bobthecow/mustache.php", + "keywords": [ + "mustache", + "templating" + ], + "support": { + "issues": "https://github.com/bobthecow/mustache.php/issues", + "source": "https://github.com/bobthecow/mustache.php/tree/v2.14.2" + }, + "time": "2022-08-23T13:07:01+00:00" + } + ], + "packages-dev": [ + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, + { + "name": "nikic/php-parser", + "version": "v5.7.0", + "source": { + "type": "git", + "url": "https://github.com/nikic/PHP-Parser.git", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "shasum": "" + }, + "require": { + "ext-ctype": "*", + "ext-json": "*", + "ext-tokenizer": "*", + "php": ">=7.4" + }, + "require-dev": { + "ircmaxell/php-yacc": "^0.0.7", + "phpunit/phpunit": "^9.0" + }, + "bin": [ + "bin/php-parse" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.x-dev" + } + }, + "autoload": { + "psr-4": { + "PhpParser\\": "lib/PhpParser" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Nikita Popov" + } + ], + "description": "A PHP parser written in PHP", + "keywords": [ + "parser", + "php" + ], + "support": { + "issues": "https://github.com/nikic/PHP-Parser/issues", + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" + }, + "time": "2025-12-06T11:56:16+00:00" + }, + { + "name": "phar-io/manifest", + "version": "2.0.4", + "source": { + "type": "git", + "url": "https://github.com/phar-io/manifest.git", + "reference": "54750ef60c58e43759730615a392c31c80e23176" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/manifest/zipball/54750ef60c58e43759730615a392c31c80e23176", + "reference": "54750ef60c58e43759730615a392c31c80e23176", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-phar": "*", + "ext-xmlwriter": "*", + "phar-io/version": "^3.0.1", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "2.0.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Component for reading phar.io manifest information from a PHP Archive (PHAR)", + "support": { + "issues": "https://github.com/phar-io/manifest/issues", + "source": "https://github.com/phar-io/manifest/tree/2.0.4" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2024-03-03T12:33:53+00:00" + }, + { + "name": "phar-io/version", + "version": "3.2.1", + "source": { + "type": "git", + "url": "https://github.com/phar-io/version.git", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phar-io/version/zipball/4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "reference": "4f7fd7836c6f332bb2933569e566a0d6c4cbed74", + "shasum": "" + }, + "require": { + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + }, + { + "name": "Sebastian Heuer", + "email": "sebastian@phpeople.de", + "role": "Developer" + }, + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "Developer" + } + ], + "description": "Library for handling version information and constraints", + "support": { + "issues": "https://github.com/phar-io/version/issues", + "source": "https://github.com/phar-io/version/tree/3.2.1" + }, + "time": "2022-02-21T01:04:05+00:00" + }, + { + "name": "phpunit/php-code-coverage", + "version": "10.1.16", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-code-coverage.git", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/7e308268858ed6baedc8704a304727d20bc07c77", + "reference": "7e308268858ed6baedc8704a304727d20bc07c77", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-libxml": "*", + "ext-xmlwriter": "*", + "nikic/php-parser": "^4.19.1 || ^5.1.0", + "php": ">=8.1", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-text-template": "^3.0.1", + "sebastian/code-unit-reverse-lookup": "^3.0.0", + "sebastian/complexity": "^3.2.0", + "sebastian/environment": "^6.1.0", + "sebastian/lines-of-code": "^2.0.2", + "sebastian/version": "^4.0.1", + "theseer/tokenizer": "^1.2.3" + }, + "require-dev": { + "phpunit/phpunit": "^10.1" + }, + "suggest": { + "ext-pcov": "PHP extension that provides line coverage", + "ext-xdebug": "PHP extension that provides line coverage as well as branch and path coverage" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.1.x-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library that provides collection, processing, and rendering functionality for PHP code coverage information.", + "homepage": "https://github.com/sebastianbergmann/php-code-coverage", + "keywords": [ + "coverage", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-code-coverage/issues", + "security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy", + "source": "https://github.com/sebastianbergmann/php-code-coverage/tree/10.1.16" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-08-22T04:31:57+00:00" + }, + { + "name": "phpunit/php-file-iterator", + "version": "4.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-file-iterator.git", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-file-iterator/zipball/a95037b6d9e608ba092da1b23931e537cadc3c3c", + "reference": "a95037b6d9e608ba092da1b23931e537cadc3c3c", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "FilterIterator implementation that filters files based on a list of suffixes.", + "homepage": "https://github.com/sebastianbergmann/php-file-iterator/", + "keywords": [ + "filesystem", + "iterator" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-file-iterator/issues", + "security": "https://github.com/sebastianbergmann/php-file-iterator/security/policy", + "source": "https://github.com/sebastianbergmann/php-file-iterator/tree/4.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T06:24:48+00:00" + }, + { + "name": "phpunit/php-invoker", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-invoker.git", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-invoker/zipball/f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "reference": "f5e568ba02fa5ba0ddd0f618391d5a9ea50b06d7", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "ext-pcntl": "*", + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-pcntl": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Invoke callables with a timeout", + "homepage": "https://github.com/sebastianbergmann/php-invoker/", + "keywords": [ + "process" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-invoker/issues", + "source": "https://github.com/sebastianbergmann/php-invoker/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:56:09+00:00" + }, + { + "name": "phpunit/php-text-template", + "version": "3.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-text-template.git", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-text-template/zipball/0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "reference": "0c7b06ff49e3d5072f057eb1fa59258bf287a748", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Simple template engine.", + "homepage": "https://github.com/sebastianbergmann/php-text-template/", + "keywords": [ + "template" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-text-template/issues", + "security": "https://github.com/sebastianbergmann/php-text-template/security/policy", + "source": "https://github.com/sebastianbergmann/php-text-template/tree/3.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-08-31T14:07:24+00:00" + }, + { + "name": "phpunit/php-timer", + "version": "6.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/php-timer.git", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/php-timer/zipball/e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "reference": "e2a2d67966e740530f4a3343fe2e030ffdc1161d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Utility class for timing", + "homepage": "https://github.com/sebastianbergmann/php-timer/", + "keywords": [ + "timer" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/php-timer/issues", + "source": "https://github.com/sebastianbergmann/php-timer/tree/6.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:57:52+00:00" + }, + { + "name": "phpunit/phpunit", + "version": "10.5.63", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/phpunit.git", + "reference": "33198268dad71e926626b618f3ec3966661e4d90" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/33198268dad71e926626b618f3ec3966661e4d90", + "reference": "33198268dad71e926626b618f3ec3966661e4d90", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-json": "*", + "ext-libxml": "*", + "ext-mbstring": "*", + "ext-xml": "*", + "ext-xmlwriter": "*", + "myclabs/deep-copy": "^1.13.4", + "phar-io/manifest": "^2.0.4", + "phar-io/version": "^3.2.1", + "php": ">=8.1", + "phpunit/php-code-coverage": "^10.1.16", + "phpunit/php-file-iterator": "^4.1.0", + "phpunit/php-invoker": "^4.0.0", + "phpunit/php-text-template": "^3.0.1", + "phpunit/php-timer": "^6.0.0", + "sebastian/cli-parser": "^2.0.1", + "sebastian/code-unit": "^2.0.0", + "sebastian/comparator": "^5.0.5", + "sebastian/diff": "^5.1.1", + "sebastian/environment": "^6.1.0", + "sebastian/exporter": "^5.1.4", + "sebastian/global-state": "^6.0.2", + "sebastian/object-enumerator": "^5.0.0", + "sebastian/recursion-context": "^5.0.1", + "sebastian/type": "^4.0.0", + "sebastian/version": "^4.0.1" + }, + "suggest": { + "ext-soap": "To be able to generate mocks based on WSDL files" + }, + "bin": [ + "phpunit" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "10.5-dev" + } + }, + "autoload": { + "files": [ + "src/Framework/Assert/Functions.php" + ], + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "The PHP Unit Testing framework.", + "homepage": "https://phpunit.de/", + "keywords": [ + "phpunit", + "testing", + "xunit" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/phpunit/issues", + "security": "https://github.com/sebastianbergmann/phpunit/security/policy", + "source": "https://github.com/sebastianbergmann/phpunit/tree/10.5.63" + }, + "funding": [ + { + "url": "https://phpunit.de/sponsors.html", + "type": "custom" + }, + { + "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/phpunit", + "type": "tidelift" + } + ], + "time": "2026-01-27T05:48:37+00:00" + }, + { + "name": "sebastian/cli-parser", + "version": "2.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/cli-parser.git", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/cli-parser/zipball/c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "reference": "c34583b87e7b7a8055bf6c450c2c77ce32a24084", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for parsing CLI options", + "homepage": "https://github.com/sebastianbergmann/cli-parser", + "support": { + "issues": "https://github.com/sebastianbergmann/cli-parser/issues", + "security": "https://github.com/sebastianbergmann/cli-parser/security/policy", + "source": "https://github.com/sebastianbergmann/cli-parser/tree/2.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:12:49+00:00" + }, + { + "name": "sebastian/code-unit", + "version": "2.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit.git", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit/zipball/a81fee9eef0b7a76af11d121767abc44c104e503", + "reference": "a81fee9eef0b7a76af11d121767abc44c104e503", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the PHP code units", + "homepage": "https://github.com/sebastianbergmann/code-unit", + "support": { + "issues": "https://github.com/sebastianbergmann/code-unit/issues", + "source": "https://github.com/sebastianbergmann/code-unit/tree/2.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:58:43+00:00" + }, + { + "name": "sebastian/code-unit-reverse-lookup", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/code-unit-reverse-lookup.git", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/code-unit-reverse-lookup/zipball/5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "reference": "5e3a687f7d8ae33fb362c5c0743794bbb2420a1d", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Looks up which function or method a line of code belongs to", + "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/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T06:59:15+00:00" + }, + { + "name": "sebastian/comparator", + "version": "5.0.5", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/comparator.git", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "reference": "55dfef806eb7dfeb6e7a6935601fef866f8ca48d", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/diff": "^5.0", + "sebastian/exporter": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@2bepublished.at" + } + ], + "description": "Provides the functionality to compare PHP values for equality", + "homepage": "https://github.com/sebastianbergmann/comparator", + "keywords": [ + "comparator", + "compare", + "equality" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/comparator/issues", + "security": "https://github.com/sebastianbergmann/comparator/security/policy", + "source": "https://github.com/sebastianbergmann/comparator/tree/5.0.5" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/comparator", + "type": "tidelift" + } + ], + "time": "2026-01-24T09:25:16+00:00" + }, + { + "name": "sebastian/complexity", + "version": "3.2.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/complexity.git", + "reference": "68ff824baeae169ec9f2137158ee529584553799" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/complexity/zipball/68ff824baeae169ec9f2137158ee529584553799", + "reference": "68ff824baeae169ec9f2137158ee529584553799", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.2-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for calculating the complexity of PHP code units", + "homepage": "https://github.com/sebastianbergmann/complexity", + "support": { + "issues": "https://github.com/sebastianbergmann/complexity/issues", + "security": "https://github.com/sebastianbergmann/complexity/security/policy", + "source": "https://github.com/sebastianbergmann/complexity/tree/3.2.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:37:17+00:00" + }, + { + "name": "sebastian/diff", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/diff.git", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/diff/zipball/c41e007b4b62af48218231d6c2275e4c9b975b2e", + "reference": "c41e007b4b62af48218231d6c2275e4c9b975b2e", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0", + "symfony/process": "^6.4" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Kore Nordmann", + "email": "mail@kore-nordmann.de" + } + ], + "description": "Diff implementation", + "homepage": "https://github.com/sebastianbergmann/diff", + "keywords": [ + "diff", + "udiff", + "unidiff", + "unified diff" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/diff/issues", + "security": "https://github.com/sebastianbergmann/diff/security/policy", + "source": "https://github.com/sebastianbergmann/diff/tree/5.1.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:15:17+00:00" + }, + { + "name": "sebastian/environment", + "version": "6.1.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/environment.git", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/environment/zipball/8074dbcd93529b357029f5cc5058fd3e43666984", + "reference": "8074dbcd93529b357029f5cc5058fd3e43666984", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "suggest": { + "ext-posix": "*" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Provides functionality to handle HHVM/PHP environments", + "homepage": "https://github.com/sebastianbergmann/environment", + "keywords": [ + "Xdebug", + "environment", + "hhvm" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/environment/issues", + "security": "https://github.com/sebastianbergmann/environment/security/policy", + "source": "https://github.com/sebastianbergmann/environment/tree/6.1.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-23T08:47:14+00:00" + }, + { + "name": "sebastian/exporter", + "version": "5.1.4", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/exporter.git", + "reference": "0735b90f4da94969541dac1da743446e276defa6" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/exporter/zipball/0735b90f4da94969541dac1da743446e276defa6", + "reference": "0735b90f4da94969541dac1da743446e276defa6", + "shasum": "" + }, + "require": { + "ext-mbstring": "*", + "php": ">=8.1", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.1-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Volker Dusch", + "email": "github@wallbash.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + }, + { + "name": "Bernhard Schussek", + "email": "bschussek@gmail.com" + } + ], + "description": "Provides the functionality to export PHP variables for visualization", + "homepage": "https://www.github.com/sebastianbergmann/exporter", + "keywords": [ + "export", + "exporter" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/exporter/issues", + "security": "https://github.com/sebastianbergmann/exporter/security/policy", + "source": "https://github.com/sebastianbergmann/exporter/tree/5.1.4" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/exporter", + "type": "tidelift" + } + ], + "time": "2025-09-24T06:09:11+00:00" + }, + { + "name": "sebastian/global-state", + "version": "6.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/global-state.git", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/global-state/zipball/987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "reference": "987bafff24ecc4c9ac418cab1145b96dd6e9cbd9", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "ext-dom": "*", + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "6.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Snapshotting of global state", + "homepage": "https://www.github.com/sebastianbergmann/global-state", + "keywords": [ + "global state" + ], + "support": { + "issues": "https://github.com/sebastianbergmann/global-state/issues", + "security": "https://github.com/sebastianbergmann/global-state/security/policy", + "source": "https://github.com/sebastianbergmann/global-state/tree/6.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2024-03-02T07:19:19+00:00" + }, + { + "name": "sebastian/lines-of-code", + "version": "2.0.2", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/lines-of-code.git", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/lines-of-code/zipball/856e7f6a75a84e339195d48c556f23be2ebf75d0", + "reference": "856e7f6a75a84e339195d48c556f23be2ebf75d0", + "shasum": "" + }, + "require": { + "nikic/php-parser": "^4.18 || ^5.0", + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "2.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library for counting the lines of code in PHP source code", + "homepage": "https://github.com/sebastianbergmann/lines-of-code", + "support": { + "issues": "https://github.com/sebastianbergmann/lines-of-code/issues", + "security": "https://github.com/sebastianbergmann/lines-of-code/security/policy", + "source": "https://github.com/sebastianbergmann/lines-of-code/tree/2.0.2" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-12-21T08:38:20+00:00" + }, + { + "name": "sebastian/object-enumerator", + "version": "5.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-enumerator.git", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-enumerator/zipball/202d0e344a580d7f7d04b3fafce6933e59dae906", + "reference": "202d0e344a580d7f7d04b3fafce6933e59dae906", + "shasum": "" + }, + "require": { + "php": ">=8.1", + "sebastian/object-reflector": "^3.0", + "sebastian/recursion-context": "^5.0" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Traverses array structures and object graphs to enumerate all referenced objects", + "homepage": "https://github.com/sebastianbergmann/object-enumerator/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-enumerator/issues", + "source": "https://github.com/sebastianbergmann/object-enumerator/tree/5.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:08:32+00:00" + }, + { + "name": "sebastian/object-reflector", + "version": "3.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/object-reflector.git", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/object-reflector/zipball/24ed13d98130f0e7122df55d06c5c4942a577957", + "reference": "24ed13d98130f0e7122df55d06c5c4942a577957", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "3.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + } + ], + "description": "Allows reflection of object attributes, including inherited and non-public ones", + "homepage": "https://github.com/sebastianbergmann/object-reflector/", + "support": { + "issues": "https://github.com/sebastianbergmann/object-reflector/issues", + "source": "https://github.com/sebastianbergmann/object-reflector/tree/3.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:06:18+00:00" + }, + { + "name": "sebastian/recursion-context", + "version": "5.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/recursion-context.git", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/recursion-context/zipball/47e34210757a2f37a97dcd207d032e1b01e64c7a", + "reference": "47e34210757a2f37a97dcd207d032e1b01e64c7a", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.5" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "5.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de" + }, + { + "name": "Jeff Welch", + "email": "whatthejeff@gmail.com" + }, + { + "name": "Adam Harvey", + "email": "aharvey@php.net" + } + ], + "description": "Provides functionality to recursively process PHP variables", + "homepage": "https://github.com/sebastianbergmann/recursion-context", + "support": { + "issues": "https://github.com/sebastianbergmann/recursion-context/issues", + "security": "https://github.com/sebastianbergmann/recursion-context/security/policy", + "source": "https://github.com/sebastianbergmann/recursion-context/tree/5.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + }, + { + "url": "https://liberapay.com/sebastianbergmann", + "type": "liberapay" + }, + { + "url": "https://thanks.dev/u/gh/sebastianbergmann", + "type": "thanks_dev" + }, + { + "url": "https://tidelift.com/funding/github/packagist/sebastian/recursion-context", + "type": "tidelift" + } + ], + "time": "2025-08-10T07:50:56+00:00" + }, + { + "name": "sebastian/type", + "version": "4.0.0", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/type.git", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/type/zipball/462699a16464c3944eefc02ebdd77882bd3925bf", + "reference": "462699a16464c3944eefc02ebdd77882bd3925bf", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "require-dev": { + "phpunit/phpunit": "^10.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Collection of value objects that represent the types of the PHP type system", + "homepage": "https://github.com/sebastianbergmann/type", + "support": { + "issues": "https://github.com/sebastianbergmann/type/issues", + "source": "https://github.com/sebastianbergmann/type/tree/4.0.0" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-03T07:10:45+00:00" + }, + { + "name": "sebastian/version", + "version": "4.0.1", + "source": { + "type": "git", + "url": "https://github.com/sebastianbergmann/version.git", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/sebastianbergmann/version/zipball/c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "reference": "c51fa83a5d8f43f1402e3f32a005e6262244ef17", + "shasum": "" + }, + "require": { + "php": ">=8.1" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-main": "4.0-dev" + } + }, + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Sebastian Bergmann", + "email": "sebastian@phpunit.de", + "role": "lead" + } + ], + "description": "Library 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/4.0.1" + }, + "funding": [ + { + "url": "https://github.com/sebastianbergmann", + "type": "github" + } + ], + "time": "2023-02-07T11:34:05+00:00" + }, + { + "name": "theseer/tokenizer", + "version": "1.3.1", + "source": { + "type": "git", + "url": "https://github.com/theseer/tokenizer.git", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/theseer/tokenizer/zipball/b7489ce515e168639d17feec34b8847c326b0b3c", + "reference": "b7489ce515e168639d17feec34b8847c326b0b3c", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-tokenizer": "*", + "ext-xmlwriter": "*", + "php": "^7.2 || ^8.0" + }, + "type": "library", + "autoload": { + "classmap": [ + "src/" + ] + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "BSD-3-Clause" + ], + "authors": [ + { + "name": "Arne Blankerts", + "email": "arne@blankerts.de", + "role": "Developer" + } + ], + "description": "A small library for converting tokenized PHP source code into XML and potentially other formats", + "support": { + "issues": "https://github.com/theseer/tokenizer/issues", + "source": "https://github.com/theseer/tokenizer/tree/1.3.1" + }, + "funding": [ + { + "url": "https://github.com/theseer", + "type": "github" + } + ], + "time": "2025-11-17T20:03:58+00:00" + } + ], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": { + "php": "^8.1" + }, + "platform-dev": {}, + "plugin-api-version": "2.9.0" +} diff --git a/php/phpunit.xml b/php/phpunit.xml new file mode 100644 index 0000000..003b92b --- /dev/null +++ b/php/phpunit.xml @@ -0,0 +1,11 @@ + + + + + tests + + + diff --git a/php/src/Calendar.php b/php/src/Calendar.php new file mode 100644 index 0000000..7a75e3e --- /dev/null +++ b/php/src/Calendar.php @@ -0,0 +1,204 @@ + 19, + 'Mon' => 13, + 'Tue' => 20, + 'Wed' => 23, + 'Thu' => 20, + 'Fri' => 6, + 'Sat' => 19, + default => 0, + }; + } + + public static function makeCalendar( + string $calendarMonth, + string $calendarYear, + array $vbmlDays, + ?int $defaultDayColor = null, + bool $hideSMTWTFS = false, + bool $hideDates = false, + bool $hideMonthYear = false + ): array { + $month = (int)$calendarMonth - 1; // 0-based + $year = (int)$calendarYear; + + $numberOfDaysInMonth = (int)date('t', mktime(0, 0, 0, $month + 1, 1, $year)); + $firstDayOfWeek = date('D', mktime(0, 0, 0, $month + 1, 1, $year)); // 'Sun','Mon',... + + $daysOfWeek = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat']; + $offset = array_search($firstDayOfWeek, $daysOfWeek); + if ($offset === false) { + $offset = 0; + } + + $calendarDayColor = $defaultDayColor ?? 65; + + $firstRowDays = ['1', (string)(7 - $offset)]; + $secondRowDays = [(string)(7 - $offset + 1), (string)(7 - $offset + 7)]; + $thirdRowDays = [(string)(7 - $offset + 8), (string)(7 - $offset + 14)]; + $fourthRowDays = [(string)(7 - $offset + 15), (string)(7 - $offset + 21)]; + $fifthStart = 7 - $offset + 22; + $fifthEnd = min(7 - $offset + $numberOfDaysInMonth, $numberOfDaysInMonth); + $fifthRowDays = [(string)$fifthStart, (string)$fifthEnd]; + + $numberOfDaysInLastRow = $fifthEnd - $fifthStart + 1; + + // Build first row + if ($firstRowDays[0] === $firstRowDays[1]) { + $firstRow = array_merge( + [0, 0, 0, $hideDates ? 0 : self::getCharacterCodeForDigit($firstRowDays[0]), 0], + array_fill(0, $offset, 0), + array_fill(0, 7 - $offset, $calendarDayColor), + array_fill(0, 22 - 12, 0) + ); + } else { + $firstRow = array_merge( + [ + 0, + $hideDates ? 0 : self::getCharacterCodeForDigit($firstRowDays[0]), + $hideDates ? 0 : 44, + $hideDates ? 0 : self::getCharacterCodeForDigit($firstRowDays[1]), + 0, + ], + array_fill(0, $offset, 0), + array_fill(0, 7 - $offset, $calendarDayColor), + array_fill(0, 22 - 12, 0) + ); + } + + // Build second row + $s0 = $secondRowDays[0]; + $s1 = $secondRowDays[1]; + $secondRow = array_merge( + mb_strlen($s0) > 1 + ? [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($s0, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($s0, 1, 1))] + : [0, $hideDates ? 0 : self::getCharacterCodeForDigit($s0)], + [$hideDates ? 0 : 44], + mb_strlen($s1) > 1 + ? [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($s1, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($s1, 1, 1))] + : [$hideDates ? 0 : self::getCharacterCodeForDigit($s1), 0], + array_fill(0, 7, $calendarDayColor), + array_fill(0, 22 - 12, 0) + ); + + // Build third row + $t0 = $thirdRowDays[0]; + $t1 = $thirdRowDays[1]; + $thirdRow = array_merge( + mb_strlen($t0) > 1 + ? [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($t0, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($t0, 1, 1))] + : [0, $hideDates ? 0 : self::getCharacterCodeForDigit($t0)], + [$hideDates ? 0 : 44], + [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($t1, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($t1, 1, 1))], + array_fill(0, 7, $calendarDayColor), + array_fill(0, 22 - 12, 0) + ); + + // Build fourth row + $f0 = $fourthRowDays[0]; + $f1 = $fourthRowDays[1]; + $fourthRow = array_merge( + [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($f0, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($f0, 1, 1))], + [$hideDates ? 0 : 44], + [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($f1, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr($f1, 1, 1))], + array_fill(0, 7, $calendarDayColor), + array_fill(0, 22 - 12, 0) + ); + + // Build fifth row + if (count($fifthRowDays) === 0 || $fifthStart > $numberOfDaysInMonth) { + $fifthRow = array_fill(0, 22, 0); + } else { + $fif0 = $fifthRowDays[0]; + $fif1 = $fifthRowDays[1]; + $fifthRow = array_merge( + [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr((string)$fif0, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr((string)$fif0, 1, 1))], + [$hideDates ? 0 : 44], + [$hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr((string)$fif1, 0, 1)), + $hideDates ? 0 : self::getCharacterCodeForDigit(mb_substr((string)$fif1, 1, 1))], + array_fill(0, $numberOfDaysInLastRow, $calendarDayColor), + array_fill(0, 22 - (5 + $numberOfDaysInLastRow), 0) + ); + } + + // Build month/year header + $monthYearStr = (string)($month + 1); + if ($hideMonthYear) { + $monthYear = [0, 0, 0, 0, 0]; + } else { + $monthYear = []; + foreach (str_split($monthYearStr) as $num) { + $monthYear[] = self::getCharacterCodeForDigit($num); + } + $monthYear[] = 59; // slash + $yearStr = (string)$year; + // Last two digits of year + $yearLast2 = mb_substr($yearStr, 2, 2); + foreach (str_split($yearLast2) as $num) { + $monthYear[] = self::getCharacterCodeForDigit($num); + } + } + $headerSpace = 5 - count($monthYear); + + $headerRow = array_merge( + $monthYear, + array_fill(0, $headerSpace, 0), + $hideSMTWTFS + ? [0, 0, 0, 0, 0, 0, 0] + : array_map([self::class, 'getCharCodeForDay'], $daysOfWeek), + array_fill(0, 22 - (7 + 5), 0) + ); + + $calendar = [ + $headerRow, + $firstRow, + $secondRow, + $thirdRow, + $fourthRow, + $fifthRow, + ]; + + // Fill in the days + foreach ($vbmlDays as $vbmlDayKey => $color) { + $day = (int)$vbmlDayKey; + $todaysRow = (int)floor(($day + $offset - 1) / 7) + 1; + $modulus = ($day + $offset - 1) % 7; + $todaysColumn = $todaysRow > 5 ? ($modulus === 0 ? 12 : 13) : $modulus + 5; + $rowIdx = $todaysRow > 5 ? 5 : $todaysRow; + $calendar[$rowIdx][$todaysColumn] = $color; + } + + return $calendar; + } + + public static function parseCalendarComponent(array $characters, int $x): array + { + return [ + 'characters' => $characters, + 'x' => $x, + ]; + } +} diff --git a/php/src/CharacterCode.php b/php/src/CharacterCode.php new file mode 100644 index 0000000..d48a668 --- /dev/null +++ b/php/src/CharacterCode.php @@ -0,0 +1,297 @@ + self::Blank, 'name' => 'Blank', 'mappings' => [' ', '©', '®', '<', '>', '²', '†', '‡', 'ˆ', 'Þ', 'þ', 'µ', '¶', '*', '^', '¬', '«', '»', '›', '³', '¹', '€', '‹', '˜', '÷', 'π', '∆', '√', '∫', '∞']], + ['code' => self::A, 'name' => 'A', 'mappings' => ['A', 'a', 'â', 'à', 'å', 'á', 'À', 'Á', 'Â', 'Ã', 'Å', 'ã', 'ä', 'Ä', '∂', 'œ', 'æ', 'Æ']], + ['code' => self::B, 'name' => 'B', 'mappings' => ['B', 'b']], + ['code' => self::C, 'name' => 'C', 'mappings' => ['C', 'c', 'ç', 'Ç', '¢', 'ć', 'Ć', 'č', 'Č']], + ['code' => self::D, 'name' => 'D', 'mappings' => ['D', 'd', 'Ð', 'ð']], + ['code' => self::E, 'name' => 'E', 'mappings' => ['E', 'e', 'é', 'ê', 'ë', 'è', 'È', 'É', 'Ê', 'Ë', '€', '£', '∑']], + ['code' => self::F, 'name' => 'F', 'mappings' => ['F', 'f', 'ƒ', 'ſ']], + ['code' => self::G, 'name' => 'G', 'mappings' => ['G', 'g', 'ğ', 'Ğ', 'ģ', 'Ģ', 'ġ', 'Ġ', 'ĝ', 'Ĝ']], + ['code' => self::H, 'name' => 'H', 'mappings' => ['H', 'h', 'ħ', 'Ħ', 'ĥ', 'Ĥ']], + ['code' => self::I, 'name' => 'I', 'mappings' => ['I', 'i', 'í', 'ï', 'î', 'ì', 'Ì', 'Í', 'Î', 'Ï', '|', '¡']], + ['code' => self::J, 'name' => 'J', 'mappings' => ['J', 'j', 'ĵ', 'Ĵ', 'į', 'Į']], + ['code' => self::K, 'name' => 'K', 'mappings' => ['K', 'k', 'ķ', 'Ķ', 'ĸ']], + ['code' => self::L, 'name' => 'L', 'mappings' => ['L', 'l', '£', 'ł', 'Ł', 'ļ', 'Ļ', 'ĺ', 'Ĺ', 'ľ', 'Ľ', 'ŀ', 'Ŀ']], + ['code' => self::M, 'name' => 'M', 'mappings' => ['M', 'm']], + ['code' => self::N, 'name' => 'N', 'mappings' => ['N', 'n', 'ñ', 'Ñ', 'ń', 'Ń', 'ň', 'Ň', 'ņ', 'Ņ']], + ['code' => self::O, 'name' => 'O', 'mappings' => ['O', 'o', 'ó', 'ô', 'ò', 'Ò', 'Ó', 'Ô', 'Õ', 'Ø', 'ð', 'õ', 'ø', 'ö', 'Ö']], + ['code' => self::P, 'name' => 'P', 'mappings' => ['P', 'p', 'Þ', 'þ', '¶']], + ['code' => self::Q, 'name' => 'Q', 'mappings' => ['Q', 'q']], + ['code' => self::R, 'name' => 'R', 'mappings' => ['R', 'r', 'ŕ', 'Ŕ', 'ř', 'Ř', 'ŗ', 'Ŗ']], + ['code' => self::S, 'name' => 'S', 'mappings' => ['S', 's', 'š', 'Š', '§', 'ś', 'Ś', 'ş', 'Ş', 'ș', 'Ș']], + ['code' => self::T, 'name' => 'T', 'mappings' => ['T', 't', 'ť', 'Ť', 'ţ', 'Ţ', 'ŧ', 'Ŧ']], + ['code' => self::U, 'name' => 'U', 'mappings' => ['U', 'u', 'û', 'ù', 'ú', 'Ù', 'Ú', 'Û', 'µ', 'ū', 'Ū', 'ů', 'Ů', 'ų', 'Ų', 'Ü']], + ['code' => self::V, 'name' => 'V', 'mappings' => ['V', 'v', 'Ʋ', 'ʋ']], + ['code' => self::W, 'name' => 'W', 'mappings' => ['W', 'w', 'ŵ', 'Ŵ', 'ẁ', 'Ẁ', 'ẃ', 'Ẃ', 'ẅ', 'Ẅ']], + ['code' => self::X, 'name' => 'X', 'mappings' => ['X', 'x', 'ẍ', 'Ẍ']], + ['code' => self::Y, 'name' => 'Y', 'mappings' => ['Y', 'y', 'ý', 'ÿ', 'Ý', 'ŷ', 'Ŷ', 'ỳ', 'Ỳ', 'ỹ', 'Ỹ', 'Ÿ']], + ['code' => self::Z, 'name' => 'Z', 'mappings' => ['Z', 'z', 'ž', 'Ž', 'ź', 'Ź', 'ż', 'Ż']], + ['code' => self::One, 'name' => 'One', 'mappings' => ['1', '¹']], + ['code' => self::Two, 'name' => 'Two', 'mappings' => ['2', '²']], + ['code' => self::Three, 'name' => 'Three', 'mappings' => ['3', '³']], + ['code' => self::Four, 'name' => 'Four', 'mappings' => ['4']], + ['code' => self::Five, 'name' => 'Five', 'mappings' => ['5']], + ['code' => self::Six, 'name' => 'Six', 'mappings' => ['6']], + ['code' => self::Seven, 'name' => 'Seven', 'mappings' => ['7']], + ['code' => self::Eight, 'name' => 'Eight', 'mappings' => ['8']], + ['code' => self::Nine, 'name' => 'Nine', 'mappings' => ['9']], + ['code' => self::Zero, 'name' => 'Zero', 'mappings' => ['0']], + ['code' => self::ExclamationMark, 'name' => 'ExclamationMark', 'mappings' => ['!', 'ǃ']], + ['code' => self::AtSign, 'name' => 'AtSign', 'mappings' => ['@']], + ['code' => self::PoundSign, 'name' => 'PoundSign', 'mappings' => ['#', '№']], + ['code' => self::DollarSign, 'name' => 'DollarSign', 'mappings' => ['$', '¢', '£', '¤', '¥', '₩', '₪', '₫', '€', '₹', '₺', '₽']], + ['code' => self::LeftParen, 'name' => 'LeftParen', 'mappings' => ['(', '[', '{', '⟨', '«']], + ['code' => self::RightParen, 'name' => 'RightParen', 'mappings' => [')', ']', '}', '⟩', '»']], + ['code' => self::Hyphen, 'name' => 'Hyphen', 'mappings' => ['-', '—', '–', '¯', '~', '_']], + ['code' => self::PlusSign, 'name' => 'PlusSign', 'mappings' => ['+', '±', '∓', '∔']], + ['code' => self::Ampersand, 'name' => 'Ampersand', 'mappings' => ['&']], + ['code' => self::EqualsSign, 'name' => 'EqualsSign', 'mappings' => ['=', '≠', '≈', '≡']], + ['code' => self::Semicolon, 'name' => 'Semicolon', 'mappings' => [';', ';', ';']], + ['code' => self::Colon, 'name' => 'Colon', 'mappings' => [':', '¦']], + ['code' => self::SingleQuote, 'name' => 'SingleQuote', 'mappings' => ["'", "\u{2018}", "\u{2019}", '`', '´', '‚', '‛', 'ʹ', 'ʻ', 'ʽ', 'ʾ', 'ʿ', 'ˈ', 'ˊ', 'ˋ']], + ['code' => self::DoubleQuote, 'name' => 'DoubleQuote', 'mappings' => ['"', '„', "\u{201C}", "\u{201D}", '¨', '˝', 'ˮ', '˵', '˶', '‟', '"']], + ['code' => self::PercentSign, 'name' => 'PercentSign', 'mappings' => ['%', '‰', '‱']], + ['code' => self::Comma, 'name' => 'Comma', 'mappings' => [',', '¸', '‚', ',', '、', '、']], + ['code' => self::Period, 'name' => 'Period', 'mappings' => ['.', '․', '‥', '…']], + ['code' => self::Slash, 'name' => 'Slash', 'mappings' => ['/', '\\', '⁄', '∕', '⧸', '⫻', '⫽', '⧵']], + ['code' => self::QuestionMark, 'name' => 'QuestionMark', 'mappings' => ['?', '¿']], + ['code' => self::DegreeSign, 'name' => 'DegreeSign', 'mappings' => ['°', '˚', 'º', '¤', '•', '·', '∙', '∘', '⚬', '⦿', '⨀', '⨁', '⨂', "❤️", '🧡', '💛', '💚', '💙', '💜', '🖤', '🤍', '🤎', '❤']], + ]; + return $codes; + } + + /** + * Build a flat map: character → code (for fast lookups). + */ + private static function getMappedCharacters(): array + { + static $map = null; + if ($map !== null) { + return $map; + } + $map = []; + foreach (self::getCharacterCodes() as $entry) { + foreach ($entry['mappings'] as $m) { + $map[$m] = $entry['code']; + } + } + return $map; + } + + public static function getCharacterCode(string $character): ?int + { + $map = self::getMappedCharacters(); + return $map[$character] ?? null; + } + + private static function validateCharacterCode(int $code): int + { + if (in_array($code, self::getValidCodes(), true)) { + return $code; + } + throw new \InvalidArgumentException("Invalid Character Code: {$code}"); + } + + /** + * Port of convertCharactersToCharacterCodes from characterCodes.ts. + * Parses strings like {42} as character codes and maps other chars. + */ + public static function convertCharactersToCharacterCodes(string $characters): array + { + $chars = mb_str_split($characters); + $count = count($chars); + $accumulator = []; + $isCharacterCode = false; + $skipNext = false; + + for ($index = 0; $index < $count; $index++) { + $current = $chars[$index]; + $next = $chars[$index + 1] ?? null; + + $characterCode = null; + if ($isCharacterCode) { + if ($next === '}') { + $characterCode = self::validateCharacterCode((int)$current); + } else { + $twoDigit = $current . ($next ?? ''); + $characterCode = self::validateCharacterCode((int)$twoDigit); + } + } + + if ($current !== '{' && $current !== '}' && !$skipNext) { + $accumulator[] = $isCharacterCode ? $characterCode : self::getCharacterCode($current); + } + + $newIsCharacterCode = ($current === '{'); + $newSkipNext = $isCharacterCode && ($next !== '}'); + + $isCharacterCode = $newIsCharacterCode; + $skipNext = $newSkipNext; + } + + return $accumulator; + } + + /** + * Port of mappingToCharacter from characterCodes.ts. + */ + public static function mappingToCharacter(string $character): string + { + // Remove variation selector-16 (U+FE0F) + if ($character === "\u{FE0F}") { + return ''; + } + + $multipleMapping = MultipleCharacterMappings::getMappings(); + if (isset($multipleMapping[$character])) { + return $multipleMapping[$character]; + } + + $supported = self::getSupportedCharacters(); + if (in_array($character, $supported, true)) { + return $character; + } + + foreach (self::getCharacterCodes() as $entry) { + if (in_array($character, $entry['mappings'], true)) { + return mb_strtolower($entry['mappings'][0]); + } + } + + return ' '; + } + + public static function getSupportedCharacters(): array + { + static $supported = null; + if ($supported !== null) { + return $supported; + } + + $result = []; + foreach (self::getCharacterCodes() as $entry) { + if (!empty($entry['mappings'])) { + $result[] = mb_strtolower($entry['mappings'][0]); + $result[] = mb_strtoupper($entry['mappings'][0]); + } + } + + // Extra explicitly supported characters + $extra = [ + "\n", + "\u{201C}", // left double quotation mark + "\u{2018}", // left single quotation mark + '{', + '}', + '⬜', + '🟥', + '🟧', + '🟨', + '🟩', + '🟦', + '🟪', + '⬛', + '❤', + ]; + + $result = array_merge($result, $extra); + $supported = array_values(array_unique($result)); + return $supported; + } +} diff --git a/php/src/CharacterCodesToAscii.php b/php/src/CharacterCodesToAscii.php new file mode 100644 index 0000000..9e2e27d --- /dev/null +++ b/php/src/CharacterCodesToAscii.php @@ -0,0 +1,106 @@ + $BLANK, + '1' => 'A ', + '2' => 'B ', + '3' => 'C ', + '4' => 'D ', + '5' => 'E ', + '6' => 'F ', + '7' => 'G ', + '8' => 'H ', + '9' => 'I ', + '10' => 'J ', + '11' => 'K ', + '12' => 'L ', + '13' => 'M ', + '14' => 'N ', + '15' => 'O ', + '16' => 'P ', + '17' => 'Q ', + '18' => 'R ', + '19' => 'S ', + '20' => 'T ', + '21' => 'U ', + '22' => 'V ', + '23' => 'W ', + '24' => 'X ', + '25' => 'Y ', + '26' => 'Z ', + '27' => '1 ', + '28' => '2 ', + '29' => '3 ', + '30' => '4 ', + '31' => '5 ', + '32' => '6 ', + '33' => '7 ', + '34' => '8 ', + '35' => '9 ', + '36' => '0 ', + '37' => '! ', + '38' => '@ ', + '39' => '# ', + '40' => '$ ', + '41' => '( ', + '42' => ') ', + '43' => ' ', + '44' => '- ', + '45' => ' ', + '46' => '+ ', + '47' => '& ', + '48' => '= ', + '49' => '; ', + '50' => ': ', + '51' => ' ', + '52' => "' ", + '53' => '" ', + '54' => '% ', + '55' => ', ', + '56' => '. ', + '57' => ' ', + '58' => ' ', + '59' => '/ ', + '60' => '? ', + '61' => ' ', + '62' => '° ', + '63' => $RED, + '64' => $ORANGE, + '65' => $YELLOW, + '66' => $GREEN, + '67' => $BLUE, + '68' => $VIOLET, + '69' => $WHITE, + '70' => $BLACK, + '71' => $FILLED, + ]; + + $rows = array_map(function (array $row) use ($map) { + return implode('', array_map(function (int $code) use ($map) { + return $map[(string)$code] ?? ''; + }, $row)); + }, $characterCodes); + + return implode("\n\n", $rows); + } +} diff --git a/php/src/CharacterCodesToString.php b/php/src/CharacterCodesToString.php new file mode 100644 index 0000000..572c062 --- /dev/null +++ b/php/src/CharacterCodesToString.php @@ -0,0 +1,183 @@ + ' ', + '1' => 'A', + '2' => 'B', + '3' => 'C', + '4' => 'D', + '5' => 'E', + '6' => 'F', + '7' => 'G', + '8' => 'H', + '9' => 'I', + '10' => 'J', + '11' => 'K', + '12' => 'L', + '13' => 'M', + '14' => 'N', + '15' => 'O', + '16' => 'P', + '17' => 'Q', + '18' => 'R', + '19' => 'S', + '20' => 'T', + '21' => 'U', + '22' => 'V', + '23' => 'W', + '24' => 'X', + '25' => 'Y', + '26' => 'Z', + '27' => '1', + '28' => '2', + '29' => '3', + '30' => '4', + '31' => '5', + '32' => '6', + '33' => '7', + '34' => '8', + '35' => '9', + '36' => '0', + '37' => '!', + '38' => '@', + '39' => '#', + '40' => '$', + '41' => '(', + '42' => ')', + '43' => ' ', + '44' => '-', + '45' => '', + '46' => '+', + '47' => '&', + '48' => '=', + '49' => ';', + '50' => ':', + '51' => '', + '52' => "'", + '53' => '"', + '54' => '%', + '55' => ',', + '56' => '.', + '57' => '', + '58' => '', + '59' => '/', + '60' => '?', + '61' => '', + '62' => '°', + '63' => '', + '64' => '', + '65' => '', + '66' => '', + '67' => '', + '68' => '', + '69' => '', + '70' => '', + '71' => ' ', + '100' => "\n", + ]; + + private static function getBreakableCharacters(): array + { + $breakable = []; + foreach (self::$characterCodesMap as $key => $val) { + if ($val === '' || $val === ' ') { + $breakable[] = (int)$key; + } + } + return $breakable; + } + + private static function countEmptyCharactersBeforeFirstWord(array $row): int + { + $breakable = self::getBreakableCharacters(); + $count = 0; + $counting = true; + foreach ($row as $current) { + if (!in_array((int)$current, $breakable, true) || !$counting) { + $counting = false; + } else { + $count++; + } + } + return $count; + } + + private static function countFirstWordLength(array $row): int + { + $breakable = self::getBreakableCharacters(); + $count = 0; + $counting = true; + $startedCounting = false; + foreach ($row as $current) { + if (!$counting) { + break; + } + $isCharacter = !in_array((int)$current, $breakable, true); + if ($isCharacter && !$startedCounting) { + $count++; + $startedCounting = true; + } elseif (!$isCharacter && !$startedCounting) { + // skip + } elseif (!$isCharacter && $startedCounting) { + $counting = false; + } else { + $count++; + } + } + return $count; + } + + public static function convert(array $characters, array $options = []): string + { + $allowLineBreaks = $options['allowLineBreaks'] ?? false; + $mergedRows = []; + + foreach ($characters as $index => $row) { + if ($index === 0) { + $mergedRows = array_merge($mergedRows, [0], $row); + } else { + if ($allowLineBreaks) { + $previousLine = $characters[$index - 1] ?? null; + if ($previousLine === null) { + $mergedRows = array_merge($mergedRows, [0], $row); + continue; + } + + $prefixBreakable = self::countEmptyCharactersBeforeFirstWord($previousLine); + $reversedPrev = array_reverse($previousLine); + $postfixBreakable = self::countEmptyCharactersBeforeFirstWord($reversedPrev); + $firstWordLen = self::countFirstWordLength($row); + $previousBreakable = $prefixBreakable + $postfixBreakable; + + $separator = $previousBreakable > $firstWordLen ? 100 : 0; + $mergedRows = array_merge($mergedRows, [$separator], $row); + } else { + $mergedRows = array_merge($mergedRows, [0], $row); + } + } + } + + $map = self::$characterCodesMap; + $str = implode('', array_map(function ($code) use ($map) { + return $map[(string)$code] ?? ''; + }, $mergedRows)); + + // Remove trailing whitespace + $str = trim($str); + // Remove duplicate whitespace (split by space, filter empty, rejoin) + $parts = explode(' ', $str); + $parts = array_filter($parts, fn($s) => $s !== ''); + $str = implode(' ', $parts); + // Remove whitespace before line breaks + $str = preg_replace('/ \n/', "\n", $str); + + return $str; + } +} diff --git a/php/src/Classic.php b/php/src/Classic.php new file mode 100644 index 0000000..538156c --- /dev/null +++ b/php/src/Classic.php @@ -0,0 +1,309 @@ + 0, + 'A' => 1, 'B' => 2, 'C' => 3, 'D' => 4, 'E' => 5, + 'F' => 6, 'G' => 7, 'H' => 8, 'I' => 9, 'J' => 10, + 'K' => 11, 'L' => 12, 'M' => 13, 'N' => 14, 'O' => 15, + 'P' => 16, 'Q' => 17, 'R' => 18, 'S' => 19, 'T' => 20, + 'U' => 21, 'V' => 22, 'W' => 23, 'X' => 24, 'Y' => 25, + 'Z' => 26, + 'a' => 1, 'b' => 2, 'c' => 3, 'd' => 4, 'e' => 5, + 'f' => 6, 'g' => 7, 'h' => 8, 'i' => 9, 'j' => 10, + 'k' => 11, 'l' => 12, 'm' => 13, 'n' => 14, 'o' => 15, + 'p' => 16, 'q' => 17, 'r' => 18, 's' => 19, 't' => 20, + 'u' => 21, 'v' => 22, 'w' => 23, 'x' => 24, 'y' => 25, + 'z' => 26, + '1' => 27, '2' => 28, '3' => 29, '4' => 30, '5' => 31, + '6' => 32, '7' => 33, '8' => 34, '9' => 35, '0' => 36, + '!' => 37, '@' => 38, '#' => 39, '$' => 40, + '(' => 41, ')' => 42, + '-' => 44, '+' => 46, '&' => 47, '=' => 48, + ';' => 49, ':' => 50, "'" => 52, '"' => 53, + '%' => 54, ',' => 55, '.' => 56, '/' => 59, + '?' => 60, '°' => 62, + '—' => 44, '–' => 44, '¯' => 44, '~' => 44, + '¸' => 55, '¦' => 50, '¿' => 60, + '[' => 41, '{' => 41, ']' => 42, '}' => 42, + '‰' => 54, '¤' => 62, '•' => 62, '·' => 62, + "\u{201C}" => 53, "\u{201D}" => 53, + "\u{2018}" => 52, "\u{2019}" => 52, + '„' => 53, '¨' => 53, "\u{2019}" => 52, + 'ˋ' => 52, 'ˊ' => 52, + '‚' => 52, '`' => 52, '´' => 52, '‟' => 53, + 'â' => 1, 'ä' => 1, 'à' => 1, 'å' => 1, 'á' => 1, + 'À' => 1, 'Á' => 1, 'Â' => 1, 'Ã' => 1, 'Ä' => 1, + 'Å' => 1, 'ã' => 1, + 'ç' => 3, 'Ç' => 3, '¢' => 3, + 'Ð' => 4, + 'é' => 5, 'ê' => 5, 'ë' => 5, 'è' => 5, + 'È' => 5, 'É' => 5, 'Ê' => 5, 'Ë' => 5, + 'ƒ' => 6, + 'í' => 9, 'ï' => 9, 'î' => 9, 'ì' => 9, + 'Ì' => 9, 'Í' => 9, 'Î' => 9, 'Ï' => 9, + '|' => 9, + '£' => 12, + 'ñ' => 14, 'Ñ' => 14, + 'ó' => 15, 'ô' => 15, 'ö' => 15, 'ò' => 15, + 'Ò' => 15, 'Ó' => 15, 'Ô' => 15, 'Õ' => 15, + 'Ö' => 15, 'Ø' => 15, 'ð' => 15, 'õ' => 15, 'ø' => 15, + '±' => 46, + 'š' => 19, 'Š' => 19, '§' => 19, + 'û' => 21, 'ù' => 21, 'ú' => 21, + 'Ù' => 21, 'Ú' => 21, 'Û' => 21, + 'ğ' => 7, + '\\' => 59, + // Character code notation + '{0}' => 0, '{1}' => 1, '{2}' => 2, '{3}' => 3, + '{4}' => 4, '{5}' => 5, '{6}' => 6, '{7}' => 7, + '{8}' => 8, '{9}' => 9, '{10}' => 10, '{11}' => 11, + '{12}' => 12, '{13}' => 13, '{14}' => 14, '{15}' => 15, + '{16}' => 16, '{17}' => 17, '{18}' => 18, '{19}' => 19, + '{20}' => 20, '{21}' => 21, '{22}' => 22, '{23}' => 23, + '{24}' => 24, '{25}' => 25, '{26}' => 26, '{27}' => 27, + '{28}' => 28, '{29}' => 29, '{30}' => 30, '{31}' => 31, + '{32}' => 32, '{33}' => 33, '{34}' => 34, '{35}' => 35, + '{36}' => 36, '{37}' => 37, '{38}' => 38, '{39}' => 39, + '{40}' => 40, '{41}' => 41, '{42}' => 42, '{43}' => 43, + '{44}' => 44, '{45}' => 45, '{46}' => 46, '{47}' => 47, + '{48}' => 48, '{49}' => 49, '{50}' => 50, '{51}' => 51, + '{52}' => 52, '{53}' => 53, '{54}' => 54, '{55}' => 55, + '{56}' => 56, '{57}' => 57, '{58}' => 58, '{59}' => 59, + '{60}' => 60, '{61}' => 61, '{62}' => 62, '{63}' => 63, + '{64}' => 64, '{65}' => 65, '{66}' => 66, '{67}' => 67, + '{68}' => 68, '{69}' => 69, '{70}' => 70, '{71}' => 71, + ]; + return $map; + } + + public static function classic(string $text, array $options = []): array + { + $emptyRow = array_fill(0, self::COLUMN_COUNT, 0); + $extraHPadding = $options['extraHPadding'] ?? 0; + $preserveDoubleSpaces = $options['preserveDoubleSpaces'] ?? false; + + $emptyBoard = array_fill(0, self::ROW_COUNT, $emptyRow); + + if (!$text) { + return $emptyBoard; + } + + $text = EmojisToCharacterCodes::convert($text); + $lines = explode("\n", $text); + + if ($preserveDoubleSpaces) { + $wordCharCodePattern = '/[a-zA-Z]+|\{.\d\}|\{\d\}|\d+| {2}| |[^\p{L}\p{N}_\s]|\p{L}/u'; + } else { + $wordCharCodePattern = '/[a-zA-Z]+|\{.\d\}|\{\d\}|\d+|\s+|[^\p{L}\p{N}_\s]|\p{L}/u'; + } + + $charMap = self::getCharMap(); + + // Convert each line to arrays of char-code words + $vestaboardCharsLines = []; + foreach ($lines as $line) { + preg_match_all($wordCharCodePattern, $line, $matches); + $words = $matches[0] ?? []; + if (empty($words)) { + $vestaboardCharsLines[] = []; + continue; + } + + // flatMap: convert each word token to char codes + $chars = []; + foreach ($words as $word) { + if (strpos($word, '{') !== false && strpos($word, '}') !== false) { + $chars[] = $charMap[$word] ?? 0; + } elseif ($preserveDoubleSpaces && $word === ' ') { + $chars[] = 0; + $chars[] = 0; + } else { + $wordChars = mb_str_split($word); + foreach ($wordChars as $char) { + if ($char === 'ä' || $char === 'Ä') { + $chars[] = $charMap['a'] ?? 1; // A + $chars[] = $charMap['e'] ?? 5; // E + } else { + $chars[] = $charMap[$char] ?? null; + } + } + } + } + + // Group chars into words split by 0 (space) + $wordGroups = []; + $currentWord = []; + for ($i = 0; $i < count($chars); $i++) { + $c = $chars[$i]; + if ($c === 0 && !$preserveDoubleSpaces) { + $wordGroups[] = $currentWord; + $currentWord = []; + } elseif ($c === 0 && $preserveDoubleSpaces) { + // When preserving double spaces, only single 0 is word boundary + $next = $chars[$i + 1] ?? null; + $prev = $chars[$i - 1] ?? null; + if ($next === 0) { + $currentWord[] = $c; + } elseif ($prev === 0) { + $currentWord[] = $c; + } else { + $wordGroups[] = $currentWord; + $currentWord = []; + } + } else { + $currentWord[] = $c; + } + } + $wordGroups[] = $currentWord; + $vestaboardCharsLines[] = $wordGroups; + } + + $contentAreaWidth = self::COLUMN_COUNT - $extraHPadding; + + // makeLines recursive function + $makeLines = null; + $makeLines = function (array $wrappedWord) use ($contentAreaWidth, &$makeLines): array { + // First, chunk each word into pieces <= contentAreaWidth + $words = []; + foreach ($wrappedWord as $word) { + $wordLen = count($word); + if ($wordLen === 0) { + continue; + } + // Chunk into contentAreaWidth-sized pieces + $numChunks = (int)ceil($wordLen / $contentAreaWidth); + for ($chunkIdx = 0; $chunkIdx < $numChunks; $chunkIdx++) { + $start = $chunkIdx * $contentAreaWidth; + $words[] = array_slice($word, $start, $contentAreaWidth); + } + } + + // Check if all words fit in one line + $totalChars = array_sum(array_map('count', $words)); + $numWords = count($words); + if ($numWords === 0) { + return [[]]; + } + if ($totalChars + $numWords - 1 <= $contentAreaWidth) { + return [$words]; + } + + // Find break point + for ($index = 0; $index <= count($words); $index++) { + $sublist = array_slice($words, 0, $index); + $required = array_sum(array_map('count', $sublist)) + count($sublist) - 1; + if ($required > $contentAreaWidth) { + $firstPart = array_slice($words, 0, $index - 1); + $rest = array_slice($words, $index - 1); + return array_merge([$firstPart], $makeLines($rest)); + } + } + return []; + }; + + $wrapping = []; + foreach ($vestaboardCharsLines as $line) { + $lineWrapped = $makeLines($line); + foreach ($lineWrapped as $w) { + $wrapping[] = $w; + } + } + + // Format: join words in each line with [0] space between + $formatted = array_map(function (array $line) { + if (empty($line)) { + return []; + } + // flatMap: [word, [0], word, [0], ...] then slice off last [0] + $result = []; + foreach ($line as $word) { + $result[] = $word; + $result[] = [0]; + } + // Remove last [0] + array_pop($result); + return $result; + }, $wrapping); + + $numContentRows = count($formatted); + + // Special case: 3 rows with no extra padding -> re-run with extraHPadding+4 + if ($numContentRows === 3 && $extraHPadding === 0) { + return self::classic($text, [ + 'extraHPadding' => $extraHPadding + 4, + 'preserveDoubleSpaces' => $preserveDoubleSpaces, + ]); + } + + // Calculate max content columns + $maxNumContentColumns = 0; + foreach ($formatted as $line) { + $lineLen = array_sum(array_map('count', $line)); + if ($lineLen > $maxNumContentColumns) { + $maxNumContentColumns = $lineLen; + } + } + + $hPad = max((int)floor((self::COLUMN_COUNT - ($maxNumContentColumns + 1)) / 2), 0); + $vPad = max((int)floor((self::ROW_COUNT - $numContentRows) / 2), 0); + + $emptyRowPaddings = array_fill(0, $vPad, $emptyRow); + $hPaddings = array_fill(0, $hPad, [0]); + + $padded = array_merge( + $emptyRowPaddings, + array_map(function ($line) use ($hPaddings) { + return array_merge($hPaddings, $line, $hPaddings); + }, $formatted), + $emptyRowPaddings + ); + + // Build codes: flatten each padded line, take first COLUMN_COUNT chars + $codes = []; + $paddedSlice = array_slice($padded, 0, self::ROW_COUNT); + foreach ($paddedSlice as $line) { + // Flatten: line is an array of word-arrays + $flat = []; + foreach ($line as $wordOrZero) { + if (is_array($wordOrZero)) { + foreach ($wordOrZero as $c) { + $flat[] = $c; + } + } else { + $flat[] = $wordOrZero; + } + } + $codes[] = array_slice($flat, 0, self::COLUMN_COUNT); + } + + // Build finalBoard + $finalBoard = []; + foreach ($emptyBoard as $rowIndex => $line) { + $finalRow = []; + foreach ($line as $colIndex => $_) { + $finalRow[] = $codes[$rowIndex][$colIndex] ?? 0; + } + $finalBoard[] = $finalRow; + } + + return $finalBoard; + } +} diff --git a/php/src/CopyCharacterCodes.php b/php/src/CopyCharacterCodes.php new file mode 100644 index 0000000..cecf209 --- /dev/null +++ b/php/src/CopyCharacterCodes.php @@ -0,0 +1,14 @@ + array_values($row), $characters); + } +} diff --git a/php/src/CreateEmptyBoard.php b/php/src/CreateEmptyBoard.php new file mode 100644 index 0000000..ec88322 --- /dev/null +++ b/php/src/CreateEmptyBoard.php @@ -0,0 +1,18 @@ + '{63}', + '🟧' => '{64}', + '🟨' => '{65}', + '🟩' => '{66}', + '🟦' => '{67}', + '🟪' => '{68}', + '⬜' => '{69}', + '⬛' => '{70}', + 'ß' => 'SS', + ]; + foreach ($replacements as $emoji => $code) { + $template = str_replace($emoji, $code, $template); + } + return $template; + } +} diff --git a/php/src/GetLinesFromWords.php b/php/src/GetLinesFromWords.php new file mode 100644 index 0000000..37625e4 --- /dev/null +++ b/php/src/GetLinesFromWords.php @@ -0,0 +1,70 @@ + string, 'length' => int] + $acc = [['line' => '', 'length' => 0]]; + $count = count($words); + + for ($index = 0; $index < $count; $index++) { + $curr = $words[$index]; + $lastIndex = count($acc) - 1; + $lineLength = $acc[$lastIndex]['length']; + $emptyLine = ($acc[$lastIndex]['line'] === ''); + + // Allow for empty lines if there is a double return + if ($curr === "\n" && isset($words[$index - 1]) && $words[$index - 1] === "\n") { + $acc[] = ['line' => '', 'length' => 0]; + continue; + } + + if ($curr === "\n") { + // Finish the line + $acc[$lastIndex]['length'] = $width; + // keep acc as is (line is finalized at full width) + continue; + } + + // Colors / character codes - treat as one bit wide + $isColorCode = (mb_strlen($curr) >= 2 && mb_substr($curr, 0, 1) === '{' && mb_substr($curr, -1) === '}'); + if ($isColorCode) { + if (1 + $lineLength > $width) { + // If a blank space is forced at beginning of a new line, ignore it + if ($curr === '{0}') { + continue; + } + $acc[] = ['line' => $curr, 'length' => 1]; + } else { + $acc[$lastIndex]['line'] .= $curr; + $acc[$lastIndex]['length'] = $lineLength + 1; + } + continue; + } + + // Regular word + $wordLen = mb_strlen($curr); + if ($width >= $wordLen + $lineLength && !$emptyLine) { + // Word fits on current line + $acc[$lastIndex]['line'] .= $curr; + $acc[$lastIndex]['length'] = $lineLength + $wordLen; + } else { + // New line + $acc[] = ['line' => $curr, 'length' => $wordLen]; + } + } + + $firstLineEmpty = ($acc[0]['line'] === ''); + if ($firstLineEmpty) { + $acc = array_slice($acc, 1); + } + + return array_map(fn($item) => $item['line'], $acc); + } +} diff --git a/php/src/HasSpecialCharacters.php b/php/src/HasSpecialCharacters.php new file mode 100644 index 0000000..a87e64f --- /dev/null +++ b/php/src/HasSpecialCharacters.php @@ -0,0 +1,24 @@ + self::removeExtraSpace($row)['row'], $codes); + $longestRow = 0; + foreach ($rows as $row) { + if (count($row) > $longestRow) { + $longestRow = count($row); + } + } + $longestRow = $longestRow - 1; + $paddingRight = (int)floor(($width - $longestRow) / 2); + $paddingLeft = $width - ($longestRow + ($paddingRight + 1)); + $padding = $paddingRight > $paddingLeft ? $paddingLeft : $paddingRight; + + return array_map(function (array $row) use ($width, $padding) { + $result = array_fill(0, $width, CharacterCode::Blank); + for ($i = 0; $i < $width; $i++) { + $srcIndex = $i - $padding; + $result[$i] = $row[$srcIndex] ?? CharacterCode::Blank; + } + return $result; + }, $rows); + + default: // center + return array_map(function (array $row) use ($width) { + $reversed = array_reverse($row); + $trimmedReversed = self::removeExtraSpace($reversed)['row']; + $trimmed = array_reverse($trimmedReversed); + $paddingLeft = (int)floor(($width - count($trimmed)) / 2); + $result = array_fill(0, $width, CharacterCode::Blank); + for ($i = 0; $i < $width; $i++) { + $srcIndex = $i - $paddingLeft; + $result[$i] = $trimmed[$srcIndex] ?? CharacterCode::Blank; + } + return $result; + }, $codes); + } + } + + private static function removeExtraSpace(array $row): array + { + $resultRow = []; + $extraSpace = true; + foreach ($row as $cur) { + if ($cur === CharacterCode::Blank && $extraSpace) { + // skip leading blank + } else { + $resultRow[] = $cur; + $extraSpace = false; + } + } + return ['row' => $resultRow, 'extraSpace' => $extraSpace]; + } +} diff --git a/php/src/LayoutComponents.php b/php/src/LayoutComponents.php new file mode 100644 index 0000000..7857cbd --- /dev/null +++ b/php/src/LayoutComponents.php @@ -0,0 +1,81 @@ + 0, 'left' => 0, 'height' => 0]; + + foreach ($components as $component) { + if (empty($component)) { + continue; + } + $componentWidth = count($component[0]); + $boardWidth = count($board[0]); + + $newLine = $position['left'] + $componentWidth > $boardWidth; + $left = $newLine ? 0 : $position['left']; + $top = $newLine ? $position['top'] + $position['height'] : $position['top']; + + foreach ($component as $rowIndex => $row) { + foreach ($row as $bitIndex => $bit) { + if (isset($board[$rowIndex + $top][$bitIndex + $left])) { + $board[$rowIndex + $top][$bitIndex + $left] = $bit; + } + } + } + + $position = [ + 'top' => $top, + 'left' => $left + $componentWidth, + 'height' => count($component), + ]; + } + + if ($absoluteComponents) { + foreach ($absoluteComponents as $component) { + if (empty($component)) continue; + foreach ($component['characters'] as $rowIndex => $row) { + foreach ($row as $bitIndex => $bit) { + if ($component['y'] + $rowIndex >= count($board)) { + continue; + } + if ($component['x'] + $bitIndex >= count($board[0])) { + continue; + } + $board[$rowIndex + $component['y']][$bitIndex + $component['x']] = $bit; + } + } + } + } + + if ($calendarComponents) { + foreach ($calendarComponents as $component) { + if (empty($component)) continue; + foreach ($component['characters'] as $rowIndex => $row) { + foreach ($row as $bitIndex => $bit) { + if ($rowIndex >= count($board)) { + continue; + } + if ($component['x'] + $bitIndex >= count($board[0]) || $bitIndex > 12) { + // match TS: return board[rowIndex][bitIndex + component.x] (no assignment) + continue; + } + $board[$rowIndex][$bitIndex + $component['x']] = $bit; + } + } + } + } + + return $board; + } +} diff --git a/php/src/MultipleCharacterMappings.php b/php/src/MultipleCharacterMappings.php new file mode 100644 index 0000000..a90bd6e --- /dev/null +++ b/php/src/MultipleCharacterMappings.php @@ -0,0 +1,41 @@ + '1/2', + '¼' => '1/4', + '¾' => '3/4', + '⅓' => '1/3', + '⅔' => '2/3', + '⅛' => '1/8', + '⅜' => '3/8', + '⅝' => '5/8', + '⅞' => '7/8', + '⅐' => '1/7', + '⅑' => '1/9', + '⅒' => '1/10', + 'æ' => 'AE', + 'Æ' => 'AE', + 'ß' => 'SS', + 'œ' => 'OE', + 'Œ' => 'OE', + '™' => 'TM', + 'ü' => 'UE', + 'Ä' => 'AE', + 'ä' => 'AE', + 'Ö' => 'OE', + 'ö' => 'OE', + 'Ü' => 'UE', + 'ẞ' => 'SS', + '…' => '...', + ]; + } +} diff --git a/php/src/ParseComponent.php b/php/src/ParseComponent.php new file mode 100644 index 0000000..53cc652 --- /dev/null +++ b/php/src/ParseComponent.php @@ -0,0 +1,69 @@ + $characters, + 'x' => $component['style']['absolutePosition']['x'] ?? 0, + 'y' => $component['style']['absolutePosition']['y'] ?? 0, + ]; + }; + } +} diff --git a/php/src/ParseProps.php b/php/src/ParseProps.php new file mode 100644 index 0000000..1c16829 --- /dev/null +++ b/php/src/ParseProps.php @@ -0,0 +1,17 @@ +render($template, $props); + } +} diff --git a/php/src/RandomColors.php b/php/src/RandomColors.php new file mode 100644 index 0000000..638cbac --- /dev/null +++ b/php/src/RandomColors.php @@ -0,0 +1,30 @@ + 0 && + mb_substr($curr, 0, 1) === '{' && + mb_substr($curr, -1) === '}' + ) { + $chunked[] = $curr; + continue; + } + if (mb_strlen($curr) > $width) { + $pieces = self::chunkString($curr, $width); + foreach ($pieces as $piece) { + $chunked[] = $piece; + } + } else { + $chunked[] = $curr; + } + } + + // Filter empty strings + return array_values(array_filter($chunked, fn($p) => $p !== '')); + } + + private static function chunkString(string $word, int $width): array + { + $result = []; + $len = mb_strlen($word); + $offset = 0; + while ($offset < $len) { + $result[] = mb_substr($word, $offset, $width); + $offset += $width; + } + return $result; + } +} diff --git a/php/src/Vbml.php b/php/src/Vbml.php new file mode 100644 index 0000000..4aac26f --- /dev/null +++ b/php/src/Vbml.php @@ -0,0 +1,94 @@ + !isset($c['style']['absolutePosition']) && !isset($c['calendar']) + )); + $components = array_map( + ParseComponent::parseComponent($height, $width, $props), + $components + ); + + // Absolute components (non-calendar) + $absoluteComponents = array_values(array_filter( + $input['components'], + fn($c) => !isset($c['calendar']) && isset($c['style']['absolutePosition']) + )); + $absoluteComponents = array_map( + ParseComponent::parseAbsoluteComponent($height, $width, $props), + $absoluteComponents + ); + + // Calendar components + $calendarComponents = []; + foreach ($input['components'] as $component) { + if (!isset($component['calendar'])) { + continue; + } + $cal = $component['calendar']; + $x = $component['style']['absolutePosition']['x'] ?? 0; + $calendar = Calendar::makeCalendar( + $cal['month'], + $cal['year'], + $cal['days'] ?? [], + $cal['defaultDayColor'] ?? null, + $cal['hideSMTWTFS'] ?? false, + $cal['hideDates'] ?? false, + $cal['hideMonthYear'] ?? false + ); + $calendarComponents[] = Calendar::parseCalendarComponent($calendar, $x); + } + + return LayoutComponents::layout($emptyBoard, $components, $absoluteComponents, $calendarComponents); + } + + public static function characterCodesToString(array $characters, array $options = []): string + { + return CharacterCodesToString::convert($characters, $options); + } + + public static function characterCodesToAscii(array $characterCodes, bool $isWhite = false): string + { + return CharacterCodesToAscii::convert($characterCodes, $isWhite); + } + + public static function copyCharacterCodes(array $characters): array + { + return CopyCharacterCodes::copy($characters); + } + + public static function classic(string $text, array $options = []): array + { + return Classic::classic($text, $options); + } + + public static function hasSpecialCharacters(?string $text): bool + { + return HasSpecialCharacters::check($text); + } + + public static function sanitizeSpecialCharacters(string $text): string + { + return SanitizeSpecialCharacters::sanitize($text); + } +} diff --git a/php/src/VerticalAlign.php b/php/src/VerticalAlign.php new file mode 100644 index 0000000..8ab9571 --- /dev/null +++ b/php/src/VerticalAlign.php @@ -0,0 +1,50 @@ +assertEquals('🟥🟧🟨🟩🟦🟪⬜⬛', $result); + } + + public function testShouldHandleRows(): void + { + $result = CharacterCodesToAscii::convert([[63, 64], [63, 64]]); + $this->assertEquals("🟥🟧\n\n🟥🟧", $result); + } + + public function testShouldSpaceOutLetters(): void + { + $result = CharacterCodesToAscii::convert([[1, 2]]); + $this->assertEquals('A B ', $result); + } +} diff --git a/php/tests/CharacterCodesToStringTest.php b/php/tests/CharacterCodesToStringTest.php new file mode 100644 index 0000000..d03addd --- /dev/null +++ b/php/tests/CharacterCodesToStringTest.php @@ -0,0 +1,55 @@ +assertEquals('AB', $result); + } + + public function testShouldConvertTwoLineSentence(): void + { + $result = CharacterCodesToString::convert([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [20, 8, 9, 19, 0, 9, 19, 0, 1, 0, 12, 15, 14, 7, 5, 18, 0, 2, 12, 15, 3, 11], + [20, 8, 1, 20, 0, 19, 16, 1, 14, 19, 0, 28, 0, 12, 9, 14, 5, 19, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]); + $this->assertEquals('THIS IS A LONGER BLOCK THAT SPANS 2 LINES', $result); + } + + public function testShouldHandleBreaks(): void + { + $result = CharacterCodesToString::convert([ + [0, 0, 8, 1, 14, 4, 12, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 2, 18, 5, 1, 11, 19, 0, 7, 18, 1, 3, 5, 6, 21, 12, 12, 25, 0, 0, 0], + ]); + $this->assertEquals('HANDLE BREAKS GRACEFULLY', $result); + } + + public function testShouldHandleLineBreaks(): void + { + $result = CharacterCodesToString::convert( + [[1, 2, 0, 0, 0], [3, 4, 0, 0, 0]], + ['allowLineBreaks' => true] + ); + $this->assertEquals("AB\nCD", $result); + } + + public function testShouldAssumeNoLineBreakIfFirstWordFits(): void + { + $result = CharacterCodesToString::convert( + [[1, 0], [2, 0]], + ['allowLineBreaks' => true] + ); + $this->assertEquals('A B', $result); + } +} diff --git a/php/tests/ClassicTest.php b/php/tests/ClassicTest.php new file mode 100644 index 0000000..aff3bb3 --- /dev/null +++ b/php/tests/ClassicTest.php @@ -0,0 +1,189 @@ +assertEquals($mockBoard, Classic::classic('Hello, World!')); + } + + public function testShouldConvertEmbeddedCharCodeString(): void + { + $mockBoard = [ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 8, 5, 12, 12, 15, 55, 0, 23, 15, 18, 12, 4, 37, 0, 29, 33, 0, 0, 0, 0, 0], + [0, 65, 32, 32, 65, 0, 34, 35, 65, 0, 65, 28, 36, 0, 8, 5, 12, 12, 15, 0, 0, 0], + [0, 23, 15, 18, 12, 4, 0, 23, 8, 1, 20, 19, 21, 16, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ]; + $string = 'Hello, World{37} 37 {65}66{65} 89{65} {65}20 hello world whatsup'; + $this->assertEquals($mockBoard, Classic::classic($string, ['extraHPadding' => 0])); + } + + public function testShouldConvertLongerString(): void + { + $string = 'reallylongwordthatismorethantwentytwocharcters'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 18, 5, 1, 12, 12, 25, 12, 15, 14, 7, 23, 15, 18, 4, 20, 8, 1, 20, 0, 0, 0], + [0, 9, 19, 13, 15, 18, 5, 20, 8, 1, 14, 20, 23, 5, 14, 20, 25, 20, 23, 0, 0, 0], + [0, 15, 3, 8, 1, 18, 3, 20, 5, 18, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertAeToClassicBoard(): void + { + $string = 'äÄ'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 1, 5, 1, 5, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertLongStringWithDigits(): void + { + $string = 'reallylongwordthatismorethan22charcters'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [18, 5, 1, 12, 12, 25, 12, 15, 14, 7, 23, 15, 18, 4, 20, 8, 1, 20, 9, 19, 13, 15], + [18, 5, 20, 8, 1, 14, 28, 28, 3, 8, 1, 18, 3, 20, 5, 18, 19, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertSingleNewlineString(): void + { + $string = "hello\n world"; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 8, 5, 12, 12, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 23, 15, 18, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertDoubleNewlineString(): void + { + $string = "hello\n\nworld"; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 8, 5, 12, 12, 15, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 23, 15, 18, 12, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertEmptyString(): void + { + $string = ''; + $emptyRow = array_fill(0, 22, 0); + $this->assertEquals( + array_fill(0, 6, $emptyRow), + Classic::classic($string) + ); + } + + public function testShouldConvertCharCode1(): void + { + $string = '{1}'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertHyphenString(): void + { + $string = '- -hyphen'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 44, 0, 44, 8, 25, 16, 8, 5, 14, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertSpecialCharacterStrings(): void + { + $string = '!@#$%^&*()_+åß∂ƒ©˙∆˚¬µ√ç∫˜µ≤≥÷{}'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [37, 38, 39, 40, 54, 0, 47, 0, 41, 42, 46, 1, 19, 19, 0, 6, 0, 0, 0, 0, 0, 0], + [0, 3, 0, 0, 0, 0, 0, 0, 41, 42, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldConvertEmojiColors(): void + { + $string = '🟥🟧🟨🟩🟦🟪⬜⬛'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 63, 64, 65, 66, 67, 68, 69, 70, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string)); + } + + public function testShouldRespectDoubleSpaces(): void + { + $string = 'hello world'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 8, 5, 12, 12, 15, 0, 0, 23, 15, 18, 12, 4, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string, ['preserveDoubleSpaces' => true])); + } + + public function testShouldPreserveTripleSpaces(): void + { + $string = 'hello world'; + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 8, 5, 12, 12, 15, 0, 0, 0, 23, 15, 18, 12, 4, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], Classic::classic($string, ['preserveDoubleSpaces' => true])); + } +} diff --git a/php/tests/CopyCharacterCodesTest.php b/php/tests/CopyCharacterCodesTest.php new file mode 100644 index 0000000..ada883b --- /dev/null +++ b/php/tests/CopyCharacterCodesTest.php @@ -0,0 +1,20 @@ +assertEquals([[1, 2]], $result); + + // Verify deep copy: modifying original does not affect the copy + $characters[0][0] = 3; + $this->assertEquals(1, $result[0][0]); + } +} diff --git a/php/tests/HasSpecialCharactersTest.php b/php/tests/HasSpecialCharactersTest.php new file mode 100644 index 0000000..1fb6f38 --- /dev/null +++ b/php/tests/HasSpecialCharactersTest.php @@ -0,0 +1,79 @@ +assertTrue(HasSpecialCharacters::check('ä')); + } + + public function testShouldReturnTrueIfMixed(): void + { + $this->assertTrue(HasSpecialCharacters::check('äa')); + } + + public function testShouldReturnFalseForLowercase(): void + { + $this->assertFalse(HasSpecialCharacters::check('abcdefghijklmnopqrstuvwxyz')); + } + + public function testShouldReturnFalseForUppercase(): void + { + $this->assertFalse(HasSpecialCharacters::check('ABCDEFGHIJKLMNOPQRSTUVWXYZ')); + } + + public function testShouldReturnFalseForNumbers(): void + { + $this->assertFalse(HasSpecialCharacters::check('0123456789')); + } + + public function testShouldReturnFalseForStandardSymbols(): void + { + $this->assertFalse(HasSpecialCharacters::check("!@#\$()-+&=;:'\"%,./?°")); + } + + public function testShouldReturnFalseForEmpty(): void + { + $this->assertFalse(HasSpecialCharacters::check('')); + } + + public function testShouldExcludeNewlines(): void + { + $this->assertFalse(HasSpecialCharacters::check("Hello\nWorld")); + } + + public function testShouldExcludeSingleQuoteFromIOS(): void + { + $this->assertFalse(HasSpecialCharacters::check("\u{2018}")); + } + + public function testShouldExcludeDoubleQuoteFromIOS(): void + { + $this->assertFalse(HasSpecialCharacters::check("\u{201C}")); + } + + public function testShouldExcludeWhiteColorSwatch(): void + { + $this->assertFalse(HasSpecialCharacters::check('⬜')); + } + + public function testShouldExcludeBlackColorSwatch(): void + { + $this->assertFalse(HasSpecialCharacters::check('⬛')); + } + + public function testShouldExcludeOrangeColorSwatch(): void + { + $this->assertFalse(HasSpecialCharacters::check('🟧')); + } + + public function testShouldIncludeFractions(): void + { + $this->assertTrue(HasSpecialCharacters::check('½')); + } +} diff --git a/php/tests/SanitizeSpecialCharactersTest.php b/php/tests/SanitizeSpecialCharactersTest.php new file mode 100644 index 0000000..23b9ab9 --- /dev/null +++ b/php/tests/SanitizeSpecialCharactersTest.php @@ -0,0 +1,151 @@ +assertEquals($text, SanitizeSpecialCharacters::sanitize($text)); + } + + public function testShouldReplaceSpecialCharacters(): void + { + $this->assertEquals('a', SanitizeSpecialCharacters::sanitize('Ã')); + } + + public function testShouldHandleSentence(): void + { + $this->assertEquals('hello world', SanitizeSpecialCharacters::sanitize('hello world')); + } + + public function testShouldHandleMixedSpecialCharacters(): void + { + $this->assertEquals('hello world', SanitizeSpecialCharacters::sanitize('héllo wôrld')); + } + + public function testShouldHandleMultipleSpecialCharactersTogether(): void + { + $this->assertEquals('ei', SanitizeSpecialCharacters::sanitize('ëï')); + } + + public function testShouldReplaceFractionsWithMultipleCharacters(): void + { + $this->assertEquals('1/2', SanitizeSpecialCharacters::sanitize('½')); + } + + public function testShouldSanitizeVariationSelectorFromHeartEmoji(): void + { + $text = "❤️"; // U+2764 U+FE0F + $this->assertEquals('❤', SanitizeSpecialCharacters::sanitize($text)); + } + + public function testShouldSanitizeVariationSelectorFromLiteral(): void + { + $text = "\u{2764}\u{FE0F}"; + $this->assertEquals("\u{2764}", SanitizeSpecialCharacters::sanitize($text)); + } + + public function testShouldNotReplaceVestaboardHeart(): void + { + $text = "\u{2764}"; + $this->assertEquals('❤', SanitizeSpecialCharacters::sanitize($text)); + } + + public function testShouldAcceptWhitespaceAfterHeart(): void + { + $text = "\u{2764} "; + $this->assertEquals($text, SanitizeSpecialCharacters::sanitize($text)); + } + + public function testShouldNotClearWhitespaceBetweenHearts(): void + { + $testString = "❤ ❤ ❤ ❤ ❤"; + $this->assertEquals($testString, SanitizeSpecialCharacters::sanitize($testString)); + } + + public function testShouldNotTrimWhitespaceWhenHeartFollowedByLatin(): void + { + $testString = "\u{2764} A"; + $this->assertEquals($testString, SanitizeSpecialCharacters::sanitize($testString)); + } + + public function testShouldNotTrimWhitespaceWhenHeartFollowedByEmoji(): void + { + $testString = "\u{2764} 🟧"; + $this->assertEquals($testString, SanitizeSpecialCharacters::sanitize($testString)); + } + + public function testShouldConvertUnsupportedEmojisToWhitespace(): void + { + $testString = "☠️⚠️✅▶️✨⌛️"; + $equivalentWhitespace = " "; // 6 spaces + $this->assertEquals($equivalentWhitespace, SanitizeSpecialCharacters::sanitize($testString)); + } + + public function testShouldHandleHeartEmojiAndUnsupportedEmojis(): void + { + $testString = "❤️☠️⚠️✅▶️✨⌛️"; + $expectation = "\u{2764} "; // U+2764 + 6 spaces + $this->assertEquals($expectation, SanitizeSpecialCharacters::sanitize($testString)); + } + + public function testShouldSanitizeGermanAndSpecialCharacters(): void + { + $this->assertEquals('AE', SanitizeSpecialCharacters::sanitize('ä')); + $this->assertEquals('AE', SanitizeSpecialCharacters::sanitize('Ä')); + $this->assertEquals('OE', SanitizeSpecialCharacters::sanitize('ö')); + $this->assertEquals('OE', SanitizeSpecialCharacters::sanitize('Ö')); + $this->assertEquals('UE', SanitizeSpecialCharacters::sanitize('ü')); + $this->assertEquals('UE', SanitizeSpecialCharacters::sanitize('Ü')); + $this->assertEquals('SS', SanitizeSpecialCharacters::sanitize('ß')); + + $this->assertEquals('o', SanitizeSpecialCharacters::sanitize('ø')); + $this->assertEquals('a', SanitizeSpecialCharacters::sanitize('å')); + + $this->assertEquals('OE', SanitizeSpecialCharacters::sanitize('œ')); + $this->assertEquals('AE', SanitizeSpecialCharacters::sanitize('æ')); + + $this->assertEquals('c', SanitizeSpecialCharacters::sanitize('ç')); + $this->assertEquals('f', SanitizeSpecialCharacters::sanitize('ƒ')); + $this->assertEquals(' ', SanitizeSpecialCharacters::sanitize('µ')); + + $this->assertEquals('...', SanitizeSpecialCharacters::sanitize('…')); + $this->assertEquals('-', SanitizeSpecialCharacters::sanitize('–')); + $this->assertEquals('/', SanitizeSpecialCharacters::sanitize('⁄')); + + $allChars = "äÄöÖüÜßøåœæçƒµ…–⁄∑¡¶¢[]|{}≠¿€®†¨π•±∂©º∆@¥≈√∫~∞"; + $result = SanitizeSpecialCharacters::sanitize($allChars); + $this->assertNotEmpty($result); + $this->assertIsString($result); + $this->assertDoesNotMatchRegularExpression('/[äÄöÖüÜßøåœæçƒµ]/u', $result); + } + + public function testShouldHandleGermanTextWithUmlauts(): void + { + $germanText = 'Über die Brücke gehen wir für Österreich'; + $result = SanitizeSpecialCharacters::sanitize($germanText); + $this->assertEquals('UEber die BrUEcke gehen wir fUEr OEsterreich', $result); + } + + public function testShouldHandleGermanSharpS(): void + { + $germanText = 'Straße'; + $result = SanitizeSpecialCharacters::sanitize($germanText); + $this->assertEquals('StraSSe', $result); + } + + public function testShouldConvertScharfesToSS(): void + { + $texts = ['ß', 'Straße', 'fußball', 'groß', 'weiß']; + $expected = ['SS', 'StraSSe', 'fuSSball', 'groSS', 'weiSS']; + + foreach ($texts as $i => $text) { + $this->assertEquals($expected[$i], SanitizeSpecialCharacters::sanitize($text)); + } + } +} diff --git a/php/tests/VbmlTest.php b/php/tests/VbmlTest.php new file mode 100644 index 0000000..bfb199e --- /dev/null +++ b/php/tests/VbmlTest.php @@ -0,0 +1,329 @@ + ['height' => 1, 'width' => 2], + 'components' => [['template' => 'hi']], + ]); + $this->assertEquals([[8, 9]], $result); + } + + public function testShouldLayoutComponentsSideBySide(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 1, 'width' => 4], + 'components' => [ + ['template' => 'hi', 'style' => ['width' => 2, 'height' => 1]], + ['template' => 'hi', 'style' => ['width' => 2, 'height' => 1]], + ], + ]); + $this->assertEquals([[8, 9, 8, 9]], $result); + } + + public function testShouldFormatAeAe(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 1, 'width' => 4], + 'components' => [ + ['template' => 'äÄ', 'style' => ['width' => 4, 'height' => 1]], + ], + ]); + $this->assertEquals([[1, 5, 1, 5]], $result); + } + + public function testShouldLayoutComponentsVertically(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 2, 'width' => 2], + 'components' => [ + ['template' => 'hi', 'style' => ['width' => 2, 'height' => 1]], + ['template' => 'hi', 'style' => ['width' => 2, 'height' => 1]], + ], + ]); + $this->assertEquals([[8, 9], [8, 9]], $result); + } + + public function testShouldFlowThirdComponentToNextLine(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 2, 'width' => 4], + 'components' => [ + ['template' => '{1}{2}', 'style' => ['width' => 2, 'height' => 1]], + ['template' => '{3}{4}', 'style' => ['width' => 2, 'height' => 1]], + ['template' => '{5}{6}', 'style' => ['width' => 2, 'height' => 1]], + ], + ]); + $this->assertEquals([[1, 2, 3, 4], [5, 6, 0, 0]], $result); + } + + public function testShouldJustifyContentVertically(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 5, 'width' => 1], + 'components' => [ + ['template' => 'abcd', 'style' => ['height' => 5, 'width' => 1, 'align' => 'justified']], + ], + ]); + $this->assertEquals([[0], [1], [2], [3], [4]], $result); + } + + public function testShouldJustifyWithThreeCharsAndRows(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 5, 'width' => 1], + 'components' => [ + ['template' => 'abc', 'style' => ['height' => 5, 'width' => 1, 'align' => 'justified']], + ], + ]); + $this->assertEquals([[0], [1], [2], [3], [0]], $result); + } + + public function testShouldLayoutAbsoluteByRelative(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 22, 'width' => 6], + 'components' => [ + ['template' => 'abc', 'style' => ['height' => 6, 'width' => 22, 'align' => 'top', 'justify' => 'left']], + ['template' => 'def', 'style' => ['height' => 1, 'width' => 3, 'align' => 'top', 'justify' => 'left', 'absolutePosition' => ['x' => 3, 'y' => 0]]], + ], + ]); + $expected = [1, 2, 3, 4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + $this->assertEquals($expected, $result[0]); + } + + public function testShouldLayoutAbsoluteOverRelative(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 22, 'width' => 6], + 'components' => [ + ['template' => 'abc', 'style' => ['height' => 6, 'width' => 22, 'align' => 'top', 'justify' => 'left']], + ['template' => 'def', 'style' => ['height' => 1, 'width' => 3, 'align' => 'top', 'justify' => 'left', 'absolutePosition' => ['x' => 0, 'y' => 0]]], + ], + ]); + $expected = [4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + $this->assertEquals($expected, $result[0]); + } + + public function testShouldLayoutAbsoluteOverRelative2(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + ['template' => 'abc', 'style' => ['height' => 6, 'width' => 22, 'align' => 'top', 'justify' => 'left']], + ['template' => 'def', 'style' => ['height' => 1, 'width' => 3, 'align' => 'top', 'justify' => 'left', 'absolutePosition' => ['x' => 0, 'y' => 0]]], + ], + ]); + $expected = [4, 5, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + $this->assertEquals($expected, $result[0]); + } + + public function testShouldLayoutRawComponents(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + ['rawCharacters' => [[1, 2, 3]]], + ], + ]); + $expected = [1, 2, 3, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]; + $this->assertEquals($expected, $result[0]); + } + + public function testShouldLayoutAbsoluteWithRawComponentsForClock(): void + { + $result = Vbml::parse([ + 'props' => ['time' => '12:00 PM'], + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + ['rawCharacters' => [ + [68, 68, 68, 68, 68, 69, 69, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68], + [68, 68, 68, 68, 69, 69, 69, 69, 68, 68, 68, 68, 68, 68, 68, 68, 65, 65, 65, 65, 68, 68], + [63, 63, 63, 69, 66, 69, 66, 69, 69, 63, 63, 63, 63, 63, 63, 65, 65, 65, 65, 65, 65, 63], + [63, 63, 66, 66, 66, 69, 66, 66, 66, 66, 63, 63, 63, 63, 63, 65, 65, 65, 65, 65, 65, 63], + [64, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 64, 64, 64, 64, 65, 65, 65, 65, 64, 64], + [66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64], + ]], + ['template' => '{{time}}', 'style' => ['height' => 1, 'width' => 8, 'absolutePosition' => ['x' => 11, 'y' => 3]]], + ], + ]); + $this->assertEquals([ + [68, 68, 68, 68, 68, 69, 69, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68, 68], + [68, 68, 68, 68, 69, 69, 69, 69, 68, 68, 68, 68, 68, 68, 68, 68, 65, 65, 65, 65, 68, 68], + [63, 63, 63, 69, 66, 69, 66, 69, 69, 63, 63, 63, 63, 63, 63, 65, 65, 65, 65, 65, 65, 63], + [63, 63, 66, 66, 66, 69, 66, 66, 66, 66, 63, 27, 28, 50, 36, 36, 0, 16, 13, 65, 65, 63], + [64, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 64, 64, 64, 64, 65, 65, 65, 65, 64, 64], + [66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 66, 64, 64, 64, 64, 64, 64, 64, 64, 64, 64], + ], $result); + } + + public function testShouldLayoutCalendarForChristmas(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + [ + 'calendar' => [ + 'defaultDayColor' => 66, + 'month' => '12', + 'year' => '2024', + 'days' => ['25' => 63], + ], + 'style' => ['absolutePosition' => ['x' => 0, 'y' => 0]], + ], + ], + ]); + $this->assertEquals([ + [27, 28, 59, 28, 30, 19, 13, 20, 23, 20, 6, 19, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 27, 44, 33, 0, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 34, 44, 27, 30, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [27, 31, 44, 28, 27, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [28, 28, 44, 28, 34, 66, 66, 66, 63, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [28, 35, 44, 29, 27, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], $result); + + foreach ($result as $row) { + $this->assertCount(22, $row); + } + } + + public function testShouldLayoutMinimalistCalendar(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + [ + 'style' => ['absolutePosition' => ['x' => 0, 'y' => 0]], + 'calendar' => [ + 'defaultDayColor' => 66, + 'hideDates' => true, + 'hideMonthYear' => true, + 'hideSMTWTFS' => true, + 'month' => '12', + 'year' => '2024', + 'days' => ['25' => 63], + ], + ], + ], + ]); + $this->assertEquals([ + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 66, 66, 66, 66, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 66, 66, 66, 63, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [0, 0, 0, 0, 0, 66, 66, 66, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], $result); + + foreach ($result as $row) { + $this->assertCount(22, $row); + } + } + + public function testShouldLayoutCalendarWithOtherComponents(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + [ + 'template' => 'December 2024 Calendar', + 'style' => ['height' => 6, 'width' => 10, 'absolutePosition' => ['x' => 13, 'y' => 0]], + ], + [ + 'calendar' => [ + 'month' => '12', + 'year' => '2024', + 'days' => ['1' => 63, '2' => 64, '3' => 65, '4' => 66, '5' => 67, '6' => 68, '7' => 63], + ], + 'style' => ['absolutePosition' => ['x' => 0, 'y' => 0]], + ], + ], + ]); + $this->assertEquals([ + [27, 28, 59, 28, 30, 19, 13, 20, 23, 20, 6, 19, 0, 4, 5, 3, 5, 13, 2, 5, 18, 0], + [0, 27, 44, 33, 0, 63, 64, 65, 66, 67, 68, 63, 0, 28, 36, 28, 30, 0, 0, 0, 0, 0], + [0, 34, 44, 27, 30, 65, 65, 65, 65, 65, 65, 65, 0, 3, 1, 12, 5, 14, 4, 1, 18, 0], + [27, 31, 44, 28, 27, 65, 65, 65, 65, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [28, 28, 44, 28, 34, 65, 65, 65, 65, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + [28, 35, 44, 29, 27, 65, 65, 65, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + ], $result); + + foreach ($result as $row) { + $this->assertCount(22, $row); + } + } + + public function testShouldLayoutCalendarOnTheRight(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 6, 'width' => 22], + 'components' => [ + [ + 'template' => 'Merry Christmas', + 'style' => ['height' => 6, 'width' => 10, 'absolutePosition' => ['x' => 0, 'y' => 0]], + ], + [ + 'calendar' => [ + 'defaultDayColor' => 66, + 'month' => '12', + 'year' => '2028', + 'days' => ['25' => 63], + ], + 'style' => ['absolutePosition' => ['x' => 10, 'y' => 0]], + ], + ], + ]); + $this->assertEquals([ + [13, 5, 18, 18, 25, 0, 0, 0, 0, 0, 27, 28, 59, 28, 34, 19, 13, 20, 23, 20, 6, 19], + [3, 8, 18, 9, 19, 20, 13, 1, 19, 0, 0, 27, 44, 28, 0, 0, 0, 0, 0, 0, 66, 66], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29, 44, 35, 0, 66, 66, 66, 66, 66, 66, 66], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 36, 44, 27, 32, 66, 66, 66, 66, 66, 66, 66], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 27, 33, 44, 28, 29, 66, 66, 66, 66, 66, 66, 66], + [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 28, 30, 44, 29, 27, 66, 63, 66, 66, 66, 66, 66], + ], $result); + + foreach ($result as $row) { + $this->assertCount(22, $row); + } + } + + public function testShouldRespectDoubleReturns(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 3, 'width' => 2], + 'components' => [ + ['template' => "h\n\ni", 'style' => ['align' => 'top', 'justify' => 'left']], + ], + ]); + $this->assertEquals([[8, 0], [0, 0], [9, 0]], $result); + } + + public function testShouldRespectTripleReturns(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 4, 'width' => 2], + 'components' => [ + ['template' => "h\n\n\ni", 'style' => ['align' => 'top', 'justify' => 'left']], + ], + ]); + $this->assertEquals([[8, 0], [0, 0], [0, 0], [9, 0]], $result); + } + + public function testShouldLetUsUseRandomColors(): void + { + $result = Vbml::parse([ + 'style' => ['height' => 1, 'width' => 1], + 'components' => [ + ['randomColors' => ['colors' => [61]]], + ], + ]); + $this->assertSame(61, $result[0][0]); + } +}