diff --git a/.editorconfig b/.editorconfig deleted file mode 100644 index 8972995..0000000 --- a/.editorconfig +++ /dev/null @@ -1,12 +0,0 @@ -# More rules can be found at https://editorconfig.org - -root = true - -# Default settings for all files -[*] -charset = utf-8 -end_of_line = lf -insert_final_newline = true -trim_trailing_whitespace = true -indent_style = space -indent_size = 4 diff --git a/.gitignore b/.gitignore index 244aa51..06868a1 100644 --- a/.gitignore +++ b/.gitignore @@ -6,13 +6,13 @@ /conifer # Docs -/_book/ -docs/.vitepress/dist docs/.vitepress/cache +docs/.vitepress/dist # test artifacts /test/wp/ /test/wp-tests-lib/ +.phpunit.result.cache # build artifacts /conifer-*.tar.gz @@ -32,6 +32,4 @@ wp-cli.local.yml # IDEs .vscode/* -.idea - -.phpunit.result.cache +.idea/ diff --git a/.lando.yml b/.lando.yml index 33bd89a..c48be06 100644 --- a/.lando.yml +++ b/.lando.yml @@ -5,9 +5,13 @@ config: php: '8.1' services: + node: + type: node:24 + run: + - yarn && yarn docs:build appserver: - build_as_root: + run_as_root: - apt-get update - apt-get install zip - apt-get install subversion -y @@ -16,11 +20,6 @@ services: - composer install - ./scripts/setup-wordpress.sh - node: - type: node:22 - build: - - yarn && yarn docs:build - database: type: mysql:5.7 @@ -127,11 +126,6 @@ tooling: cmd: 'yarn docs:build' description: 'Build Conifer docs' - rector: - service: appserver - cmd: 'rector' - description: 'Run Rector commands' - proxy: appserver: - conifer.lndo.site diff --git a/composer.json b/composer.json index ec9de40..0aa6904 100644 --- a/composer.json +++ b/composer.json @@ -22,29 +22,26 @@ "prefer-stable": true, "require": { "php": ">=8.1", - "ext-dom": "*", - "ext-libxml": "*", "timber/timber": "^2.2" }, "require-dev": { "10up/wp_mock": "dev-dev", - "behat/behat": "^3.25.0", + "behat/behat": "^v3.29.0", "johnpbloch/wordpress-core": "^6.5.5", "johnpbloch/wordpress-core-installer": "^2.0.0", "mikey179/vfsstream": "~1", "mnsami/composer-custom-directory-installer": "^2.0", "php-stubs/acf-pro-stubs": "^6.5", - "phpunit/phpunit": "^9.0", - "rector/rector": "^2.2", - "sitecrafting/groot": "dev-master", - "szepeviktor/phpstan-wordpress": "^2.0.3", - "wp-coding-standards/wpcs": "^3.2.0" + "phpunit/phpunit": "^9", + "sitecrafting/groot": "^1.0", + "szepeviktor/phpstan-wordpress": "^v2.0.3", + "wp-coding-standards/wpcs": "^2.3" }, "config": { + "sort-packages": true, "platform": { "php": "8.1" }, - "sort-packages": true, "allow-plugins": { "composer/installers": true, "dealerdirect/phpcodesniffer-composer-installer": true, diff --git a/composer.lock b/composer.lock index 9128247..b21a76e 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "a1143987badb25d7703a7ecf98b1c129", + "content-hash": "ffdb9236f5cc2e26b517c69e07fba0fd", "packages": [ { "name": "symfony/deprecation-contracts", @@ -344,16 +344,16 @@ }, { "name": "twig/twig", - "version": "v3.22.0", + "version": "v3.23.0", "source": { "type": "git", "url": "https://github.com/twigphp/Twig.git", - "reference": "4509984193026de413baf4ba80f68590a7f2c51d" + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/twigphp/Twig/zipball/4509984193026de413baf4ba80f68590a7f2c51d", - "reference": "4509984193026de413baf4ba80f68590a7f2c51d", + "url": "https://api.github.com/repos/twigphp/Twig/zipball/a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", + "reference": "a64dc5d2cc7d6cafb9347f6cd802d0d06d0351c9", "shasum": "" }, "require": { @@ -407,7 +407,7 @@ ], "support": { "issues": "https://github.com/twigphp/Twig/issues", - "source": "https://github.com/twigphp/Twig/tree/v3.22.0" + "source": "https://github.com/twigphp/Twig/tree/v3.23.0" }, "funding": [ { @@ -419,7 +419,7 @@ "type": "tidelift" } ], - "time": "2025-10-29T15:56:47+00:00" + "time": "2026-01-23T21:00:41+00:00" } ], "packages-dev": [ @@ -536,16 +536,16 @@ }, { "name": "behat/behat", - "version": "v3.26.0", + "version": "v3.29.0", "source": { "type": "git", "url": "https://github.com/Behat/Behat.git", - "reference": "1b6b08efa995fe4135901b862d112adc7e95ecbb" + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Behat/zipball/1b6b08efa995fe4135901b862d112adc7e95ecbb", - "reference": "1b6b08efa995fe4135901b862d112adc7e95ecbb", + "url": "https://api.github.com/repos/Behat/Behat/zipball/51bdf81639a14645c5d2c06926f4aa37d204921b", + "reference": "51bdf81639a14645c5d2c06926f4aa37d204921b", "shasum": "" }, "require": { @@ -554,7 +554,7 @@ "composer/xdebug-handler": "^1.4 || ^2.0 || ^3.0", "ext-mbstring": "*", "nikic/php-parser": "^4.19.2 || ^5.2", - "php": ">=8.1 <8.5", + "php": ">=8.1 <8.6", "psr/container": "^1.0 || ^2.0", "symfony/config": "^5.4 || ^6.4 || ^7.0", "symfony/console": "^5.4 || ^6.4 || ^7.0", @@ -564,8 +564,8 @@ "symfony/yaml": "^5.4 || ^6.4 || ^7.0" }, "require-dev": { - "friendsofphp/php-cs-fixer": "^3.68", "opis/json-schema": "^2.5", + "php-cs-fixer/shim": "^3.89", "phpstan/phpstan": "^2.0", "phpunit/phpunit": "^9.6", "rector/rector": "2.1.7", @@ -581,11 +581,6 @@ "bin/behat" ], "type": "library", - "extra": { - "branch-alias": { - "dev-master": "3.x-dev" - } - }, "autoload": { "psr-4": { "Behat\\Hook\\": "src/Behat/Hook/", @@ -625,22 +620,36 @@ ], "support": { "issues": "https://github.com/Behat/Behat/issues", - "source": "https://github.com/Behat/Behat/tree/v3.26.0" + "source": "https://github.com/Behat/Behat/tree/v3.29.0" }, - "time": "2025-10-29T09:46:14+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-11T09:51:30+00:00" }, { "name": "behat/gherkin", - "version": "v4.15.0", + "version": "v4.16.1", "source": { "type": "git", "url": "https://github.com/Behat/Gherkin.git", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b" + "reference": "e26037937dfd48528746764dd870bc5d0836665f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/Behat/Gherkin/zipball/05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", - "reference": "05a7459283e8e6af0d46ec25b8bb5960ca3cfa7b", + "url": "https://api.github.com/repos/Behat/Gherkin/zipball/e26037937dfd48528746764dd870bc5d0836665f", + "reference": "e26037937dfd48528746764dd870bc5d0836665f", "shasum": "" }, "require": { @@ -648,7 +657,7 @@ "php": ">=8.1 <8.6" }, "require-dev": { - "cucumber/gherkin-monorepo": "dev-gherkin-v36.0.0", + "cucumber/gherkin-monorepo": "dev-gherkin-v37.0.0", "friendsofphp/php-cs-fixer": "^3.77", "mikey179/vfsstream": "^1.6", "phpstan/extension-installer": "^1", @@ -694,9 +703,23 @@ ], "support": { "issues": "https://github.com/Behat/Gherkin/issues", - "source": "https://github.com/Behat/Gherkin/tree/v4.15.0" + "source": "https://github.com/Behat/Gherkin/tree/v4.16.1" }, - "time": "2025-11-05T15:34:04+00:00" + "funding": [ + { + "url": "https://github.com/acoulton", + "type": "github" + }, + { + "url": "https://github.com/carlos-granados", + "type": "github" + }, + { + "url": "https://github.com/stof", + "type": "github" + } + ], + "time": "2025-12-08T16:12:58+00:00" }, { "name": "composer/pcre", @@ -843,102 +866,6 @@ ], "time": "2024-05-06T16:37:16+00:00" }, - { - "name": "dealerdirect/phpcodesniffer-composer-installer", - "version": "v1.2.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/composer-installer.git", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/composer-installer/zipball/845eb62303d2ca9b289ef216356568ccc075ffd1", - "reference": "845eb62303d2ca9b289ef216356568ccc075ffd1", - "shasum": "" - }, - "require": { - "composer-plugin-api": "^2.2", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.1.0 || ^4.0" - }, - "require-dev": { - "composer/composer": "^2.2", - "ext-json": "*", - "ext-zip": "*", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcompatibility/php-compatibility": "^9.0 || ^10.0.0@dev", - "yoast/phpunit-polyfills": "^1.0" - }, - "type": "composer-plugin", - "extra": { - "class": "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\Plugin" - }, - "autoload": { - "psr-4": { - "PHPCSStandards\\Composer\\Plugin\\Installers\\PHPCodeSniffer\\": "src/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "authors": [ - { - "name": "Franck Nijhof", - "email": "opensource@frenck.dev", - "homepage": "https://frenck.dev", - "role": "Open source developer" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/composer-installer/graphs/contributors" - } - ], - "description": "PHP_CodeSniffer Standards Composer Installer Plugin", - "keywords": [ - "PHPCodeSniffer", - "PHP_CodeSniffer", - "code quality", - "codesniffer", - "composer", - "installer", - "phpcbf", - "phpcs", - "plugin", - "qa", - "quality", - "standard", - "standards", - "style guide", - "stylecheck", - "tests" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/composer-installer/issues", - "security": "https://github.com/PHPCSStandards/composer-installer/security/policy", - "source": "https://github.com/PHPCSStandards/composer-installer" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-11-11T04:32:07+00:00" - }, { "name": "doctrine/instantiator", "version": "2.0.0", @@ -1062,16 +989,16 @@ }, { "name": "johnpbloch/wordpress-core", - "version": "6.8.3", + "version": "6.9.1", "source": { "type": "git", "url": "https://github.com/johnpbloch/wordpress-core.git", - "reference": "0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1" + "reference": "840ffab74cb3d19cc0076363358f783041b5f3cf" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1", - "reference": "0641ab5518c94c1ab094ad4ccdc46aa9c4657fc1", + "url": "https://api.github.com/repos/johnpbloch/wordpress-core/zipball/840ffab74cb3d19cc0076363358f783041b5f3cf", + "reference": "840ffab74cb3d19cc0076363358f783041b5f3cf", "shasum": "" }, "require": { @@ -1079,7 +1006,7 @@ "php": ">=7.2.24" }, "provide": { - "wordpress/core-implementation": "6.8.3" + "wordpress/core-implementation": "6.9.1" }, "type": "wordpress-core", "notification-url": "https://packagist.org/downloads/", @@ -1106,7 +1033,7 @@ "source": "https://core.trac.wordpress.org/browser", "wiki": "https://codex.wordpress.org/" }, - "time": "2025-09-30T18:14:19+00:00" + "time": "2026-02-03T18:03:48+00:00" }, { "name": "johnpbloch/wordpress-core-installer", @@ -1415,16 +1342,16 @@ }, { "name": "nikic/php-parser", - "version": "v5.6.2", + "version": "v5.7.0", "source": { "type": "git", "url": "https://github.com/nikic/PHP-Parser.git", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb" + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3a454ca033b9e06b63282ce19562e892747449bb", - "reference": "3a454ca033b9e06b63282ce19562e892747449bb", + "url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/dca41cd15c2ac9d055ad70dbfd011130757d1f82", + "reference": "dca41cd15c2ac9d055ad70dbfd011130757d1f82", "shasum": "" }, "require": { @@ -1467,9 +1394,9 @@ ], "support": { "issues": "https://github.com/nikic/PHP-Parser/issues", - "source": "https://github.com/nikic/PHP-Parser/tree/v5.6.2" + "source": "https://github.com/nikic/PHP-Parser/tree/v5.7.0" }, - "time": "2025-10-21T19:32:17+00:00" + "time": "2025-12-06T11:56:16+00:00" }, { "name": "phar-io/manifest", @@ -1643,16 +1570,16 @@ }, { "name": "php-stubs/wordpress-stubs", - "version": "v6.8.3", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/php-stubs/wordpress-stubs.git", - "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114" + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/abeb5a8b58fda7ac21f15ee596f302f2959a7114", - "reference": "abeb5a8b58fda7ac21f15ee596f302f2959a7114", + "url": "https://api.github.com/repos/php-stubs/wordpress-stubs/zipball/f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", + "reference": "f12220f303e0d7c0844c0e5e957b0c3cee48d2f7", "shasum": "" }, "conflict": { @@ -1663,9 +1590,10 @@ "nikic/php-parser": "^5.5", "php": "^7.4 || ^8.0", "php-stubs/generator": "^0.8.3", - "phpdocumentor/reflection-docblock": "^5.4.1", + "phpdocumentor/reflection-docblock": "^6.0", "phpstan/phpstan": "^2.1", "phpunit/phpunit": "^9.5", + "symfony/polyfill-php80": "*", "szepeviktor/phpcs-psr-12-neutron-hybrid-ruleset": "^1.1.1", "wp-coding-standards/wpcs": "3.1.0 as 2.3.0" }, @@ -1688,192 +1616,17 @@ ], "support": { "issues": "https://github.com/php-stubs/wordpress-stubs/issues", - "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.8.3" - }, - "time": "2025-09-30T20:58:47+00:00" - }, - { - "name": "phpcsstandards/phpcsextra", - "version": "1.5.0", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSExtra.git", - "reference": "b598aa890815b8df16363271b659d73280129101" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSExtra/zipball/b598aa890815b8df16363271b659d73280129101", - "reference": "b598aa890815b8df16363271b659d73280129101", - "shasum": "" - }, - "require": { - "php": ">=5.4", - "phpcsstandards/phpcsutils": "^1.2.0", - "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" - }, - "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.2.0", - "phpcsstandards/phpcsdevtools": "^1.2.1", - "phpunit/phpunit": "^4.5 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.3.4" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSExtra/graphs/contributors" - } - ], - "description": "A collection of sniffs and standards for use with PHP_CodeSniffer.", - "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "standards", - "static analysis" - ], - "support": { - "issues": "https://github.com/PHPCSStandards/PHPCSExtra/issues", - "security": "https://github.com/PHPCSStandards/PHPCSExtra/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSExtra" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-11-12T23:06:57+00:00" - }, - { - "name": "phpcsstandards/phpcsutils", - "version": "1.2.1", - "source": { - "type": "git", - "url": "https://github.com/PHPCSStandards/PHPCSUtils.git", - "reference": "d71128c702c180ca3b27c761b6773f883394f162" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHPCSUtils/zipball/d71128c702c180ca3b27c761b6773f883394f162", - "reference": "d71128c702c180ca3b27c761b6773f883394f162", - "shasum": "" - }, - "require": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.4.1 || ^0.5 || ^0.6.2 || ^0.7 || ^1.0", - "php": ">=5.4", - "squizlabs/php_codesniffer": "^3.13.5 || ^4.0.1" - }, - "require-dev": { - "ext-filter": "*", - "php-parallel-lint/php-console-highlighter": "^1.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", - "phpcsstandards/phpcsdevcs": "^1.2.0", - "yoast/phpunit-polyfills": "^1.1.0 || ^2.0.0 || ^3.0.0" - }, - "type": "phpcodesniffer-standard", - "extra": { - "branch-alias": { - "dev-stable": "1.x-dev", - "dev-develop": "1.x-dev" - } + "source": "https://github.com/php-stubs/wordpress-stubs/tree/v6.9.1" }, - "autoload": { - "classmap": [ - "PHPCSUtils/" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "LGPL-3.0-or-later" - ], - "authors": [ - { - "name": "Juliette Reinders Folmer", - "homepage": "https://github.com/jrfnl", - "role": "lead" - }, - { - "name": "Contributors", - "homepage": "https://github.com/PHPCSStandards/PHPCSUtils/graphs/contributors" - } - ], - "description": "A suite of utility functions for use with PHP_CodeSniffer", - "homepage": "https://phpcsutils.com/", - "keywords": [ - "PHP_CodeSniffer", - "phpcbf", - "phpcodesniffer-standard", - "phpcs", - "phpcs3", - "phpcs4", - "standards", - "static analysis", - "tokens", - "utility" - ], - "support": { - "docs": "https://phpcsutils.com/", - "issues": "https://github.com/PHPCSStandards/PHPCSUtils/issues", - "security": "https://github.com/PHPCSStandards/PHPCSUtils/security/policy", - "source": "https://github.com/PHPCSStandards/PHPCSUtils" - }, - "funding": [ - { - "url": "https://github.com/PHPCSStandards", - "type": "github" - }, - { - "url": "https://github.com/jrfnl", - "type": "github" - }, - { - "url": "https://opencollective.com/php_codesniffer", - "type": "open_collective" - }, - { - "url": "https://thanks.dev/u/gh/phpcsstandards", - "type": "thanks_dev" - } - ], - "time": "2025-11-17T12:58:33+00:00" + "time": "2026-02-03T19:29:21+00:00" }, { "name": "phpstan/phpstan", - "version": "2.1.32", + "version": "2.1.40", "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpstan/phpstan/zipball/e126cad1e30a99b137b8ed75a85a676450ebb227", - "reference": "e126cad1e30a99b137b8ed75a85a676450ebb227", + "url": "https://api.github.com/repos/phpstan/phpstan/zipball/9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", + "reference": "9b2c7aeb83a75d8680ea5e7c9b7fca88052b766b", "shasum": "" }, "require": { @@ -1918,7 +1671,7 @@ "type": "github" } ], - "time": "2025-11-11T15:18:17+00:00" + "time": "2026-02-23T15:04:35+00:00" }, { "name": "phpunit/php-code-coverage", @@ -2241,16 +1994,16 @@ }, { "name": "phpunit/phpunit", - "version": "9.6.29", + "version": "9.6.34", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/phpunit.git", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3" + "reference": "b36f02317466907a230d3aa1d34467041271ef4a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", - "reference": "9ecfec57835a5581bc888ea7e13b51eb55ab9dd3", + "url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/b36f02317466907a230d3aa1d34467041271ef4a", + "reference": "b36f02317466907a230d3aa1d34467041271ef4a", "shasum": "" }, "require": { @@ -2272,7 +2025,7 @@ "phpunit/php-timer": "^5.0.3", "sebastian/cli-parser": "^1.0.2", "sebastian/code-unit": "^1.0.8", - "sebastian/comparator": "^4.0.9", + "sebastian/comparator": "^4.0.10", "sebastian/diff": "^4.0.6", "sebastian/environment": "^5.1.5", "sebastian/exporter": "^4.0.8", @@ -2324,7 +2077,7 @@ "support": { "issues": "https://github.com/sebastianbergmann/phpunit/issues", "security": "https://github.com/sebastianbergmann/phpunit/security/policy", - "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.29" + "source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.34" }, "funding": [ { @@ -2348,7 +2101,7 @@ "type": "tidelift" } ], - "time": "2025-09-24T06:29:11+00:00" + "time": "2026-01-27T05:45:00+00:00" }, { "name": "psr/container", @@ -2503,66 +2256,6 @@ }, "time": "2024-09-11T13:17:53+00:00" }, - { - "name": "rector/rector", - "version": "2.2.8", - "source": { - "type": "git", - "url": "https://github.com/rectorphp/rector.git", - "reference": "303aa811649ccd1d32e51e62d5c85949d01b5f1b" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/rectorphp/rector/zipball/303aa811649ccd1d32e51e62d5c85949d01b5f1b", - "reference": "303aa811649ccd1d32e51e62d5c85949d01b5f1b", - "shasum": "" - }, - "require": { - "php": "^7.4|^8.0", - "phpstan/phpstan": "^2.1.32" - }, - "conflict": { - "rector/rector-doctrine": "*", - "rector/rector-downgrade-php": "*", - "rector/rector-phpunit": "*", - "rector/rector-symfony": "*" - }, - "suggest": { - "ext-dom": "To manipulate phpunit.xml via the custom-rule command" - }, - "bin": [ - "bin/rector" - ], - "type": "library", - "autoload": { - "files": [ - "bootstrap.php" - ] - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Instant Upgrade and Automated Refactoring of any PHP code", - "homepage": "https://getrector.com/", - "keywords": [ - "automation", - "dev", - "migration", - "refactoring" - ], - "support": { - "issues": "https://github.com/rectorphp/rector/issues", - "source": "https://github.com/rectorphp/rector/tree/2.2.8" - }, - "funding": [ - { - "url": "https://github.com/tomasvotruba", - "type": "github" - } - ], - "time": "2025-11-12T18:38:00+00:00" - }, { "name": "sebastian/cli-parser", "version": "1.0.2", @@ -2732,16 +2425,16 @@ }, { "name": "sebastian/comparator", - "version": "4.0.9", + "version": "4.0.10", "source": { "type": "git", "url": "https://github.com/sebastianbergmann/comparator.git", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5" + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/67a2df3a62639eab2cc5906065e9805d4fd5dfc5", - "reference": "67a2df3a62639eab2cc5906065e9805d4fd5dfc5", + "url": "https://api.github.com/repos/sebastianbergmann/comparator/zipball/e4df00b9b3571187db2831ae9aada2c6efbd715d", + "reference": "e4df00b9b3571187db2831ae9aada2c6efbd715d", "shasum": "" }, "require": { @@ -2794,7 +2487,7 @@ ], "support": { "issues": "https://github.com/sebastianbergmann/comparator/issues", - "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.9" + "source": "https://github.com/sebastianbergmann/comparator/tree/4.0.10" }, "funding": [ { @@ -2814,7 +2507,7 @@ "type": "tidelift" } ], - "time": "2025-08-10T06:51:50+00:00" + "time": "2026-01-24T09:22:56+00:00" }, { "name": "sebastian/complexity", @@ -3576,16 +3269,16 @@ }, { "name": "sitecrafting/groot", - "version": "dev-master", + "version": "1.0.0", "source": { "type": "git", "url": "https://github.com/sitecrafting/groot.git", - "reference": "92c6d81d85b074b66a0d0b0317e02c30e6b5f4e0" + "reference": "9b87bf9de39675140de7eb4c3c3f61f137249baa" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/sitecrafting/groot/zipball/92c6d81d85b074b66a0d0b0317e02c30e6b5f4e0", - "reference": "92c6d81d85b074b66a0d0b0317e02c30e6b5f4e0", + "url": "https://api.github.com/repos/sitecrafting/groot/zipball/9b87bf9de39675140de7eb4c3c3f61f137249baa", + "reference": "9b87bf9de39675140de7eb4c3c3f61f137249baa", "shasum": "" }, "require": { @@ -3601,7 +3294,6 @@ "squizlabs/php_codesniffer": "3.*", "wp-coding-standards/wpcs": "^0.14" }, - "default-branch": true, "type": "library", "extra": { "wordpress-install-dir": { @@ -3619,16 +3311,20 @@ ], "authors": [ { - "name": "SiteCrafting, Inc.", - "email": "hello@sitecrafting.com" + "name": "Coby Tamayo", + "email": "ctamayo@sitecrafting.com" + }, + { + "name": "Reena Hensley", + "email": "rhensley@sitecrafting.com" } ], "description": "The official SiteCrafting WordPress starter theme", "support": { "issues": "https://github.com/sitecrafting/groot/issues", - "source": "https://github.com/sitecrafting/groot/tree/master" + "source": "https://github.com/sitecrafting/groot/tree/v1.0.0" }, - "time": "2025-10-17T20:06:34+00:00" + "time": "2024-10-25T20:54:01+00:00" }, { "name": "squizlabs/php_codesniffer", @@ -3711,16 +3407,16 @@ }, { "name": "symfony/config", - "version": "v6.4.28", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/config.git", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62" + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/config/zipball/15947c18ef3ddb0b2f4ec936b9e90e2520979f62", - "reference": "15947c18ef3ddb0b2f4ec936b9e90e2520979f62", + "url": "https://api.github.com/repos/symfony/config/zipball/ce9cb0c0d281aaf188b802d4968e42bfb60701e9", + "reference": "ce9cb0c0d281aaf188b802d4968e42bfb60701e9", "shasum": "" }, "require": { @@ -3766,7 +3462,7 @@ "description": "Helps you find, load, combine, autofill and validate configuration values of any kind", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/config/tree/v6.4.28" + "source": "https://github.com/symfony/config/tree/v6.4.34" }, "funding": [ { @@ -3786,20 +3482,20 @@ "type": "tidelift" } ], - "time": "2025-11-01T19:52:02+00:00" + "time": "2026-02-24T17:34:50+00:00" }, { "name": "symfony/console", - "version": "v6.4.27", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/console.git", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc" + "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/console/zipball/13d3176cf8ad8ced24202844e9f95af11e2959fc", - "reference": "13d3176cf8ad8ced24202844e9f95af11e2959fc", + "url": "https://api.github.com/repos/symfony/console/zipball/7b1f1c37eff5910ddda2831345467e593a5120ad", + "reference": "7b1f1c37eff5910ddda2831345467e593a5120ad", "shasum": "" }, "require": { @@ -3864,7 +3560,7 @@ "terminal" ], "support": { - "source": "https://github.com/symfony/console/tree/v6.4.27" + "source": "https://github.com/symfony/console/tree/v6.4.34" }, "funding": [ { @@ -3884,20 +3580,20 @@ "type": "tidelift" } ], - "time": "2025-10-06T10:25:16+00:00" + "time": "2026-02-23T15:42:15+00:00" }, { "name": "symfony/dependency-injection", - "version": "v6.4.26", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/dependency-injection.git", - "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898" + "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/5f311eaf0b321f8ec640f6bae12da43a14026898", - "reference": "5f311eaf0b321f8ec640f6bae12da43a14026898", + "url": "https://api.github.com/repos/symfony/dependency-injection/zipball/91e49958b8a6092e48e4711894a1aeb1b151c62a", + "reference": "91e49958b8a6092e48e4711894a1aeb1b151c62a", "shasum": "" }, "require": { @@ -3949,7 +3645,7 @@ "description": "Allows you to standardize and centralize the way objects are constructed in your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/dependency-injection/tree/v6.4.26" + "source": "https://github.com/symfony/dependency-injection/tree/v6.4.34" }, "funding": [ { @@ -3969,20 +3665,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T09:57:09+00:00" + "time": "2026-02-24T15:33:38+00:00" }, { "name": "symfony/event-dispatcher", - "version": "v6.4.25", + "version": "v6.4.32", "source": { "type": "git", "url": "https://github.com/symfony/event-dispatcher.git", - "reference": "b0cf3162020603587363f0551cd3be43958611ff" + "reference": "99d7e101826e6610606b9433248f80c1997cd20b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/b0cf3162020603587363f0551cd3be43958611ff", - "reference": "b0cf3162020603587363f0551cd3be43958611ff", + "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/99d7e101826e6610606b9433248f80c1997cd20b", + "reference": "99d7e101826e6610606b9433248f80c1997cd20b", "shasum": "" }, "require": { @@ -4033,7 +3729,7 @@ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.25" + "source": "https://github.com/symfony/event-dispatcher/tree/v6.4.32" }, "funding": [ { @@ -4053,7 +3749,7 @@ "type": "tidelift" } ], - "time": "2025-08-13T09:41:44+00:00" + "time": "2026-01-05T11:13:48+00:00" }, { "name": "symfony/event-dispatcher-contracts", @@ -4133,16 +3829,16 @@ }, { "name": "symfony/filesystem", - "version": "v6.4.24", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/filesystem.git", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8" + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/filesystem/zipball/75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", - "reference": "75ae2edb7cdcc0c53766c30b0a2512b8df574bd8", + "url": "https://api.github.com/repos/symfony/filesystem/zipball/01ffe0411b842f93c571e5c391f289c3fdd498c3", + "reference": "01ffe0411b842f93c571e5c391f289c3fdd498c3", "shasum": "" }, "require": { @@ -4179,7 +3875,7 @@ "description": "Provides basic utilities for the filesystem", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/filesystem/tree/v6.4.24" + "source": "https://github.com/symfony/filesystem/tree/v6.4.34" }, "funding": [ { @@ -4199,7 +3895,7 @@ "type": "tidelift" } ], - "time": "2025-07-10T08:14:14+00:00" + "time": "2026-02-24T17:51:06+00:00" }, { "name": "symfony/polyfill-intl-grapheme", @@ -4457,16 +4153,16 @@ }, { "name": "symfony/string", - "version": "v6.4.26", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/string.git", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea" + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/string/zipball/5621f039a71a11c87c106c1c598bdcd04a19aeea", - "reference": "5621f039a71a11c87c106c1c598bdcd04a19aeea", + "url": "https://api.github.com/repos/symfony/string/zipball/2adaf4106f2ef4c67271971bde6d3fe0a6936432", + "reference": "2adaf4106f2ef4c67271971bde6d3fe0a6936432", "shasum": "" }, "require": { @@ -4522,7 +4218,7 @@ "utf8" ], "support": { - "source": "https://github.com/symfony/string/tree/v6.4.26" + "source": "https://github.com/symfony/string/tree/v6.4.34" }, "funding": [ { @@ -4542,20 +4238,20 @@ "type": "tidelift" } ], - "time": "2025-09-11T14:32:46+00:00" + "time": "2026-02-08T20:44:54+00:00" }, { "name": "symfony/translation", - "version": "v6.4.26", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/translation.git", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4" + "reference": "d07d117db41341511671b0a1a2be48f2772189ce" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/translation/zipball/c8559fe25c7ee7aa9d28f228903a46db008156a4", - "reference": "c8559fe25c7ee7aa9d28f228903a46db008156a4", + "url": "https://api.github.com/repos/symfony/translation/zipball/d07d117db41341511671b0a1a2be48f2772189ce", + "reference": "d07d117db41341511671b0a1a2be48f2772189ce", "shasum": "" }, "require": { @@ -4621,7 +4317,7 @@ "description": "Provides tools to internationalize your application", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/translation/tree/v6.4.26" + "source": "https://github.com/symfony/translation/tree/v6.4.34" }, "funding": [ { @@ -4641,7 +4337,7 @@ "type": "tidelift" } ], - "time": "2025-09-05T18:17:25+00:00" + "time": "2026-02-16T20:44:03+00:00" }, { "name": "symfony/translation-contracts", @@ -4808,16 +4504,16 @@ }, { "name": "symfony/yaml", - "version": "v6.4.26", + "version": "v6.4.34", "source": { "type": "git", "url": "https://github.com/symfony/yaml.git", - "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0" + "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/symfony/yaml/zipball/0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", - "reference": "0fc8b966fd0dcaab544ae59bfc3a433f048c17b0", + "url": "https://api.github.com/repos/symfony/yaml/zipball/7bca30dabed7900a08c5ad4f1d6483f881a64d0f", + "reference": "7bca30dabed7900a08c5ad4f1d6483f881a64d0f", "shasum": "" }, "require": { @@ -4860,7 +4556,7 @@ "description": "Loads and dumps YAML files", "homepage": "https://symfony.com", "support": { - "source": "https://github.com/symfony/yaml/tree/v6.4.26" + "source": "https://github.com/symfony/yaml/tree/v6.4.34" }, "funding": [ { @@ -4880,7 +4576,7 @@ "type": "tidelift" } ], - "time": "2025-09-26T15:07:38+00:00" + "time": "2026-02-06T18:32:11+00:00" }, { "name": "szepeviktor/phpstan-wordpress", @@ -4997,38 +4693,30 @@ }, { "name": "wp-coding-standards/wpcs", - "version": "3.2.0", + "version": "2.3.0", "source": { "type": "git", "url": "https://github.com/WordPress/WordPress-Coding-Standards.git", - "reference": "d2421de7cec3274ae622c22c744de9a62c7925af" + "reference": "7da1894633f168fe244afc6de00d141f27517b62" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/d2421de7cec3274ae622c22c744de9a62c7925af", - "reference": "d2421de7cec3274ae622c22c744de9a62c7925af", + "url": "https://api.github.com/repos/WordPress/WordPress-Coding-Standards/zipball/7da1894633f168fe244afc6de00d141f27517b62", + "reference": "7da1894633f168fe244afc6de00d141f27517b62", "shasum": "" }, "require": { - "ext-filter": "*", - "ext-libxml": "*", - "ext-tokenizer": "*", - "ext-xmlreader": "*", "php": ">=5.4", - "phpcsstandards/phpcsextra": "^1.4.0", - "phpcsstandards/phpcsutils": "^1.1.0", - "squizlabs/php_codesniffer": "^3.13.0" + "squizlabs/php_codesniffer": "^3.3.1" }, "require-dev": { - "php-parallel-lint/php-console-highlighter": "^1.0.0", - "php-parallel-lint/php-parallel-lint": "^1.4.0", + "dealerdirect/phpcodesniffer-composer-installer": "^0.5 || ^0.6", "phpcompatibility/php-compatibility": "^9.0", - "phpcsstandards/phpcsdevtools": "^1.2.0", - "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0 || ^8.0 || ^9.0" + "phpcsstandards/phpcsdevtools": "^1.0", + "phpunit/phpunit": "^4.0 || ^5.0 || ^6.0 || ^7.0" }, "suggest": { - "ext-iconv": "For improved results", - "ext-mbstring": "For improved results" + "dealerdirect/phpcodesniffer-composer-installer": "^0.6 || This Composer plugin will sort out the PHPCS 'installed_paths' automatically." }, "type": "phpcodesniffer-standard", "notification-url": "https://packagist.org/downloads/", @@ -5045,7 +4733,6 @@ "keywords": [ "phpcs", "standards", - "static analysis", "wordpress" ], "support": { @@ -5053,27 +4740,18 @@ "source": "https://github.com/WordPress/WordPress-Coding-Standards", "wiki": "https://github.com/WordPress/WordPress-Coding-Standards/wiki" }, - "funding": [ - { - "url": "https://opencollective.com/php_codesniffer", - "type": "custom" - } - ], - "time": "2025-07-24T20:08:31+00:00" + "time": "2020-05-13T23:57:56+00:00" } ], "aliases": [], "minimum-stability": "dev", "stability-flags": { - "10up/wp_mock": 20, - "sitecrafting/groot": 20 + "10up/wp_mock": 20 }, "prefer-stable": true, "prefer-lowest": false, "platform": { - "php": ">=8.1", - "ext-dom": "*", - "ext-libxml": "*" + "php": ">=8.1" }, "platform-dev": {}, "platform-overrides": { diff --git a/docs/.vitepress/config.mts b/docs/.vitepress/config.mts index e550066..b66d4c3 100644 --- a/docs/.vitepress/config.mts +++ b/docs/.vitepress/config.mts @@ -45,7 +45,7 @@ export default defineConfig({ { text: 'Forms', link: '/forms' }, { text: 'Notifiers', link: '/notifiers' }, { text: 'Shortcodes', link: '/shortcodes' }, - { text: 'Twig', link: '/twig' }, + { text: 'Twig Helpers', link: '/twig' }, ], }, { @@ -76,4 +76,4 @@ export default defineConfig({ }, ], }, -}) +}) \ No newline at end of file diff --git a/docs/GLOSSARY.md b/docs/GLOSSARY.md index e318912..5ee470a 100644 --- a/docs/GLOSSARY.md +++ b/docs/GLOSSARY.md @@ -6,12 +6,12 @@ The anonymous function you pass to the `Conifer\Site::configure()` method. See T Example: -``` +```php /* functions.php */ use Conifer\Site; $site = new Site(); $site->configure(function() { - /* now we're in the config callback; call add_action() and stuff here... */ + /* now we're in the config callback; call add_action() and stuff here... */ }); ``` diff --git a/docs/SUMMARY.md b/docs/SUMMARY.md index d2002e6..f20d793 100644 --- a/docs/SUMMARY.md +++ b/docs/SUMMARY.md @@ -1,33 +1,35 @@ ## Getting Started -* [What is Conifer?](/what-is-conifer.md) -* [Requirements](/requirements.md) -* [Installation](/installation.md) +* [What is Conifer?](what-is-conifer.md) +* [Requirements](requirements.md) +* [Installation](installation.md) * [Conifer Basics](basics.md) ## Features * [The Site Class](site.md) * [Posts and Post Types](posts.md) -* [AJAX Handlers](/ajax-handlers.md) +* [AJAX Handlers](ajax-handlers.md) * [Alerts](alerts.md) * [Search](search.md) * [Forms](forms.md) * [Admin Helpers](admin.md) -* [Authorization](/authorization.md) -* [Notifiers](/notifiers.md) -* [Shortcodes](/shortcodes.md) +* [Authorization](authorization.md) +* [Notifiers](notifiers.md) +* [Shortcodes](shortcodes.md) ## Contributing -* [How to Contribute](/how-to-contribute.md) -* [Development Setup](/dev-setup.md) -* [Testing](/testing.md) -* [Governance](/governance.md) -* [Code of Conduct](/code-of-conduct.md) +* [How to Contribute](how-to-contribute.md) +* [Development Setup](dev-setup.md) +* [Testing](testing.md) +* [Governance](governance.md) +* [Code of Conduct](code-of-conduct.md) ## Changelog -* [2020](/changelog/2020.md) -* [2019](/changelog/2019.md) -* [2018](/changelog/2018.md) +* [2026](changelog/2026.md) +* [2024](changelog/2024.md) +* [2020](changelog/2020.md) +* [2019](changelog/2019.md) +* [2018](changelog/2018.md) \ No newline at end of file diff --git a/docs/changelog/2026.md b/docs/changelog/2026.md new file mode 100644 index 0000000..6333a3d --- /dev/null +++ b/docs/changelog/2026.md @@ -0,0 +1,5 @@ +## v1.0.4 + +* Update multiple dependencies. +* Remove dependencies that were no longer needed. +* Migrate from [Gitbook](https://www.gitbook.com) to [VitePress](https://vitepress.dev). diff --git a/docs/code-of-conduct.md b/docs/code-of-conduct.md index 80d183c..8c50e94 100644 --- a/docs/code-of-conduct.md +++ b/docs/code-of-conduct.md @@ -20,7 +20,7 @@ Examples of unacceptable behavior by participants include: * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission -* Other conduct which could reasonably be considered inappropriate in a professional setting +* Other conduct, which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities diff --git a/docs/forms.md b/docs/forms.md index b292e82..d958498 100644 --- a/docs/forms.md +++ b/docs/forms.md @@ -3,7 +3,6 @@ ## Getting Started Conifer's Form API allows you to represent your custom forms as first-class OO citizens and helps to streamline validation, entry processing, and front-end output. To get started, extend the `Conifer\Form\AbstractBase` class and add your custom fields, validators, and processing logic: - ```php use Conifer\Form\AbstractBase; @@ -36,7 +35,7 @@ class MyForm extends AbstractBase { ]; } - // Process an incoming form submission + // Process an incoming form submission public function process(array $request) { $valid = $this->validate($request); if ($valid) { @@ -71,6 +70,7 @@ Timber::render('my-form-page.twig', ['myForm' => $form]); Displaying validation errors and submitted values is just as simple: + ```twig {% if myForm.has_errors %}

Danger, Will Robinson! Your form has the following errors: {{ myForm.get_unique_error_messages|join(', ') }}

diff --git a/docs/governance.md b/docs/governance.md index d1d84b3..f82166b 100644 --- a/docs/governance.md +++ b/docs/governance.md @@ -2,8 +2,8 @@ Governance of the project is simple and straightforward: -- **Maintainers** make consensus driven decisions about the project. -- The **Benevolent Dictator** has the final say on all decisions but generally only exercises this power on an as-needed basis, most usually when the maintainers cannot reach consensus. +- **Maintainers** make consensus-driven decisions about the project. +- The **Benevolent Dictator** has the final say on all decisions but generally only exercises this power on an as-needed basis, most usually when the maintainers cannot reach consensus. ## Current Team @@ -13,10 +13,12 @@ The team consists of: ### Benevolent Dictator -- [Coby Tamayo](https://github.com/acobster/) +- [Scott Dunham](https://github.com/sdunham) ### Maintainers -- [Scott Dunham](https://github.com/sdunham) +- [Axel Koziol](https://github.com/akoziolsc) - [Phil Price](https://github.com/philmprice) +- [Reena Hensley](https://github.com/rbhensley) +- [Ryan Hendrickson](https://github.com/rhendrickson-sc) diff --git a/docs/how-to-contribute.md b/docs/how-to-contribute.md index 96cbd5b..a51c7c8 100644 --- a/docs/how-to-contribute.md +++ b/docs/how-to-contribute.md @@ -25,4 +25,4 @@ If you're wondering how best to set up Conifer to work on the source code, check In general, please follow the Pull Request template that GitHub prompts you with when you create a PR. This comprises various criteria you should think through. Not all criteria necessarily need to be met for every PR (for example, update to documentation only don't need unit tests). Please apply good judgment, think through the effects of your change, and try to empathize with maintainers as well as future developers who may be interested in the reasoning behind a given change. -If you edit any code, please run unit tests and coding standard checks (`lando unit`, `lando sniff`, and `lando rector`, respectively) before creating your PR. If either of these checks fails, it is your responsibility to figure out why, and fix any failures caused by your code. Of course, if you don't understand why something failed or the reasoning behind a certain test/check, feel free to reach out and we can work with you to figure out the best solution! +If you edit any code, please run unit tests and coding standard checks (`lando unit` and `lando sniff`, respectively) before creating your PR. If either of these checks fails, it is your responsibility to figure out why, and fix any failures caused by your code. Of course, if you don't understand why something failed or the reasoning behind a certain test/check, feel free to reach out and we can work with you to figure out the best solution! diff --git a/docs/index.md b/docs/index.md index 4797513..b6c3cd8 100644 --- a/docs/index.md +++ b/docs/index.md @@ -21,5 +21,4 @@ features: details: Easily add any Trig functions/filters your project needs. - title: Custom admin columns and filters details: Easily add custom admin columns and filters to your admin screens, without having to remember all the arguments to the manage_*_columns hooks. ---- - +--- \ No newline at end of file diff --git a/docs/installation.md b/docs/installation.md index 161a072..2d9af32 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,21 +4,20 @@ This is the recommended route for most use-cases. -``` +```bash composer require --save sitecrafting/conifer ``` -In order to put Conifer in the wp-content/plugins directory -automatically, we recommend [composer-custom-directory-installer](https://github.com/mnsami/composer-custom-directory-installer): +To put Conifer in the wp-content/plugins directory automatically, we recommend [composer-custom-directory-installer](https://github.com/mnsami/composer-custom-directory-installer): ```json { - "require": { - "sitecrafting/conifer": "dev-master" - }, - "require-dev": { - "mnsami/composer-custom-directory-installer": "^1.1" - } + "require": { + "sitecrafting/conifer": "^1.0.0" + }, + "require-dev": { + "mnsami/composer-custom-directory-installer": "^1.1" + } } ``` @@ -30,7 +29,7 @@ automatically, we recommend [composer-custom-directory-installer](https://github ## From source -``` +```bash git clone https://github.com/sitecrafting/conifer /path/to/wp-content/plugins/conifer cd /path/to/wp-content/plugins/conifer composer install diff --git a/docs/notifiers.md b/docs/notifiers.md index 77665d4..98a3449 100644 --- a/docs/notifiers.md +++ b/docs/notifiers.md @@ -19,7 +19,7 @@ $notifier->notify_html( // send a plaintext message with URL $notifier->notify_plaintext( 'A Funny Thing Happened on the Internet', - "O joy of joy! O dream of dreams!\n\nhttp://example.com/cat.gif" + "O joy of joy! O dream of dreams!\n\nhttps://example.com/cat.gif" ); ``` diff --git a/docs/testing.md b/docs/testing.md index d3525d9..95a3d5a 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -30,12 +30,6 @@ As of 1.0, static analysis by PHPStan is also available: lando analyze ``` -As of 1.0, Rector is also included: - -```shell -lando rector -``` - ## Writing new tests Our guidelines for how and what to test are: diff --git a/docs/twig-helpers.md b/docs/twig-helpers.md new file mode 100644 index 0000000..e124609 --- /dev/null +++ b/docs/twig-helpers.md @@ -0,0 +1,107 @@ +# Twig Helpers + +Conifer defines the `Conifer\Twig\HelperInterface` for quickly defining custom Twig functions and filters. + +The interface is simple: + +```php +add_twig_helper(new ThemeTwigHelper()); +``` + +Each public method must return an array of callables, keyed by the function/filter name to be used in Twig templates. For example, say you want to define a `zany_caps()` Twig filter, for getting all zany with capitalization: + +```twig +{{ 'hello world' | zany_caps }} +{# renders: #} +HeLlO WoRlD +``` + +First, write a `ThemeTwigHelper` and define your filter as an instance method. Inside the array returned by `get_filters()`, include the method as a `callable`: + +```php + function(string $input): string { + return strtoupper($input); + }, + 'no_caps' => function(string $input): string { + return strtolower($input); + }, + ]; + } + + public function get_filters() : array { + return [ + 'zany_caps' => [$this, 'zany_caps'], + ]; + } + + public function zany_caps(string $input) { + $chars = explode('', $input); + $zanyChars = array_map(function(string $char, int $i) { + // capitalize even letters; force odd letters to lowercase + return $i % 2 === 0 ? strtoupper($char) : strtolower($char); + }, $chars, array_keys($chars)); + // glue everything back together + return implode('', $zanyChars); + } +} +``` + +Now, instantiate and register your helper: + +```php +$site->add_twig_helper(new ThemeTwigHelper()); +``` + +The text being filtered is always the first argument to the callback, just like when you register a callback directly with `Twig_SimpleFilter`. + +Note that in this example, we didn't define any Twig functions, but to keep PHP happy, we still have to implement both public methods, where one simply returns an empty array. + +## Filters and Functions can be any callable + +Note that each callable returned in `get_filters()` or `get_functions()` can be *any* callable. It doesn't have to be an instance method! For example, say you wanted to use PHP's [`ltrim()`](http://us3.php.net/manual/en/function.ltrim.php) as a Twig filter: + +```twig +{{ 'asdfg' | ltrim('sad') }} +{# renders: #} +fg +``` + +Because the string `"ltrim"` is a callable, you can return it inside the result of `get_filters()`: + +```php +public function get_filters() : array { + return [ + 'ltrim' => 'ltrim', + ] +} +``` + +## Built-in Helpers + +Conifer comes with several built-in helpers defining various utility functions and filters. +These built-in helpers can change in-between releases, so please check the [GitHub repository](https://github.com/sitecrafting/conifer/tree/main/lib/Conifer/Twig) for the most up-to-date list. diff --git a/docs/twig.md b/docs/twig.md deleted file mode 100644 index f62a7af..0000000 --- a/docs/twig.md +++ /dev/null @@ -1,96 +0,0 @@ -# Twig Helpers - -Conifer defines the `Conifer\Twig\HelperInterface` for quickly defining custom Twig functions and filters. - -The interface is simple: - -```php -namespace Conifer\Twig; - -interface HelperInterface { - public function get_functions() : array; - public function get_filters() : array; -} -``` - -To register custom Twig helpers, just pass an instance of `HelperInterface` to the `Conifer\Site::add_twig_helper()` method: - -```php -$site->add_twig_helper(new MyTwigHelper()); -``` - -Each public method must return an array of callables, keyed by the function/filter name to be used in Twig templates. For example, say you want to define a `zany_caps()` Twig filter, for getting all zany with capitalization: - -```twig -{{ 'hello world' | zany_caps }} -{# renders: #} -HeLlO WoRlD -``` - -First, write a `CapsHelper` and define your filter as an instance method. Inside the array returned by `get_filters()`, include the method as a `callable`: - -```php -use Conifer\Twig\HelperInterface; - -class CapsHelper implements HelperInterface { - public function get_filters() : array { - return [ - 'zany_caps' => [$this, 'zany_caps'], - ]; - } - - public function zany_caps(string $text) { - $chars = explode('', $text); - $zanyChars = array_map(function(string $char, int $i) { - // capitalize even letters; force odd letters to lowercase - return $i % 2 === 0 ? strtoupper($char) : strtolower($char); - }, $chars, array_keys($chars)); - // glue everything back together - return implode('', $zanyChars); - } - - public function get_functions() : array { return []; } -} -``` - -Now, simply instantiate and register your helper: - -```php -$site->add_twig_helper(new CapsHelper()); -``` - -The text being filtered is always the first argument to the callback, just like when you register a callback directly with `Twig_SimpleFilter`. - -Note that in this example, we didn't define any Twig functions, but to keep PHP happy, we still have to implement both public methods, where one simply returns an empty array. - -## Filters and Functions can be any callable - -Note too that each callable returned in `get_filters()` or `get_functions()` can be *any* callable, as long as it operates on strings. It doesn't have to be an instance method! For example, say you wanted to use PHP's [`ltrim()`](http://us3.php.net/manual/en/function.ltrim.php) as a Twig filter: - -```twig -{{ 'asdfg' | ltrim('sad') }} -{# renders: #} -fg -``` - -Because the string `"ltrim"` is a callable, you can simply return it inside the result of `get_filters()`: - -```php - public function get_filters() : array { - return [ - 'ltrim' => 'ltrim', - ] - } -``` - -## Built-in Helpers - -Conifer comes with several built-in helpers defining various utility functions and filters. See the API reference for details about what they do: - -* `FormHelper` -* `ImageHelper` -* `NumberHelper` -* `TermHelper` -* `TextHelper` -* `WordPressHelper` - diff --git a/lib/Conifer/Admin/Notice.php b/lib/Conifer/Admin/Notice.php index 1e8b5c8..da26750 100644 --- a/lib/Conifer/Admin/Notice.php +++ b/lib/Conifer/Admin/Notice.php @@ -1,5 +1,4 @@ */ -declare(strict_types=1); - namespace Conifer\Admin; /** * Provides a high-level API for dislaying all sorts of WP Admin notices */ class Notice { - /** - * The session array key where flash notice data is stored - * - * @var string - */ - const FLASH_SESSION_KEY = 'conifer_admin_notices'; - - /** - * Whether flash notices are enabled. Default: false - */ - private static bool $flash_enabled = false; - - /** - * Classes to put on the notice
- * - * @var string - */ - protected $classes; - - /** - * Constructor - * - * @param string $message the message to display - * @param string $extraClasses any extra HTML class to display. - * Multiple classes can be specified with a space-separated string, e.g. - * `"one two three"` - */ - public function __construct(protected string $message, string $extraClasses = '' ) { - // clean up classes and convert to an array - $classes = array_map(trim(...), array_filter(explode(' ', $extraClasses))); - - $this->classes = array_unique(array_merge([ 'notice' ], $classes)); - } - - /** - * Clear all flash notices in session - */ - public static function clear_flash_notices(): void { - $_SESSION[static::FLASH_SESSION_KEY] = []; - } - - /** - * Enable flash notices to be stored in the `$_SESSION` superglobal - */ - public static function enable_flash_notices(): void { - self::$flash_enabled = true; - - add_action('admin_init', [ static::class, 'display_flash_notices' ]); - } - - /** - * Disable flash notices - */ - public static function disable_flash_notices(): void { - self::$flash_enabled = false; - } - - /** - * Whether flash notices are enabled - * - * @return bool - */ - public static function flash_notices_enabled(): bool { - return self::$flash_enabled; - } - - /** - * Display any flash notices stored in session during the admin_notices hook - */ - public static function display_flash_notices(): void { - if (!static::flash_notices_enabled()) { - return; - } - - foreach (static::get_flash_notices() as $notice) { - $notice->display(); - } - - static::clear_flash_notices(); - } - - /** - * Get the flash notices to be displayed based on session data - * - * @return Notice[] an array of Notice instances - */ - public static function get_flash_notices(): array { - if (!static::flash_notices_enabled()) { - return []; - } - - $sessionNotices = $_SESSION[static::FLASH_SESSION_KEY] ?? []; - if (empty($sessionNotices) || !is_array($sessionNotices)) { - return []; - } - - // filter out invalid notice data - $sessionNotices = array_filter($sessionNotices, fn($notice ): bool => static::valid_session_notice($notice), ARRAY_FILTER_USE_BOTH); - - return array_map(fn(array $notice ): self => new static($notice['message'], $notice['class'] ?? ''), $sessionNotices); - } - - /** - * Display the admin notice - * - * @see https://codex.wordpress.org/Plugin_API/Action_Reference/admin_notices - */ - public function display(): void { - add_action('admin_notices', function (): void { - // Because this class is designed to echo HTML, the user is responsible - // for ensuring the message doesn't contain any malicious markup. - // Class is already escaped. - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $this->html(); - }); + /** + * The session array key where flash notice data is stored + * + * @var string + */ + const FLASH_SESSION_KEY = 'conifer_admin_notices'; + + /** + * Whether flash notices are enabled. Default: false + * + * @var bool + */ + private static $flash_enabled = false; + + /** + * Classes to put on the notice
+ * + * @var string + */ + protected $classes; + + /** + * The message to display + * + * @var string + */ + protected $message; + + /** + * Constructor + * + * @param string $message the message to display + * @param string $extraClasses any extra HTML class to display. + * Multiple classes can be specified with a space-separated string, e.g. + * `"one two three"` + */ + public function __construct(string $message, string $extraClasses = '') { + $this->message = $message; + + // clean up classes and convert to an array + $classes = array_map('trim', array_filter(explode(' ', $extraClasses))); + + $this->classes = array_unique(array_merge(['notice'], $classes)); + } + + /** + * Clear all flash notices in session + */ + public static function clear_flash_notices() { + $_SESSION[static::FLASH_SESSION_KEY] = []; + } + + /** + * Enable flash notices to be stored in the `$_SESSION` superglobal + */ + public static function enable_flash_notices() { + self::$flash_enabled = true; + + add_action('admin_init', [static::class, 'display_flash_notices']); + } + + /** + * Disable flash notices + */ + public static function disable_flash_notices() { + self::$flash_enabled = false; + } + + /** + * Whether flash notices are enabled + * + * @return bool + */ + public static function flash_notices_enabled() : bool { + return self::$flash_enabled; + } + + /** + * Display any flash notices stored in session during the admin_notices hook + */ + public static function display_flash_notices() { + if (!static::flash_notices_enabled()) { + return; } - /** - * Display this notice as an error - */ - public function error(): void { - $this->add_class('notice-error'); - $this->display(); + foreach (static::get_flash_notices() as $notice) { + $notice->display(); } - /** - * Display this notice as a warning - */ - public function warning(): void { - $this->add_class('notice-warning'); - $this->display(); + static::clear_flash_notices(); + } + + /** + * Get the flash notices to be displayed based on session data + * + * @return Notice[] an array of Notice instances + */ + public static function get_flash_notices() : array { + if (!static::flash_notices_enabled()) { + return []; } - /** - * Display this notice as an info message - */ - public function info(): void { - $this->add_class('notice-info'); - $this->display(); + $sessionNotices = $_SESSION[static::FLASH_SESSION_KEY] ?? []; + if (empty($sessionNotices) || !is_array($sessionNotices)) { + return []; } - /** - * Display this notice as a success message - */ - public function success(): void { - $this->add_class('notice-success'); - $this->display(); + // filter out invalid notice data + $sessionNotices = array_filter($sessionNotices, function($notice, $idx) { + return static::valid_session_notice($notice); + }, ARRAY_FILTER_USE_BOTH); + + return array_map(function(array $notice) : self { + return new static($notice['message'], $notice['class'] ?? ''); + }, $sessionNotices); + } + + /** + * Display the admin notice + * + * @see https://codex.wordpress.org/Plugin_API/Action_Reference/admin_notices + */ + public function display() { + add_action('admin_notices', function() { + // Because this class is designed to echo HTML, the user is responsible + // for ensuring the message doesn't contain any malicious markup. + // Class is already escaped. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->html(); + }); + } + + /** + * Display this notice as an error + */ + public function error() { + $this->add_class('notice-error'); + $this->display(); + } + + /** + * Display this notice as a warning + */ + public function warning() { + $this->add_class('notice-warning'); + $this->display(); + } + + /** + * Display this notice as an info message + */ + public function info() { + $this->add_class('notice-info'); + $this->display(); + } + + /** + * Display this notice as a success message + */ + public function success() { + $this->add_class('notice-success'); + $this->display(); + } + + /** + * Display this notice as an error message on the next page load + */ + public function flash_error() { + $this->add_class('notice-error'); + $this->flash(); + } + + /** + * Display this notice as a warning on the next page load + */ + public function flash_warning() { + $this->add_class('notice-warning'); + $this->flash(); + } + + /** + * Display this notice as an info message on the next page load + */ + public function flash_info() { + $this->add_class('notice-info'); + $this->flash(); + } + + /** + * Display this notice as a success message on the next page load + */ + public function flash_success() { + $this->add_class('notice-success'); + $this->flash(); + } + + /** + * Display this notice on the next page load + */ + public function flash() { + // set up a handler for the admin_notices action, to ensure that any + // flash notices are added AFTER displaying notices for this request + add_action('admin_notices', function() { + $_SESSION[static::FLASH_SESSION_KEY] + = $_SESSION[static::FLASH_SESSION_KEY] ?? []; + + $_SESSION[static::FLASH_SESSION_KEY][] = [ + 'class' => $this->get_class(), + 'message' => $this->message, + ]; + }); + } + + /** + * Get the message `
` markup + * + * @return string the HTML to be rendered + */ + public function html() : string { + // default to error style + if (!$this->has_style_class()) { + $this->add_class('notice-error'); } - /** - * Display this notice as an error message on the next page load - */ - public function flash_error(): void { - $this->add_class('notice-error'); - $this->flash(); + return sprintf( + '

%s

', + esc_attr($this->get_class()), + $this->message + ); + } + + /** + * Add an HTML class to be rendered on this notice + * + * @param string $class the class to be added + * @return Notice + */ + public function add_class(string $class) : self { + if ($this->has_class($class)) { + // noop + return $this; } - /** - * Display this notice as a warning on the next page load - */ - public function flash_warning(): void { - $this->add_class('notice-warning'); - $this->flash(); + $this->classes[] = trim($class); + return $this; + } + + /** + * Get the HTML class or classes to be rendered in the notice markup + * + * @return string e.g. `"notice notice-error"` + */ + public function get_class() : string { + return trim(implode(' ', $this->classes)); + } + + /** + * Whether this notice has a special style class that WordPress targets + * in its built-in admin styles. + * + * @return bool + */ + public function has_style_class() : bool { + $styleClasses = [ + 'notice-error', + 'notice-warning', + 'notice-info', + 'notice-success', + ]; + + foreach ($styleClasses as $class) { + if ($this->has_class($class)) { + return true; + } } - /** - * Display this notice as an info message on the next page load - */ - public function flash_info(): void { - $this->add_class('notice-info'); - $this->flash(); - } - - /** - * Display this notice as a success message on the next page load - */ - public function flash_success(): void { - $this->add_class('notice-success'); - $this->flash(); - } - - /** - * Display this notice on the next page load - */ - public function flash(): void { - // set up a handler for the admin_notices action, to ensure that any - // flash notices are added AFTER displaying notices for this request - add_action('admin_notices', function (): void { - $_SESSION[static::FLASH_SESSION_KEY] ??= []; - - $_SESSION[static::FLASH_SESSION_KEY][] = [ - 'class' => $this->get_class(), - 'message' => $this->message, - ]; - }); - } - - /** - * Get the message `
` markup - * - * @return string the HTML to be rendered - */ - public function html(): string { - // default to error style - if (!$this->has_style_class()) { - $this->add_class('notice-error'); - } - - return sprintf( - '

%s

', - esc_attr($this->get_class()), - $this->message - ); - } - - /** - * Add an HTML class to be rendered on this notice - * - * @param string $class_name the class to be added - * @return Notice - */ - public function add_class(string $class_name ): self { - if ($this->has_class($class_name)) { - // noop - return $this; - } - - $this->classes[] = trim($class_name); - return $this; - } - - /** - * Get the HTML class or classes to be rendered in the notice markup - * - * @return string e.g. `"notice notice-error"` - */ - public function get_class(): string { - return trim(implode(' ', $this->classes)); - } - - /** - * Whether this notice has a special style class that WordPress targets - * in its built-in admin styles. - * - * @return bool - */ - public function has_style_class(): bool { - $styleClasses = [ - 'notice-error', - 'notice-warning', - 'notice-info', - 'notice-success', - ]; - - foreach ($styleClasses as $class) { - if ($this->has_class($class)) { - return true; - } - } - - return false; - } - - /** - * Whether this Notice has the given $class - * - * @param string $class_name - * @return bool - */ - public function has_class(string $class_name ): bool { - return in_array($class_name, $this->classes, true); - } - - - /** - * Validate a session notice array - * - * @param $notice - * @return bool - */ - protected static function valid_session_notice($notice ): bool { - return is_array($notice) - && !empty($notice['message']) - && is_string($notice['message']) - && (empty($notice['class']) || is_string($notice['class'])); - } + return false; + } + + /** + * Whether this Notice has the given $class + * + * @param bool + */ + public function has_class(string $class) : bool { + return in_array($class, $this->classes, true); + } + + + /** + * Validate a session notice array + * + * @return bool + */ + protected static function valid_session_notice($notice) : bool { + return is_array($notice) + && !empty($notice['message']) + && is_string($notice['message']) + && (empty($notice['class']) || is_string($notice['class'])); + } } diff --git a/lib/Conifer/Admin/Page.php b/lib/Conifer/Admin/Page.php index 850a9e6..352ecc6 100644 --- a/lib/Conifer/Admin/Page.php +++ b/lib/Conifer/Admin/Page.php @@ -7,8 +7,6 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Admin; /** @@ -29,221 +27,245 @@ * ``` */ abstract class Page { - /** - * The menu_title - */ - protected string $menu_title; - - /** - * The menu_slug - * - * @var string - */ - protected string $slug; - - /** - * Render the content of this admin Page. - * - * @param array $data optional view data for rendering in a specific context - * @return string - */ - abstract public function render(array $data = [] ): string; - - /** - * Constructor - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @param string $title the page_title for this page - * @param string $menuTitle the menu_title for this Page. - * Defaults to `$title` - * @param string $capability the capability required to view this Page. - * Defaults to `"manage_options"`. - * @param string $slug the menu_slug for this Page. - * Defaults to the sanitized `$menuTitle`. - * @param string $icon_url the icon_url for this Page - */ - public function __construct( - protected string $title, - string $menuTitle = '', - protected string $capability = 'manage_options', - string $slug = '', - protected string $icon_url = '' - ) { - $this->menu_title = !empty($menuTitle) ? $menuTitle : $this->title; - $this->slug = !empty($slug) ? $slug : sanitize_key($this->menu_title); - } - - /** - * Add this Admin Page to the admin main menu - * - * @return Page returns this Page - */ - public function add(): Page { - add_action('admin_menu', $this->do_add(...)); - - return $this; - } - - /** - * The callback to the `admin_menu` action. - */ - public function do_add(): Page { - $renderCallback = function (): void { - // NOTE: Since render() is specifically for outputting HTML in the admin - // area, users are responsible for escaping their own output accordingly. + /** + * The page_title + * + * @var string + */ + protected $title; + + /** + * The menu_title + * + * @var string + */ + protected $menu_title; + + /** + * The capability + * + * @var string + */ + protected $capability; + + /** + * The menu_slug + * + * @var string + */ + protected $slug; + + /** + * The icon_url + * + * @var string + */ + protected $icon_url; + + /** + * Render the content of this admin Page. + * + * @param array $data optional view data for rendering in a specific context + * @return string + */ + abstract public function render(array $data = []) : string; + + /** + * Constructor + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @param string $title the page_title for this page + * @param string $menuTitle the menu_title for this Page. + * Defaults to `$title` + * @param string $capability the capability required to view this Page. + * Defaults to `"manage_options"`. + * @param string $slug the menu_slug for this Page. + * Defaults to the sanitized `$menuTitle`. + * @param string $iconUrl the icon_url for this Page + */ + public function __construct( + string $title, + string $menuTitle = '', + string $capability = 'manage_options', + string $slug = '', + string $iconUrl = '' + ) { + $this->title = $title; + $this->menu_title = $menuTitle ?: $title; + $this->capability = $capability; + $this->slug = $slug ?: sanitize_key($this->menu_title); + $this->icon_url = $iconUrl; + } + + /** + * Add this Admin Page to the admin main menu + * + * @return Page returns this Page + */ + public function add() : Page { + add_action('admin_menu', [$this, 'do_add']); + + return $this; + } + + /** + * The callback to the `admin_menu` action. + */ + public function do_add(): void { + $renderCallback = function() { + // NOTE: Since render() is specifically for outputting HTML in the admin + // area, users are responsible for escaping their own output accordingly. // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $this->render([ 'slug' => $this->slug ]); - }; - - add_menu_page( - $this->title, - $this->menu_title, - $this->capability, - $this->slug, - $renderCallback, - $this->icon_url - ); - - return $this; - } - - /** - * Add a sub-menu admin page to this Page. - * - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @param string $class_name the `SubPage` child class to instantiate to - * represent the new sub-page. - * @param string $title the page_title for the sub-page - * @param string $menuTitle the menu_title for the sub-page. - * Defaults to `$title`. - * @param string $capability the capability required for viewing the sub-page. - * Defaults to the required capability for this Page. - * @param string $slug the menu_slug for the sub-page. - * Defaults to the sanitized `$menuTitle`. - * @return Page returns this Page. - */ - public function add_sub_page( - string $class_name, - string $title, - string $menuTitle = '', - string $capability = '', - string $slug = '' - ): self { - $page = new $class_name($this, $title, $menuTitle, $capability, $slug); - $page->add(); - - return $this; - } - - /** - * Get the `page_title` to be passed to WP when this Page is added. - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @return string - */ - public function get_title(): string { - return $this->title; - } - - /** - * Get the `menu_title` to be passed to WP when this Page is added. - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @return string - */ - public function get_menu_title(): string { - return $this->menu_title; - } - - /** - * Get the `capability` to be passed to WP when this Page is added. - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @return string - */ - public function get_capability(): string { - return $this->capability; - } - - /** - * Get the `menu_slug` to be passed to WP when this Page is added. - * When adding sub-pages, this is what is passed as `parent_slug` - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @return string - */ - public function get_slug(): string { - return $this->slug; - } - - /** - * Get the `icon_url` to be passed to WP when this Page is added. - * - * @see https://developer.wordpress.org/reference/functions/add_menu_page/ - * @return string - */ - public function get_icon_url(): string { - return $this->icon_url; - } - - /** - * Set the slug for this Admin Page. - * - * @param string $slug the menu_slug for this Page - * @return Page returns this Page object - */ - public function set_slug(string $slug ): Page { - $this->slug = $slug; - return $this; - } - - /** - * Set the menu_title for this Admin Page. - * - * @param string $menuTitle the title to display in the Admin menu - * @return Page returns this Page object - */ - public function set_menu_title(string $menuTitle ): Page { - $this->menu_title = $menuTitle; - return $this; - } - - /** - * Set the capability required to view this Admin Page. - * - * @param string $capability the WP capability string, e.g. "edit_posts" - * @return Page returns this Page object - */ - public function set_capability(string $capability ): Page { - $this->capability = $capability; - return $this; - } - - /** - * Set the title for this Admin Page. - * - * @param string $title the element text to display on this Admin Page - * @return Page returns this Page object - */ - public function set_title(string $title ): Page { - $this->title = $title; - return $this; - } - - /** - * Set the icon_url for this Admin Page. - * - * @param string $url the icon_url to be displayed in the Menu for this - * Admin Page. - * - * @return Page returns this Page object - */ - public function set_icon_url(string $url ): Page { - $this->icon_url = $url; - return $this; - } + echo $this->render([ 'slug' => $this->slug ]); + }; + + add_menu_page( + $this->title, + $this->menu_title, + $this->capability, + $this->slug, + $renderCallback, + $this->icon_url + ); + } + + /** + * Add a sub-menu admin page to this Page. + * + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @param string $class the `SubPage` child class to instantiate to + * represent the new sub-page. + * @param string $title the page_title for the sub-page + * @param string $menuTitle the menu_title for the sub-page. + * Defaults to `$title`. + * @param string $capability the capability required for viewing the sub-page. + * Defaults to the required capability for this Page. + * @param string $slug the menu_slug for the sub-page. + * Defaults to the sanitized `$menuTitle`. + * @return Page returns this Page. + */ + public function add_sub_page( + string $class, + string $title, + string $menuTitle = '', + string $capability = '', + string $slug = '' + ) : self { + $page = new $class($this, $title, $menuTitle, $capability, $slug); + $page->add(); + + return $this; + } + + /** + * Get the `page_title` to be passed to WP when this Page is added. + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @return string + */ + public function get_title() : string { + return $this->title; + } + + /** + * Get the `menu_title` to be passed to WP when this Page is added. + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @return string + */ + public function get_menu_title() : string { + return $this->menu_title; + } + + /** + * Get the `capability` to be passed to WP when this Page is added. + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @return string + */ + public function get_capability() : string { + return $this->capability; + } + + /** + * Get the `menu_slug` to be passed to WP when this Page is added. + * When adding sub-pages, this is what is passed as `parent_slug` + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @return string + */ + public function get_slug() : string { + return $this->slug; + } + + /** + * Get the `icon_url` to be passed to WP when this Page is added. + * + * @see https://developer.wordpress.org/reference/functions/add_menu_page/ + * @return string + */ + public function get_icon_url() : string { + return $this->icon_url; + } + + /** + * Set the slug for this Admin Page. + * + * @param string $slug the menu_slug for this Page + * @return Page returns this Page object + */ + public function set_slug(string $slug) : Page { + $this->slug = $slug; + return $this; + } + + /** + * Set the menu_title for this Admin Page. + * + * @param string $menuTitle the title to display in the Admin menu + * @return Page returns this Page object + */ + public function set_menu_title(string $menuTitle) : Page { + $this->menu_title = $menuTitle; + return $this; + } + + /** + * Set the capability required to view this Admin Page. + * + * @param string $capability the WP capability string, e.g. "edit_posts" + * @return Page returns this Page object + */ + public function set_capability(string $capability) : Page { + $this->capability = $capability; + return $this; + } + + /** + * Set the title for this Admin Page. + * + * @param string $title the <title> element text to display on this Admin Page + * @return Page returns this Page object + */ + public function set_title(string $title) : Page { + $this->title = $title; + return $this; + } + + /** + * Set the icon_url for this Admin Page. + * + * @param string $url the icon_url to be displayed in the Menu for this + * Admin Page. + * + * @return Page returns this Page object + */ + public function set_icon_url(string $url) : Page { + $this->icon_url = $url; + return $this; + } } diff --git a/lib/Conifer/Admin/SubPage.php b/lib/Conifer/Admin/SubPage.php index 11ad8d8..618fb87 100644 --- a/lib/Conifer/Admin/SubPage.php +++ b/lib/Conifer/Admin/SubPage.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Admin; /** @@ -45,65 +43,72 @@ * ``` */ abstract class SubPage extends Page { - /** - * Constructor - * - * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ - * @param Page $parent_page the parent Admin Page, which provides the `parent_slug` - * for this submenu page. - * @param string $title the `page_title` to use as the <title> element text - * on this SubPage. - * @param string $menuTitle the title to display for this SubPage in the - * admin menu. - * @param string $capability the WP capability required to view this SubPage. - * Defaults to the capabaility set on the parent AdminPage. - * @param string $slug the `menu_slug` for this SubPage. - */ - public function __construct( - protected Page $parent_page, - string $title, - string $menuTitle = '', - string $capability = '', - string $slug = '' - ) { - parent::__construct( - $title, - $menuTitle, - !empty($capability) ? $capability : $this->parent_page->get_capability(), - $slug - ); - } + /** + * The parent Page + * + * @var Page + */ + protected $parent; - /** - * Add this SubPage to the WP Admin menu - * - * @return Page returns this SubPage - */ - public function add(): Page { - add_action('admin_menu', $this->do_add(...)); - return $this; - } + /** + * Constructor + * + * @see https://developer.wordpress.org/reference/functions/add_submenu_page/ + * @param Page $parent the parent Admin Page, which provides the `parent_slug` + * for this submenu page. + * @param string $title the `page_title` to use as the <title> element text + * on this SubPage. + * @param string $menuTitle the title to display for this SubPage in the + * admin menu. + * @param string $capability the WP capability required to view this SubPage. + * Defaults to the capabaility set on the parent AdminPage. + * @param string $slug the `menu_slug` for this SubPage. + */ + public function __construct( + Page $parent, + string $title, + string $menuTitle = '', + string $capability = '', + string $slug = '' + ) { + $this->parent = $parent; - /** - * The callback to the `admin_menu` action. - */ - public function do_add(): Page { - $renderCallback = function (): void { - // NOTE: Since render() is specifically for outputting HTML in the admin - // area, users are responsible for escaping their own output accordingly. - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $this->render([ 'slug' => $this->slug ]); - }; + parent::__construct( + $title, + $menuTitle, + $capability ?: $parent->get_capability(), + $slug + ); + } - add_submenu_page( - $this->parent_page->get_slug(), - $this->title, - $this->menu_title, - $this->capability, - $this->slug, - $renderCallback - ); + /** + * Add this SubPage to the WP Admin menu + * + * @return Page returns this SubPage + */ + public function add() : Page { + add_action('admin_menu', [$this, 'do_add']); + return $this; + } + + /** + * The callback to the `admin_menu` action. + */ + public function do_add(): void { + $renderCallback = function() { + // NOTE: Since render() is specifically for outputting HTML in the admin + // area, users are responsible for escaping their own output accordingly. + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $this->render([ 'slug' => $this->slug ]); + }; - return $this; - } + add_submenu_page( + $this->parent->get_slug(), + $this->title, + $this->menu_title, + $this->capability, + $this->slug, + $renderCallback + ); + } } diff --git a/lib/Conifer/AjaxHandler/AbstractBase.php b/lib/Conifer/AjaxHandler/AbstractBase.php index 9085fd5..1ce9a04 100644 --- a/lib/Conifer/AjaxHandler/AbstractBase.php +++ b/lib/Conifer/AjaxHandler/AbstractBase.php @@ -1,5 +1,4 @@ <?php - /** * Class to encapsulate handling AJAX calls. Handling a WP AJAX action * requires only implementing a child class with an execute() method, @@ -72,8 +71,6 @@ * @package Conifer */ -declare(strict_types=1); - namespace Conifer\AjaxHandler; use BadMethodCallException; @@ -83,168 +80,170 @@ // TODO: Need to address logging for AJAX requests. Stripped logging out for now. See #25 // TODO: Need to add nonce verification to this class and update any related documentation to reflect this requirement abstract class AbstractBase { - /** - * The request array for this AJAX request (either POST or GET) - * - * @var array - */ - protected array $request; - - /** - * The $_COOKIE array for this AJAX request - */ - protected array $cookie; - - /** - * The name of the AJAX action being requested - */ - protected string $action; - - /** - * Associative array which maps an action to the method name used to handle that action - */ - protected array $action_methods; - - /** - * Abstract method used to define the functionality when handling an AJAX request. - * Should return an array to be encoded in the response. - * - * @return array The response after handling the request - */ - abstract protected function execute(): array; - - - /* - * Static handler methods - */ - - /** - * Handle an HTTP request. - * - * @param ?array $requestData The request data (`$_GET`, `$_POST`, etc). Defaults to $_REQUEST. - */ - public static function handle(?array $requestData = null ): void { + /** + * The request array for this AJAX request (either POST or GET) + * + * @var array + */ + protected $request; + /** + * The $_COOKIE array for this AJAX request + * + * @var array + */ + protected $cookie; + /** + * The name of the AJAX action being requested + * + * @var string + */ + protected $action; + /** + * Associative array which maps an action to the method name used to handle that action + * + * @var array + */ + protected $action_methods; + + /** + * Abstract method used to define the functionality when handling an AJAX request. + * Should return an array to be encoded in the response. + * + * @return array The response after handling the request + */ + abstract protected function execute() : array; + + + /* + * Static handler methods + */ + + /** + * Handle an HTTP request. + */ + public static function handle(?array $requestData = null) { // phpcs:ignore WordPress.Security.NonceVerification.Recommended - $handler = new static($requestData ?? $_REQUEST); - $handler->set_cookie($_COOKIE); - $handler->send_json_response($handler->execute()); - } - - /** - * Handle an HTTP POST request. - */ - public static function handle_post(): void { - static::handle($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing - } - - /** - * Handle an HTTP GET request. - */ - public static function handle_get(): void { - static::handle($_GET); // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification - } - - - /* - * Instance Methods - */ - /** - * Constructor. - * TODO decide whether to require Monolog?? - * - * @param array<string, mixed> $request the raw request params, i.e. GET/POST - * @throws LogicException If the request array doesn't contain an action - */ - public function __construct(array $request ) { - if (empty($request['action'])) { - throw new LogicException( - 'Trying to handle an AJAX call without an action! The "action" request parameter is required.' - ); - } - - $this->action = $request['action']; - $this->request = $request; - $this->action_methods = []; - } - - /** - * Send $response as a JSON HTTP response and close the connection. - * - * @param array $response the response to be converted to JSON - */ - protected function send_json_response(array $response ): void { - wp_send_json($response); + $handler = new static($requestData ?? $_REQUEST); + $handler->set_cookie($_COOKIE); + $handler->send_json_response($handler->execute()); + } + + /** + * Handle an HTTP POST request. + */ + public static function handle_post() { + static::handle($_POST); // phpcs:ignore WordPress.Security.NonceVerification.Missing + } + + /** + * Handle an HTTP GET request. + */ + public static function handle_get() { + static::handle($_GET); // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification + } + + + /* + * Instance Methods + */ + + /** + * Constructor. + * TODO decide whether to require Monolog?? + * + * @param array $request the raw request params, i.e. GET/POST + * @throws LogicException If the request array doesn't contain an action + */ + public function __construct(array $request) { + if (empty($request['action'])) { + throw new LogicException( + 'Trying to handle an AJAX call without an action! The "action" request parameter is required.' + ); } - /** - * Get a param value from the request - * - * @param mixed $name the key of the value to get from the request - * @return mixed the value for the request param. Defaults to the empty string if not set. - */ - protected function param(mixed $name ): mixed { - return $this->request[$name] ?? ''; + $this->action = $request['action']; + $this->request = $request; + $this->action_methods = []; + } + + /** + * Send $response as a JSON HTTP response and close the connection. + * + * @param array $response the response to be converted to JSON + */ + protected function send_json_response(array $response) { + wp_send_json($response); + } + + /** + * Get a param value from the request + * + * @param mixed $name the key of the value to get from the request + * @return mixed the value for the request param. Defaults to the empty string if not set. + */ + protected function param($name) { + return isset($this->request[$name]) ? $this->request[$name] : ''; + } + + /** + * Get a param value from the cookie + * + * @param mixed $name the key of the value to get from the cookie + * @return mixed the value for the cookie param. Defaults to the empty string if not set. + */ + protected function cookie($name) { + return isset($this->cookie[$name]) ? $this->cookie[$name] : ''; + } + + /** + * Dispatch a handler method dynamically based on the requested action + * + * @return mixed $response the result of calling the corrsponding *_action method. + * This **should** be an array. + * @throws LogicException If the specified action doesn't exist in the action_methods array + * @throws BadMethodCallException If the action method doesn't exist or isn't an instance method + */ + protected function dispatch_action() { + // check that a handler is configure for the current action + if (empty($this->action_methods[$this->action])) { + throw new LogicException("No handler method specified for action: {$this->action}!"); } - /** - * Get a param value from the cookie - * - * @param mixed $name the key of the value to get from the cookie - * @return mixed the value for the cookie param. Defaults to the empty string if not set. - */ - protected function cookie(mixed $name ): mixed { - return $this->cookie[$name] ?? ''; + // check that the handler method has been implemented + $method = $this->action_methods[$this->action]; + $reflection = new ReflectionClass($this); + if (!$reflection->hasMethod($method)) { + throw new BadMethodCallException("Method `{$method}` for action {$this->action} has not been implemented!"); } - /** - * Dispatch a handler method dynamically based on the requested action - * - * @return mixed $response the result of calling the corresponding *_action method. - * This **should** be an array. - * @throws LogicException If the specified action doesn't exist in the action_methods array - * @throws BadMethodCallException If the action method doesn't exist or isn't an instance method - */ - protected function dispatch_action(): mixed { - // check that a handler is configure for the current action - if (empty($this->action_methods[$this->action])) { - throw new LogicException(sprintf('No handler method specified for action: %s!', $this->action)); - } - - // check that the handler method has been implemented - $method = $this->action_methods[$this->action]; - $reflection = new ReflectionClass($this); - if (!$reflection->hasMethod($method)) { - throw new BadMethodCallException(sprintf('Method `%s` for action %s has not been implemented!', $method, $this->action)); - } - - // check that we're calling an instance method - if ($reflection->getMethod($method)->isStatic()) { - throw new BadMethodCallException(sprintf('Method %s for action %s must not be static!', $method, $this->action)); - } - - return $this->{$method}(); + // check that we're calling an instance method + if ($reflection->getMethod($method)->isStatic()) { + throw new BadMethodCallException("Method {$method} for action {$this->action} must not be static!"); } - /** - * Adds the specified action name to the action_methods array as a key, - * with the specified method name as the value. Used to determine the method - * user to handle the specified action. - * - * @param string $action The name of the action to be mapper to a method name - * @param string $methodName The name of a method name to be used when handling thins action - * @return AbstractBase The current AbstractBase class instance - */ - protected function map_action(string $action, string $methodName ): static { - $this->action_methods[$action] = $methodName; - return $this; - } - - /** - * Saves the request cookie array to this AbstractBase handler instance - * - * @param array $cookie The request cookie array - */ - private function set_cookie(array $cookie ): void { - $this->cookie = $cookie; - } + return $this->{$method}(); + } + + /** + * Adds the specified action name to the action_methods array as a key, + * with the specified method name as the value. Used to determine the method + * user to handle the specified action. + * + * @param string $action The name of the action to be mapper to a method name + * @param string $methodName The name of a method name to be used when handling thins action + * @return Conifer\AjaxHandler\AbstractBase The current AbstractBase class instance + */ + protected function map_action($action, $methodName) { + $this->action_methods[$action] = $methodName; + return $this; + } + + /** + * Saves the request cookie array to this AbstractBase handler instance + * + * @param array $cookie The request cookie array + */ + private function set_cookie(array $cookie) { + $this->cookie = $cookie; + } } diff --git a/lib/Conifer/Alert/DismissableAlert.php b/lib/Conifer/Alert/DismissableAlert.php index af8ef78..9b33333 100644 --- a/lib/Conifer/Alert/DismissableAlert.php +++ b/lib/Conifer/Alert/DismissableAlert.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Alert; /** @@ -16,118 +14,131 @@ * persistence for tracking dismissals across requests */ class DismissableAlert { - /** - * Constructor. Takes the Alert message as a string. - * Takes an optional array of options. - * - * @param string $message Alert message text - * @param array $options an array with any of the following keys: - * - * - 'cookies': (array) The $_COOKIE superglobal (to allow for filtering) - * - 'cookie_prefix': (string) The prefix for the cookie that indicates - * whether this Alert has been dismissed. This is appended to a hash of - * the message text to guaranteed uniqueness. Defaults to: - * "wp-user_dismissed_alert_" - * Note that on certain hosting - * platforms, notably Pantheon, cookies may be filtered out by the edge - * cache based on this prefix. Pantheon *does not* filter out cookies - * starting with `wp-` or `wordpress_`, so it's a good idea to start your - * prefix with one of these. See below for more details. - * - 'cookie_expires': UTC datetime when cookie should expire. Defaults to - * UTC-formatted string of one year from now. - * - 'cookie_path': the path to set for the cookie. Defaults to "/" - * - * @see https://pantheon.io/docs/cookies - */ - public function __construct( - protected string $message, - protected array $options = [] - ) { - } + /** + * The Alert message + * + * @var string + */ + protected $message; - /** - * The full text of the cookie. This is handy for setting a cookie in - * JavaScript via `document.cookie = "..."` - * - * @return string - */ - public function cookie_text(): string { - return sprintf( - '%s=1; expires=%s; path=%s', - $this->cookie_name(), - $this->cookie_expires(), - $this->cookie_path() - ); - } + /** + * Configurable options to the constructor + * + * @var array + */ + protected $options; - /** - * Returns an identifier unique to the Alert message - * - * @return string - */ - public function cookie_name(): string { - return $this->cookie_prefix() . md5($this->message); - } + /** + * Constructor. Takes the Alert message as a string. + * Takes an optional array of options. + * + * @param string $message Alert message text + * @param array $options an array with any of the following keys: + * + * - 'cookies': (array) The $_COOKIE superglobal (to allow for filtering) + * - 'cookie_prefix': (string) The prefix for the cookie that indicates + * whether this Alert has been dismissed. This is appended to a hash of + * the message text to guaranteed uniqueness. Defaults to: + * "wp-user_dismissed_alert_" + * Note that on certain hosting + * platforms, notably Pantheon, cookies may be filtered out by the edge + * cache based on this prefix. Pantheon *does not* filter out cookies + * starting with `wp-` or `wordpress_`, so it's a good idea to start your + * prefix with one of these. See below for more details. + * - 'cookie_expires': UTC datetime when cookie should expire. Defaults to + * UTC-formatted string of one year from now. + * - 'cookie_path': the path to set for the cookie. Defaults to "/" + * + * @see https://pantheon.io/docs/cookies + */ + public function __construct(string $message, array $options = []) { + $this->message = $message; + $this->options = $options; + } - /** - * Get the cookie expiration date/time. This is only used in setting the - * cookies on dismissal. - * - * @return string - */ - public function cookie_expires(): string { - if (!empty($this->options['cookie_expires'])) { - return $this->options['cookie_expires']; - } + /** + * The full text of the cookie. This is handy for setting a cookie in + * JavaScript via `document.cookie = "..."` + * + * @return string + */ + public function cookie_text() : string { + return sprintf( + '%s=1; expires=%s; path=%s', + $this->cookie_name(), + $this->cookie_expires(), + $this->cookie_path() + ); + } - $now = new \DateTime('now', new \DateTimeZone('UTC')); - $now->modify('+1 year'); - return $now->format('r'); - } + /** + * Returns an identifier unique to the Alert message + * + * @return string + */ + public function cookie_name() : string { + return $this->cookie_prefix() . md5($this->message); + } - /** - * Get the path to set for the cookie. Defaults to "/" - * - * @return string - */ - public function cookie_path(): string { - return $this->options['cookie_path'] ?? '/'; + /** + * Get the cookie expiration date/time. This is only used in setting the + * cookies on dismissal. + * + * @return string + */ + public function cookie_expires() : string { + if (!empty($this->options['cookie_expires'])) { + return $this->options['cookie_expires']; } - /** - * Get the Alert message - * - * @return string - */ - public function message(): string { - return $this->message; - } + $now = new \DateTime('now', new \DateTimeZone('UTC')); + $now->modify('+1 year'); + return $now->format('r'); + } - /** - * Whether this Alert has been dismissed (based on the user's cookies) - * - * @return bool - */ - public function dismissed(): bool { - return !empty($this->cookies()[$this->cookie_name()]); - } + /** + * Get the path to set for the cookie. Defaults to "/" + * + * @return string + */ + public function cookie_path() : string { + return $this->options['cookie_path'] ?? '/'; + } + /** + * Get the Alert message + * + * @return string + */ + public function message() : string { + return $this->message; + } - /** - * Get the cookies from options or the $_COOKIE superglobal - * - * @return array - */ - protected function cookies(): array { - return (array) ($this->options['cookies'] ?? $_COOKIE); - } + /** + * Whether this Alert has been dismissed (based on the user's cookies) + * + * @return bool + */ + public function dismissed() : bool { + return !empty($this->cookies()[$this->cookie_name()]); + } - /** - * String that the cookie should start with - * - * @return string - */ - protected function cookie_prefix(): string { - return $this->options['cookie_prefix'] ?? 'wp-user_dismissed_alert_'; - } + + /** + * Get the cookies from options or the $_COOKIE superglobal + * + * @return array + */ + protected function cookies() : array { + return (array) ($this->options['cookies'] ?? $_COOKIE); + } + + /** + * String that the cookie should start with + * + * @return string + */ + protected function cookie_prefix() : string { + return $this->options['cookie_prefix'] ?? 'wp-user_dismissed_alert_'; + } } diff --git a/lib/Conifer/Authorization/AbstractPolicy.php b/lib/Conifer/Authorization/AbstractPolicy.php index 396b0f6..ff1ca73 100644 --- a/lib/Conifer/Authorization/AbstractPolicy.php +++ b/lib/Conifer/Authorization/AbstractPolicy.php @@ -1,5 +1,4 @@ <?php - /** * AbstractPolicy class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Authorization; /** @@ -16,10 +13,10 @@ * authorization logic */ abstract class AbstractPolicy implements PolicyInterface { - /** - * Create and adopt a new instance - */ - public static function register(): PolicyInterface { - return (new static())->adopt(); - } + /** + * Create and adopt a new instance + */ + public static function register() : PolicyInterface { + return (new static())->adopt(); + } } diff --git a/lib/Conifer/Authorization/PolicyInterface.php b/lib/Conifer/Authorization/PolicyInterface.php index 1695376..8005c0b 100644 --- a/lib/Conifer/Authorization/PolicyInterface.php +++ b/lib/Conifer/Authorization/PolicyInterface.php @@ -1,5 +1,4 @@ <?php - /** * PolicyInterface * @@ -7,21 +6,19 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Authorization; /** * Interface for a high-level authorization API */ interface PolicyInterface { - /** - * Put this policy in place, typically via an action or filter - */ - public function adopt(): self; + /** + * Put this policy in place, typically via an action or filter + */ + public function adopt() : self; - /** - * Create and adopt a new instance of this interface - */ - public static function register(): self; + /** + * Create and adopt a new instance of this interface + */ + public static function register() : self; } diff --git a/lib/Conifer/Authorization/ShortcodePolicy.php b/lib/Conifer/Authorization/ShortcodePolicy.php index 368bd55..a62022c 100644 --- a/lib/Conifer/Authorization/ShortcodePolicy.php +++ b/lib/Conifer/Authorization/ShortcodePolicy.php @@ -1,5 +1,4 @@ <?php - /** * ShortcodePolicy class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Authorization; use Timber\Timber; @@ -20,99 +17,112 @@ */ abstract class ShortcodePolicy extends AbstractPolicy { - /** - * Sets the shortcode tag for the new shortcode policy - * - * @param string $tag - */ - public function __construct(protected string $tag = 'protected' ) { - } - - /** - * Filter the shortcode content based on the implementation of the `decide` - * method. - * - * @return PolicyInterface fluent interface - */ - public function adopt(): PolicyInterface { - add_shortcode($this->tag(), fn(array $atts, string $content = '' ): string => $this->enforce($atts, $content, $this->get_user())); - - return $this; - } - - - /** - * Determine whether the user has access to content based on shortcode - * attributes, user data, and possibly the content itself. - * - * @param array $atts the shortcode attributes - * @param string $content the shortcode content - * @param \Timber\User $user the user to check against - * @return bool whether `$user` meets the criteria described in `$atts` - */ - abstract public function decide( - array $atts, - string $content, - User $user - ): bool; - - /** - * Get the shortcode tag to be declared - * - * @see https://codex.wordpress.org/Function_Reference/add_shortcode - * @return string the shortcode tag to declare - */ - protected function tag(): string { - return $this->tag; - } - - /** - * Filter the shortcode content based on the current user's data - * - * @param string $template the template file being loaded - * @param \Timber\User the User whose privileges we want to check - */ - public function enforce( - array $atts, - string $content, - User $user - ): string { - $authorized = $this->decide($atts, $content, $user); - - return $authorized - ? $this->filter_authorized($content) - : $this->filter_unauthorized($content); - } - - - /** - * Get the user to check against shortcode attributes. - * Override this method to perform authorization against someone other - * than the current user. - * - * @return \Timber\User - */ - protected function get_user(): User { - return Timber::get_user(); - } - - /** - * Get the filtered shortcode content to display to unauthorized users. - * Override this method to display something other than the empty string. - * - * @return string the content to display - */ - protected function filter_unauthorized(string $content ): string { - return ''; - } - - /** - * Get the filtered shortcode content to display to _authorized_ users. - * Override this method to display something other thatn the original content. - * - * @return string the content to display - */ - protected function filter_authorized(string $content ): string { - return $content; - } + /** + * The shortcode tag + * + * @var string + */ + protected $tag; + + /** + * Sets the shortcode tag for the new shortcode policy + * + * @param string $tag + */ + public function __construct(string $tag = 'protected') { + $this->tag = $tag; + } + + /** + * Filter the shortcode content based on the implementation of the `decide` + * method. + * + * @return PolicyInterface fluent interface + */ + public function adopt() : PolicyInterface { + add_shortcode($this->tag(), function( + array $atts, + string $content = '' + ) : string { + return $this->enforce($atts, $content, $this->get_user()); + }); + + return $this; + } + + + /** + * Determine whether the user has access to content based on shortcode + * attributes, user data, and possibly the content itself. + * + * @param array $atts the shortcode attributes + * @param string $content the shortcode content + * @param \Timber\User $user the user to check against + * @return bool whether `$user` meets the criteria described in `$atts` + */ + abstract public function decide( + array $atts, + string $content, + User $user + ) : bool; + + /** + * Get the shortcode tag to be declared + * + * @see https://codex.wordpress.org/Function_Reference/add_shortcode + * @return string the shortcode tag to declare + */ + protected function tag() : string { + return $this->tag; + } + + /** + * Filter the shortcode content based on the current user's data + * + * @param string $template the template file being loaded + * @param \Timber\User the User whose privileges we want to check + */ + public function enforce( + array $atts, + string $content, + User $user + ) : string { + $authorized = $this->decide($atts, $content, $user); + + return $authorized + ? $this->filter_authorized($content) + : $this->filter_unauthorized($content); + } + + + /** + * Get the user to check against shortcode attributes. + * Override this method to perform authorization against someone other + * than the current user. + * + * @return \Timber\User + */ + protected function get_user() : User { + return Timber::get_user(); + } + + /** + * Get the filtered shortcode content to display to unauthorized users. + * Override this method to display something other than the empty string. + * + * @return string the content to display + */ + protected function filter_unauthorized(string $content) : string { + return ''; + } + + /** + * Get the filtered shortcode content to display to _authorized_ users. + * Override this method to display something other thatn the original content. + * + * @return string the content to display + */ + protected function filter_authorized(string $content) : string { + return $content; + } } diff --git a/lib/Conifer/Authorization/TemplatePolicy.php b/lib/Conifer/Authorization/TemplatePolicy.php index 779eaac..a76fa1a 100644 --- a/lib/Conifer/Authorization/TemplatePolicy.php +++ b/lib/Conifer/Authorization/TemplatePolicy.php @@ -1,5 +1,4 @@ <?php - /** * TemplatePolicy class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Authorization; use Timber\Timber; @@ -19,27 +16,25 @@ * authorization logic */ abstract class TemplatePolicy extends AbstractPolicy { - /** - * Adopt this policy - * - * If `$this->enforce()` redirects for any reason, then the $template passed in won't be returned. - * - * @return PolicyInterface fluent interface - */ - public function adopt(): PolicyInterface { - add_filter('template_include', function (string $template ): string { - $this->enforce($template, Timber::get_user()); - return $template; - }); + /** + * Adopt this policy + * + * @return PolicyInterface fluent interface + */ + public function adopt() : PolicyInterface { + add_filter('template_include', function(string $template) { + $this->enforce($template, Timber::get_user()); + return $template; + }); - return $this; - } + return $this; + } - /** - * Enforce this template-level policy - * - * @param string $template the template file being loaded - * @param User $user the User whose privileges we want to check - */ - abstract public function enforce(string $template, User $user ); + /** + * Enforce this template-level policy + * + * @param string $template the template file being loaded + * @param \Timber\User the User whose privileges we want to check + */ + abstract public function enforce(string $template, User $user); } diff --git a/lib/Conifer/Authorization/UserRoleShortcodePolicy.php b/lib/Conifer/Authorization/UserRoleShortcodePolicy.php index e052040..af18e4f 100644 --- a/lib/Conifer/Authorization/UserRoleShortcodePolicy.php +++ b/lib/Conifer/Authorization/UserRoleShortcodePolicy.php @@ -1,5 +1,4 @@ <?php - /** * UserRoleShortcodePolicy class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Authorization; use Timber\User; @@ -17,29 +14,34 @@ * A ShortcodePolicy that filters content based on the current user's role */ class UserRoleShortcodePolicy extends ShortcodePolicy { - /** - * Check whether the user's role matches up with the required role - * declared in the shortcode - * - * @inheritdoc - */ - public function decide( - array $atts, - string $content, - User $user - ): bool { - // Parse the role[s] attribute to determine which roles are authorized - $roleAttr = $atts['role'] ?? $atts['roles'] ?? 'administrator'; - $authorizedRoles = array_map(trim(...), explode(',', $roleAttr)); - - // Get the user's roles for comparison - // WP returns user roles in an idiosyncratic way: role names are keys and - // `true` values means the user has that role. We just want to flatten - // this to a simple array of role/capability strings - // If the user is not logged in and has no roles the users wp_capabilities returns false and we want an empty array - $userRoles = $user->meta('wp_capabilities') === false ? [] : array_keys(array_filter($user->meta('wp_capabilities'))); + /** + * Check whether the user's role matches up with the required role + * declared in the shortcode + * + * @inheritdoc + */ + public function decide( + array $atts, + string $content, + User $user + ) : bool { + // Parse the role[s] attribute to determine which roles are authorized + $roleAttr = $atts['role'] ?? $atts['roles'] ?? 'administrator'; + $authorizedRoles = array_map('trim', explode(',', $roleAttr)); - // Make sure the user has at least one authorized role - return array_intersect($authorizedRoles, $userRoles) !== []; + // Get the user's roles for comparison + // WP returns user roles in an idiosyncratic way: role names are keys and + // `true` values means the user has that role. We just want to flatten + // this to a simple array of role/capability strings + // If the user is not logged in and has no roles the users wp_capabilities returns false and we want an empty array + if ($user->meta('wp_capabilities') === false) { + $userRoles = []; + } else { + $userRoles = array_keys(array_filter($user->meta('wp_capabilities'))); } + + // Make sure the user has at least one authorized role + return !empty(array_intersect($authorizedRoles, $userRoles)); + } + } diff --git a/lib/Conifer/Form/AbstractBase.php b/lib/Conifer/Form/AbstractBase.php index 5d3afd8..23417e2 100644 --- a/lib/Conifer/Form/AbstractBase.php +++ b/lib/Conifer/Form/AbstractBase.php @@ -1,5 +1,4 @@ <?php - /** * Conifer\Form\AbstractBase class * @@ -7,12 +6,9 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Form; use Closure; -use InvalidArgumentException; /** * Abstract form base class, encapsulating the Conifer Form API @@ -105,628 +101,648 @@ * are validated, and the messages that are displayed for specific reasons. */ abstract class AbstractBase { - const MESSAGE_FIELD_REQUIRED = '%s is required'; - - const MESSAGE_INVALID_MIME_TYPE = 'Wrong file type for %s'; - - const MESSAGE_FILE_SIZE = 'The uploaded file for %s exceeds the maximum allowed size'; - - const MESSAGE_UPLOAD_ERROR = 'An error occurred with the upload for %s'; - - /** - * The fields configured for this form as an array of arrays. - * - * Each key in the top-level array is the name of a field; each field is - * represented simply as an array. Validations are declarative, in that each - * field tells this class exactly how to validate it. - * - * @var mixed[] - */ - protected array $fields = []; - - /** - * The errors collected while processing this form, as arrays. Each error array - * should have a "message" and a "field" index. - * - * @var mixed[] - */ - protected array $errors = []; - - /** - * Whether this form submission was processed successfully. - * - * @var boolean - */ - protected bool $success = false; - - /** - * Process the form submission. - * - * @param array $request the submitted form data, e.g. $_POST - */ - abstract public function process(array $request ); - - /** - * Constructor - * - * @var ?array $files Uploaded files for this form, e.g. $_FILES - */ - public function __construct( - protected ?array $files = null - ) { - } - - /** - * Create a new instance from the request array (e.g. $_POST) - * and return the hydrated form object. Takes a variable number of arguments, - * but the first argument MUST be the submitted values, as an associative - * array. The remaining arguments, if any, are passed to the constructor. - * - * @return AbstractBase the form object - * @throws InvalidArgumentException if the first argument is not an array - */ - public static function create_from_submission(...$args ): AbstractBase { - [$submission] = array_splice($args, 0, 1); - - if (!is_array($submission)) { - throw new InvalidArgumentException( - 'First argument to create_from_submission()' - . ' must be an array of submitted values' - ); - } - - // hydrate the fields and return the new instance - return (new static(...$args))->hydrate($submission); - } - - /** - * Get the fields configured for this form - * - * @return mixed[] an array of form fields. - */ - public function get_fields(): array { - return $this->fields; - } - - /** - * Get a field definition by its name - * - * @return ?array the field, or null if it doesn't exist - */ - public function get_field(string $name ): ?array { - return $this->fields[$name] ?? null; - } - - /** - * Get the current value for the given form field. - * - * @param string $field the name of the form field whose value you want. - * @return mixed the submitted value, or the existing persisted value if no - * value has been submitted, or otherwise null. - */ - public function get(string $field ): mixed { - return $this->{$field} ?? null; - } - - /** - * Get the files configured uploaded to this form - * - * @return array an array of uploaded files. - */ - public function get_files(): array { - $this->throw_files_exception_if_not_set(); - return $this->files; - } - - /** - * Set the uploaded files for this form, generally to - * the contents of the $_FILES superglobal. - * - * @return AbstractBase This form instance - */ - public function set_files(array $files = null ): AbstractBase { - $this->files = $files; - return $this; - } - - /** - * Get an uploaded file by field name - * - * @param string $field The name of the form field used to upload the file you want - * @return array An array of data for the given file upload. Will be empty if the - * field doesn't exist or an error occurred during upload. - */ - public function get_file(string $field ): array { - $this->throw_files_exception_if_not_set(); - $file = $this->files[$field] ?? []; - - // Return an empty array if an upload error occurred - if ($file && $file['error'] !== UPLOAD_ERR_OK) { - $file = []; - } - - return $file; - } - - /** - * Whether or not `$field` was checked in the submission, optionally - * matching on `$value` (e.g. for radio buttons). - * - * @param string $field the `name` of the field - * @param string|null $value (optional) the value to check against. This is - * necessary e.g. for radio inputs, where there's more than one possible - * value. - * @return bool true if the field was checked - */ - public function checked(string $field, string $value = null ): bool { - $fieldValue = $this->get($field); - - // at the very least, check that the field is present in the submission... - if (null === $fieldValue) { - return false; - } - - if (is_array($fieldValue)) { - // array field, e.g. multiple checkboxes or multiselect. - return in_array($value, $fieldValue, true); - } - - // Single value. - // EITHER the caller specified no value to match against, - // OR the submitted value matches the caller's value exactly. - return (!isset($value) || $fieldValue === $value); - } - - /** - * Whether `$field` was selected with the value `$optionValue`. - * - * ```php - * if ($form->selected('company_to_contact', 'acme')) { - * $acmeClient = new AcmeClient(); - * $acmeClient->sendMessage('Someone chose you!'); - * } - * ``` - * - * @param string $field the name of the field to check - * @param mixed $optionValue the value of the option to check against the - * actual submitted value - * @return bool - */ - public function selected(string $field, mixed $optionValue ): bool { - $fieldValue = $this->get($field); - - // at the very least, check that the field is present in the submission... - if (null === $fieldValue) { - return false; - } - - if (is_array($fieldValue)) { - return in_array($optionValue, $fieldValue, true); - } - - return $fieldValue === $optionValue; - } - - /** - * Get the errors collected while processing this form, if any - * - * @return mixed[] - */ - public function get_errors(): array { - return $this->errors; - } - - /** - * Whether this form has any validation errors - * - * @return bool - */ - public function has_errors(): bool { - return $this->errors !== []; - } - - /** - * Whether the field `$fieldName` has any validation errors - * - * @param string $fieldName the name of the field to check - * @return bool - */ - public function has_errors_for(string $fieldName ): bool { - return $this->get_errors_for($fieldName) !== []; - } - - /** - * Whether this form has been processed without errors - * - * @return boolean - */ - public function succeeded(): bool { - return $this->success; - } - - /** - * Get all unique error messages as a flat array, - * e.g. for displaying in a list at the top of the <form> element - * - * @return array - */ - public function get_unique_error_messages(): array { - return array_unique(array_map(fn(array $error ) => $error['message'], $this->errors)); - } - - /** - * Add an error message to a specific field. You can also add errors - * to the form globally or to some aspect of the form, as long as you use the same - * $fieldName to refer to it at render time using get_errors_for(). - * - * @param string $fieldName the name of the field this error is associated with - */ - public function add_error(string $fieldName, string $message ): void { - $this->errors[] = [ - 'field' => $fieldName, - 'message' => $message, - ]; - } - - /** - * Check field values according to their respective validators, - * returning whether all validations pass. - * NOTE: This doesn't add any errors explicitly; specific validators are - * responsible for that. - * - * @param array $submission the submitted fields as key/value pairs - * @throws \LogicException if validators are configured incorrectly - * @return boolean whether the submission is valid - */ - public function validate(array $submission ): bool { - $valid = true; - - foreach ($this->get_fields() as $name => $field) { - $field['name'] = $name; - $valid = $this->validate_field($field, $submission) && $valid; - } - - return $valid; - } - - /** - * Check whether a value was submitted for the given field, - * adding an error if not. - * - * @param array<string, mixed> $field the field array itself - * @param string $value the submitted value - * @return boolean - */ - public function validate_required_field(array $field, string $value ): bool { - $valid = $value !== ''; - - if (!$valid) { - // use field-defined message, or fallback on crunching message ourselves - $message = $field['required_message'] ?? sprintf( - static::MESSAGE_FIELD_REQUIRED, - $field['label'] ?? $field['name'] - ); - - $this->add_error($field['name'], $message); - } - - return $valid; - } - - /** - * Alias of validate_required_field - * - * @param array $field the field array itself - * @param string $value the submitted value - * @return boolean - */ - public function require(array $field, string $value ): bool { - return $this->validate_required_field($field, $value); - } - - /** - * Check whether a required file upload was submitted, - * adding an error if not. - * - * @param array<string, mixed> $field The file upload field array itself - * @return boolean - */ - public function validate_required_file(array $field ): bool { - $this->throw_files_exception_if_not_set(); - $valid = isset($this->files[$field['name']]); - // The file isn't set in the global array, add an error - if (!$valid) { - $message = $field['required_message'] ?? sprintf( - static::MESSAGE_FIELD_REQUIRED, - $field['label'] ?? $field['name'] - ); - $this->add_error($field['name'], $message); - } else { - // The file exists, but let's make sure it uploaded without any errors - $valid = $valid && $this->validate_file_upload($field); - } - - return $valid; - } - - /** - * Alias of validate_required_file - * - * @param array $field The file upload field array itself - * @return boolean - */ - public function require_file(array $field ): bool { - return $this->validate_required_file($field); - } - - /** - * Check whether the specified file field has an upload error, - * adding an error if so. - * - * @param array<string, mixed> $field The file upload field array itself - * @return boolean - */ - public function validate_file_upload(array $field ): bool { - $this->throw_files_exception_if_not_set(); - $file = $this->files[$field['name']] ?? []; - $valid = $file && $file['error'] === UPLOAD_ERR_OK; - // Something has gone wrong, add the appropriate error message - if (!$valid) { - $this->add_error( - $field['name'], - $this->get_file_upload_error_message( - $field, - $file['error'] - ) - ); - } - - return $valid; - } - - /** - * Check if the specified file upload field submission matches - * an array of whitelisted mime types - * - * @param array<string, mixed> $field The file upload field array itself - * @param string $value The submitted value - * @param array $submission The submitted fields as key/value pairs - * @param array $validTypes Whitelisted MIME types for the specified field - * @return boolean - */ - public function validate_file_mime_type( - array $field, - string $value, - array $submission, - array $validTypes = [] - ): bool { - $fileArr = $this->get_file($field['name']); - if ($fileArr === []) { - // if this is a required field, assume the user has specified - // validate_required_file - return true; - } - - $valid = in_array($fileArr['type'], $validTypes, true); - if (!$valid) { - $this->add_error($field['name'], sprintf( - static::MESSAGE_INVALID_MIME_TYPE, - $field['label'] ?? $field['name'] - )); - } - - return $valid; - } - - /** - * Get errors for a specific field - * - * @param string $fieldName the name of the field to get errors for - * @return array an array of error arrays - */ - public function get_errors_for(string $fieldName ): array { - return array_filter( $this->get_errors(), fn(array $error ): bool => $error['field'] === $fieldName); - } - - /** - * Get error messages for a specific field - * - * @param string $fieldName the name of the field to get errors for - * @return array an array of error message strings - */ - public function get_error_messages_for(string $fieldName ): array { - return array_map(fn(array $field ) => $field['message'], $this->get_errors_for($fieldName)); - } - - /** - * Hydrate this form object with the submitted values - * - * @param array $submission the current request params; - * typically `$_POST`, `$_GET`, or `$_REQUEST`. - * @param array<string, mixed> $options an options array. Supported options: - * - `stripslashes` or `strip_slashes`: whether to run `stripslashes()` on - * any string values. When `true`, recursively applies to array options - * as well. Default: `false`; will default to `true` in a future version - * of Conifer. - * @return AbstractBase this form instance - */ - public function hydrate(array $submission, array $options = [] ): AbstractBase { - $stripslashes = $options['stripslashes'] - ?? $options['strip_slashes'] - ?? false; - - foreach ($this->get_whitelisted_fields($submission) as $field => $value) { - $this->$field = $stripslashes - ? $this->stripslashes_deep($value) - : $value; - } - - return $this; - } - - /** - * Get the submitted values, filtered down to only fields decared in the - * constructor - * - * @param array $submission the submitted fields - * @return array the whitelisted fields - */ - public function get_whitelisted_fields(array $submission ): array { - return array_reduce(array_keys($this->fields), function ( - array $whitelist, - string $fieldName - ) use ($submission ): array { - // we don't always want to return a value for a field, e.g. - // for checkbox where null vs. empty string matters - $whitelist[$fieldName] = $this->filter_field( - $this->fields[$fieldName], - $submission[$fieldName] ?? null - ); - - return $whitelist; - }, []); - } - - /** - * Returns an error message for a given file upload field, - * based on the provided PHP upload error code. - * - * @param array<string, mixed> $field The file upload field array itself - * @param integer $errorCode The error code specified by PHP for the file upload - * @return string The error message, based on the specified upload error code - */ - public function get_file_upload_error_message(array $field, int $errorCode ) { - $errorMessage = match ($errorCode) { - UPLOAD_ERR_INI_SIZE, UPLOAD_ERR_FORM_SIZE => static::MESSAGE_FILE_SIZE, - UPLOAD_ERR_NO_FILE => static::MESSAGE_FIELD_REQUIRED, - default => static::MESSAGE_UPLOAD_ERROR, - }; - return sprintf($errorMessage, $field['label'] ?? $field['name']); - } - - /** - * Filter a submitted field value using the field's declared filter logic, - * if any. If a field default is provided, the default is applied *after* - * the filter (that is, to the result of the filter callback). - * - * @param array<string, mixed> $field the field definition - * @param mixed $value the submitted field value - * @return mixed the filtered value - */ - protected function filter_field(array $field, $value ) { - $filter = $field['filter'] ?? null; - if (is_callable($filter)) { - // apply the filter - $value = $filter($value) ?? null; - } - - // Fallback on configured default, if any. - // If the submitted value is falsey and there's no default, use the falsey - // value as submitted. - return !empty($value) ? $value : ($field['default'] ?? $value); - } - - /** - * Validate a single field - * - * @param array<string, mixed> $field the field to validate. - * MUST include at least a `name` index. - * @param array $submission the data submitted. - * @throws \LogicException if validators are not defined as an array - * @return boolean - */ - protected function validate_field(array $field, array $submission ): bool { - $valid = true; - - // check the validators for this field - $validators = $field['validators'] ?? []; - if (!is_array($validators)) { - throw new \LogicException('Validators must be defined as an array!'); - } - - // call each validator for this field - foreach ($validators as $validator) { - // validate this field, making sure invalid results carry forward - $valid = $this->execute_validator( - $validator, - $field, - $submission - ) && $valid; - } - - return $valid; - } - - /** - * Execute a single field validator - * - * @param mixed $validator the callback to execute, responsible for adding - * any errors raised - * @param array<string, mixed> $field the field to validate - * @param array $submission the submitted data - * @throws \LogicException if validator is not callable - * @return boolean - */ - protected function execute_validator( + const MESSAGE_FIELD_REQUIRED = '%s is required'; + const MESSAGE_INVALID_MIME_TYPE = 'Wrong file type for %s'; + const MESSAGE_FILE_SIZE = 'The uploaded file for %s exceeds the maximum allowed size'; + const MESSAGE_UPLOAD_ERROR = 'An error occurred with the upload for %s'; + + /** + * The fields configured for this form as an array of arrays. + * + * Each key in the top-level array is the name of a field; each field is + * represented simply as an array. Validations are declarative, in that each + * field tells this class exactly how to validate it. + * + * @var array + */ + protected $fields; + + /** + * The files uploaded to this form as an array of arrays. This will generally + * be the contents of the $_FILES superglobal. + * + * @see http://php.net/manual/en/reserved.variables.files.php + * @var array + */ + protected $files; + + /** + * The errors collected while processing this form, as arrays. Each error array + * should have a "message" and a "field" index. + * + * @var array + */ + protected $errors; + + /** + * Whether this form submission was processed successfully. + * + * @var boolean + */ + protected $success; + + /** + * Process the form submission. + * + * @param array $request the submitted form data, e.g. $_POST + */ + abstract public function process(array $request); + + /** + * Constructor + * + * @var array Uploaded files for this form, e.g. $_FILES + */ + public function __construct(array $files = null) { + $this->errors = []; + $this->fields = []; + $this->files = $files; + $this->success = false; + } + + /** + * Create a new instance from the request array (e.g. $_POST) + * and return the hydrated form object. Takes a variable number of arguments, + * but the first argument MUST be the submitted values, as an associative + * array. The remaining arguments, if any, are passed to the constructor. + * + * @throws \InvalidArgumentException if the first argument is not an array + * @return \Conifer\Form\AbstractBase the form object + */ + public static function create_from_submission(...$args) { + list($submission) = array_splice($args, 0, 1); + + if (!is_array($submission)) { + throw new \InvalidArgumentException( + 'First argument to create_from_submission()' + . ' must be an array of submitted values' + ); + } + + // hydrate the fields and return the new instance + return (new static(...$args))->hydrate($submission); + } + + /** + * Get the fields configured for this form + * + * @return array an array of form fields. + */ + public function get_fields() : array { + return $this->fields; + } + + /** + * Get a field definition by its name + * + * @return array|null the field, or null if it doesn't exist + */ + public function get_field(string $name) { + return $this->fields[$name] ?? null; + } + + /** + * Get the current value for the given form field. + * + * @param string $name the name of the form field whose value you want. + * @return the submitted value, or the existing persisted value if no + * value has been submitted, or otherwise null. + */ + public function get($field) { + return $this->{$field} ?? null; + } + + /** + * Get the files configured uploaded to this form + * + * @return array an array of uploaded files. + */ + public function get_files() : array { + $this->throw_files_exception_if_not_set(); + return $this->files; + } + + /** + * Set the uploaded files for this form, generally to + * the contents of the $_FILES superglobal. + * + * @return \Conifer\Form\AbstractBase This form instance + */ + public function set_files(array $files = null) : AbstractBase { + $this->files = $files; + return $this; + } + + /** + * Get an uploaded file by field name + * + * @param string $field The name of the form field used to upload the file you want + * @return array An array of data for the given file upload. Will be empty if the + * field doesn't exist or an error occurred during upload. + */ + public function get_file(string $field) : array { + $this->throw_files_exception_if_not_set(); + $file = $this->files[$field] ?? []; + + // Return an empty array if an upload error occurred + if ($file && $file['error'] !== UPLOAD_ERR_OK) { + $file = []; + } + + return $file; + } + + /** + * Whether or not `$field` was checked in the submission, optionally + * matching on `$value` (e.g. for radio buttons). + * + * @param string $field the `name` of the field + * @param string $value (optional) the value to check against. This is + * necessary e.g. for radio inputs, where there's more than one possible + * value. + * @return bool true if the field was checked + */ + public function checked($field, $value = null) : bool { + $fieldValue = $this->get($field); + + // at the very least, check that the field is present in the submission... + if (null === $fieldValue) { + return false; + } + + if (is_array($fieldValue)) { + // array field, e.g. multiple checkboxes or multiselect. + return in_array($value, $fieldValue, true); + } + + // Single value. + // EITHER the caller specified no value to match against, + // OR the submitted value matches the caller's value exactly. + return (!isset($value) || $fieldValue === $value); + } + + /** + * Whether `$field` was selected with the value `$optionValue`. + * + * ```php + * if ($form->selected('company_to_contact', 'acme')) { + * $acmeClient = new AcmeClient(); + * $acmeClient->sendMessage('Someone chose you!'); + * } + * ``` + * + * @param string $field the name of the field to check + * @param mixed $optionValue the value of the option to check against the + * actual submitted value + * @return bool + */ + public function selected(string $field, $optionValue) : bool { + $fieldValue = $this->get($field); + + // at the very least, check that the field is present in the submission... + if (null === $fieldValue) { + return false; + } + + if (is_array($fieldValue)) { + return in_array($optionValue, $fieldValue, true); + } + + return $fieldValue === $optionValue; + } + + /** + * Get the errors collected while processing this form, if any + * + * @return array + */ + public function get_errors() : array { + return $this->errors; + } + + /** + * Whether this form has any validation errors + * + * @return bool + */ + public function has_errors() : bool { + return !empty($this->errors); + } + + /** + * Whether the field `$fieldName` has any validation errors + * + * @param string $fieldName the name of the field to check + * @return bool + */ + public function has_errors_for(string $fieldName) : bool { + return !empty($this->get_errors_for($fieldName)); + } + + /** + * Whether this form has been processed without errors + * + * @return boolean + */ + public function succeeded() { + return !!$this->success; + } + + /** + * Get all unique error messages as a flat array, + * e.g. for displaying in a list at the top of the <form> element + * + * @return array + */ + public function get_unique_error_messages() : array { + return array_unique(array_map(function(array $error) { + return $error['message']; + }, $this->errors)); + } + + /** + * Add an error message to a specific field. You can also add errors + * to the form globally or to some aspect of the form, as long as you use the same + * $fieldName to refer to it at render time using get_errors_for(). + * + * @param string $fieldName the name of the field this error is associated with + */ + public function add_error(string $fieldName, string $message) { + $this->errors[] = [ + 'field' => $fieldName, + 'message' => $message, + ]; + } + + /** + * Check field values according to their respective validators, + * returning whether all validations pass. + * NOTE: This doesn't add any errors explicitly; specific validators are + * responsible for that. + * + * @param array $submission the submitted fields as key/value pairs + * @throws \LogicException if validators are configured incorrectly + * @return boolean whether the submission is valid + */ + public function validate(array $submission) : bool { + $valid = true; + + foreach ($this->get_fields() as $name => $field) { + $field['name'] = $name; + $valid = $this->validate_field($field, $submission) && $valid; + } + + return $valid; + } + + /** + * Check whether a value was submitted for the given field, + * adding an error if not. + * + * @param array $field the field array itself + * @param string $value the submitted value + * @return boolean + */ + public function validate_required_field(array $field, string $value) : bool { + $valid = !empty($value); + + if (!$valid) { + // use field-defined message, or fallback on crunching message ourselves + $message = $field['required_message'] ?? sprintf( + static::MESSAGE_FIELD_REQUIRED, + $field['label'] ?? $field['name'] + ); + + $this->add_error($field['name'], $message); + } + + return $valid; + } + + /** + * Alias of validate_required_field + * + * @param array $field the field array itself + * @param string $value the submitted value + * @return boolean + */ + public function require(array $field, string $value) : bool { + return $this->validate_required_field($field, $value); + } + + /** + * Check whether a required file upload was submitted, + * adding an error if not. + * + * @param array $field The file upload field array itself + * @return boolean + */ + public function validate_required_file(array $field) : bool { + $this->throw_files_exception_if_not_set(); + $valid = isset($this->files[$field['name']]); + // The file isn't set in the global array, add an error + if (!$valid) { + $message = $field['required_message'] ?? sprintf( + static::MESSAGE_FIELD_REQUIRED, + $field['label'] ?? $field['name'] + ); + $this->add_error($field['name'], $message); + } else { + // The file exists, but let's make sure it uploaded without any errors + $valid = $valid && $this->validate_file_upload($field); + } + return $valid; + } + + /** + * Alias of validate_required_file + * + * @param array $field The file upload field array itself + * @return boolean + */ + public function require_file(array $field) : bool { + return $this->validate_required_file($field); + } + + /** + * Check whether the specified file field has an upload error, + * adding an error if so. + * + * @param array $field The file upload field array itself + * @return boolean + */ + public function validate_file_upload(array $field) : bool { + $this->throw_files_exception_if_not_set(); + $file = $this->files[$field['name']] ?? []; + $valid = $file && $file['error'] === UPLOAD_ERR_OK; + // Something has gone wrong, add the appropriate error message + if (!$valid) { + $this->add_error( + $field['name'], + $this->get_file_upload_error_message( + $field, + $file['error'] + ) + ); + } + return $valid; + } + + /** + * Check if the specified file upload field submission matches + * an array of whitelisted mime types + * + * @param array $field The file upload field array itself + * @param string $value The submitted value + * @param array $submission The submitted fields as key/value pairs + * @param array $validTypes Whitelisted MIME types for the specified field + * @return boolean + */ + public function validate_file_mime_type( + array $field, + string $value, + array $submission, + array $validTypes = [] + ) : bool { + $fileArr = $this->get_file($field['name']); + if (empty($fileArr)) { + // if this is a required field, assume the user has specified + // validate_required_file + return true; + } + + $valid = in_array($fileArr['type'], $validTypes, true); + if (!$valid) { + $this->add_error($field['name'], sprintf( + static::MESSAGE_INVALID_MIME_TYPE, + $field['label'] ?? $field['name'] + )); + } + + return $valid; + } + + /** + * Get errors for a specific field + * + * @param string $fieldName the name of the field to get errors for + * @return array an array of error arrays + */ + public function get_errors_for(string $fieldName) : array { + return array_filter( $this->get_errors(), function(array $error) use ($fieldName) { + return $error['field'] === $fieldName; + }); + } + + /** + * Get error messages for a specific field + * + * @param string $fieldName the name of the field to get errors for + * @return array an array of error message strings + */ + public function get_error_messages_for(string $fieldName) : array { + return array_map(function(array $field) { + return $field['message']; + }, $this->get_errors_for($fieldName)); + } + + /** + * Hydrate this form object with the submitted values + * + * @param array $submission the current request params; + * typically `$_POST`, `$_GET`, or `$_REQUEST`. + * @param array $options an options array. Supported options: + * - `stripslashes` or `strip_slashes`: whether to run `stripslashes()` on + * any string values. When `true`, recursively applies to array options + * as well. Default: `false`; will default to `true` in a future version + * of Conifer. + * @return \Conifer\Form\AbstractBase this form instance + */ + public function hydrate(array $submission, $options = []) : AbstractBase { + $stripslashes = $options['stripslashes'] + ?? $options['strip_slashes'] + ?? false; + + foreach ($this->get_whitelisted_fields($submission) as $field => $value) { + $this->$field = $stripslashes + ? $this->stripslashes_deep($value) + : $value; + } + return $this; + } + + /** + * Get the submitted values, filtered down to only fields decared in the + * constructor + * + * @param array $submission the submitted fields + * @return array the whitelisted fields + */ + public function get_whitelisted_fields(array $submission) { + return array_reduce(array_keys($this->fields), function( + array $whitelist, + string $fieldName + ) use ($submission) { + // we don't always want to return a value for a field, e.g. + // for checkbox where null vs. empty string matters + $whitelist[$fieldName] = $this->filter_field( + $this->fields[$fieldName], + $submission[$fieldName] ?? null + ); + + return $whitelist; + }, []); + } + + /** + * Returns an error message for a given file upload field, + * based on the provided PHP upload error code. + * + * @param array $field The file upload field array itself + * @param integer $errorCode The error code specified by PHP for the file upload + * @return string The error message, based on the specified upload error code + */ + public function get_file_upload_error_message(array $field, int $errorCode) { + switch ($errorCode) { + case UPLOAD_ERR_INI_SIZE: + case UPLOAD_ERR_FORM_SIZE: + $errorMessage = static::MESSAGE_FILE_SIZE; + break; + case UPLOAD_ERR_NO_FILE: + $errorMessage = static::MESSAGE_FIELD_REQUIRED; + break; + default: + $errorMessage = static::MESSAGE_UPLOAD_ERROR; + break; + } + return sprintf($errorMessage, $field['label'] ?? $field['name']); + } + + /** + * Filter a submitted field value using the field's declared filter logic, + * if any. If a field default is provided, the default is applied *after* + * the filter (that is, to the result of the filter callback). + * + * @param array $field the field definition + * @param mixed $value the submitted field value + * @return mixed the filtered value + */ + protected function filter_field(array $field, $value) { + $filter = $field['filter'] ?? null; + if (is_callable($filter)) { + // apply the filter + $value = $filter($value) ?? null; + } + + // Fallback on configured default, if any. + // If the submitted value is falsey and there's no default, use the falsey + // value as submitted. + $value = $value ?: $field['default'] ?? $value; + + return $value; + } + + /** + * Validate a single field + * + * @param array $field the field to validate. + * MUST include at least a `name` index. + * @param array $submission the data submitted. + * @throws \LogicException if validators are not defined as an array + * @return boolean + */ + protected function validate_field(array $field, array $submission) : bool { + $valid = true; + + // check the validators for this field + $validators = $field['validators'] ?? []; + if (!is_array($validators)) { + throw new \LogicException('Validators must be defined as an array!'); + } + + // call each validator for this field + foreach ($validators as $validator) { + // validate this field, making sure invalid results carry forward + $valid = $this->execute_validator( $validator, - array $field, - array $submission - ): bool { - // get user-defined args to validator callback - $additionalArgs = []; - if (is_array($validator)) { - // splice validator into callback, saving args for later - $additionalArgs = array_splice($validator, 2); - } elseif (is_string($validator) && is_callable([ $this, $validator ])) { - $validator = [ $this, $validator ]; - } - - if (!is_callable($validator)) { - throw new \LogicException( - $field['name'] . ' field validator must be defined as a callable,' - . ' or else must be the name of an instance method.' - ); - } - - // get the submitted value for this field, defaulting to empty string - $value = $submission[$field['name']] ?? ''; - - // compile args - $validatorArgs = array_merge( - [ $field, $value, $submission ], - $additionalArgs - ); - - if ($validator instanceof Closure) { - $valid = $validator->call($this, ...$validatorArgs); - } else { - $valid = call_user_func_array($validator, $validatorArgs); - } - - return $valid; - } - - /** - * Throws an exception if the files property is null - * - * @throws \LogicException If no files were set for this form - * - * @return void - */ - protected function throw_files_exception_if_not_set(): void { - if (is_null($this->files)) { - throw new \LogicException('The files property must be set in order to use file-related functions.'); - } - } - - /** - * Recursively strip slashes from arrays and any string values they contain. - * - * @internal - */ - private function stripslashes_deep($val ): string|array { - return is_array($val) - ? array_map([ $this, 'stripslashes_deep' ], $val) - : stripslashes((string) $val); - } + $field, + $submission + ) && $valid; + } + + return $valid; + } + + /** + * Execute a single field validator + * + * @param mixed $validator the callback to execute, responsible for adding + * any errors raised + * @param array $field the field to validate + * @param array $submission the submitted data + * @throws \LogicException if validator is not callable + * @return boolean + */ + protected function execute_validator( + $validator, + array $field, + array $submission + ) : bool { + // get user-defined args to validator callback + $additionalArgs = []; + if (is_array($validator)) { + // splice validator into callback, saving args for later + $additionalArgs = array_splice($validator, 2); + } elseif (is_string($validator) && is_callable([$this, $validator])) { + $validator = [$this, $validator]; + } + + if (!is_callable($validator)) { + throw new \LogicException( + "{$field['name']} field validator must be defined as a callable," + . ' or else must be the name of an instance method.' + ); + } + + // get the submitted value for this field, defaulting to empty string + $value = $submission[$field['name']] ?? ''; + + // compile args + $validatorArgs = array_merge( + [$field, $value, $submission], + $additionalArgs + ); + + if ($validator instanceof Closure) { + $valid = $validator->call($this, ...$validatorArgs); + } else { + $valid = call_user_func_array($validator, $validatorArgs); + } + + return $valid; + } + + /** + * Throws an exception if the files property is null + * + * @throws \LogicException If no files were set for this form + * + * @return void + */ + protected function throw_files_exception_if_not_set() { + if (is_null($this->files)) { + throw new \LogicException('The files property must be set in order to use file-related functions.'); + } + } + + /** + * Recursively strip slashes from arrays and any string values they contain. + * + * @internal + */ + private function stripslashes_deep($val) { + return is_array($val) + ? array_map([$this, 'stripslashes_deep'], $val) + : stripslashes((string) $val); + } } diff --git a/lib/Conifer/Integrations/YoastIntegration.php b/lib/Conifer/Integrations/YoastIntegration.php index 4ea2abc..4e20c6e 100644 --- a/lib/Conifer/Integrations/YoastIntegration.php +++ b/lib/Conifer/Integrations/YoastIntegration.php @@ -1,11 +1,8 @@ <?php - /** * YoastIntegration class */ -declare(strict_types=1); - namespace Conifer\Integrations; /** @@ -15,12 +12,16 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ class YoastIntegration { - /** - * Demote Yoast SEO meta box to the bottom of the post edit screen - */ - public static function demote_metabox(): void { - if (is_admin()) { - add_filter('wpseo_metabox_prio', fn(): string => 'low'); - } + /** + * Demote Yoast SEO meta box to the bottom of the post edit screen + */ + public static function demote_metabox() { + if (is_admin()) { + add_filter('wpseo_metabox_prio', function() { + return 'low'; + }); } + } } + + diff --git a/lib/Conifer/Navigation/Menu.php b/lib/Conifer/Navigation/Menu.php index 0360855..8878f90 100644 --- a/lib/Conifer/Navigation/Menu.php +++ b/lib/Conifer/Navigation/Menu.php @@ -1,11 +1,8 @@ <?php - /** * Custom Menu class extending TimberMenu. */ -declare(strict_types=1); - namespace Conifer\Navigation; use Timber\Menu as TimberMenu; @@ -18,19 +15,21 @@ */ class Menu extends TimberMenu { - /** - * Get the top-level nav item that points, or whose ancestor points, - * to the current post - * - * @return ?Conifer\MenuItem the current top-level MenuItem - */ - public function get_current_top_level_item() { - foreach ( $this->get_items() as $item ) { - if ( $item->points_to_current_post_or_ancestor() ) { - return $item; - } - } - - return null; + /** + * Get the top-level nav item that points, or whose ancestor points, + * to the current post + * + * @return ?Conifer\MenuItem the current top-level MenuItem + */ + public function get_current_top_level_item() { + foreach ( $this->get_items() as $item ) { + if ( $item->points_to_current_post_or_ancestor() ) { + return $item; + } } + return null; + } } + + + diff --git a/lib/Conifer/Navigation/MenuItem.php b/lib/Conifer/Navigation/MenuItem.php index baedb86..a9c6da9 100644 --- a/lib/Conifer/Navigation/MenuItem.php +++ b/lib/Conifer/Navigation/MenuItem.php @@ -1,11 +1,8 @@ <?php - /** * Custom MenuItem class */ -declare(strict_types=1); - namespace Conifer\Navigation; use Timber\MenuItem as TimberMenuItem; @@ -17,42 +14,40 @@ * @package Conifer */ class MenuItem extends TimberMenuItem { - const CLASS_HAS_CHILDREN = 'menu-item-has-children'; - - const CLASS_CURRENT = 'current-menu-item'; - - const CLASS_CURRENT_ANCESTOR = 'current-menu-ancestor'; - - /** - * Whether to display this item's children. Typically for use in - * side navigation structures with lots of hierarchy. - * - * @return boolean true if this Item has nav children AND - * represents the current page or an ancestor of the current page - */ - public function display_children(): bool { - // If this item has children, - // and it points to the current top-level post in the nav structure, - // display its children - return $this->has_children() && $this->points_to_current_post_or_ancestor(); - } - - /** - * Whether this item points to the current post, or an ancestor of the current post - * - * @return boolean - */ - public function points_to_current_post_or_ancestor(): bool { - return in_array(static::CLASS_CURRENT, $this->classes, true) - || in_array(static::CLASS_CURRENT_ANCESTOR, $this->classes, true); - } - - /** - * Whether this MenuItem has child MenuItems or not. - * - * @return boolean true if this MenuItem has children. - */ - public function has_children(): bool { - return in_array(static::CLASS_HAS_CHILDREN, $this->classes, true); - } + const CLASS_HAS_CHILDREN = 'menu-item-has-children'; + const CLASS_CURRENT = 'current-menu-item'; + const CLASS_CURRENT_ANCESTOR = 'current-menu-ancestor'; + + /** + * Whether to display this item's children. Typically for use in + * side navigation structures with lots of hierarchy. + * + * @return boolean true if this Item has nav children AND + * represents the current page or an ancestor of the current page + */ + public function display_children() { + // If this item has children, + // and it points to the current top-level post in the nav structure, + // display its children + return $this->has_children() && $this->points_to_current_post_or_ancestor(); + } + + /** + * Whether this item points to the current post, or an ancestor of the current post + * + * @return boolean + */ + public function points_to_current_post_or_ancestor() { + return in_array(static::CLASS_CURRENT, $this->classes, true) + || in_array(static::CLASS_CURRENT_ANCESTOR, $this->classes, true); + } + + /** + * Whether this MenuItem has child MenuItems or not. + * + * @return boolean true if this MenuItem has children. + */ + public function has_children() { + return in_array(static::CLASS_HAS_CHILDREN, $this->classes, true); + } } diff --git a/lib/Conifer/Notifier/AdminNotifier.php b/lib/Conifer/Notifier/AdminNotifier.php index f45719c..ad8c0b2 100644 --- a/lib/Conifer/Notifier/AdminNotifier.php +++ b/lib/Conifer/Notifier/AdminNotifier.php @@ -4,8 +4,6 @@ * AdminNotifier class */ -declare(strict_types=1); - namespace Conifer\Notifier; /** @@ -16,10 +14,12 @@ * @package Conifer */ class AdminNotifier extends EmailNotifier { - /** - * Get the admin email address configured in General Settings - */ - public function to() { - return get_option('admin_email'); - } + /** + * Get the admin email address configured in General Settings + */ + public function to() { + return get_option('admin_email'); + } } + + diff --git a/lib/Conifer/Notifier/EmailNotifier.php b/lib/Conifer/Notifier/EmailNotifier.php index ae2dae0..9ea8ab1 100644 --- a/lib/Conifer/Notifier/EmailNotifier.php +++ b/lib/Conifer/Notifier/EmailNotifier.php @@ -4,8 +4,6 @@ * EmailNotifier class */ -declare(strict_types=1); - namespace Conifer\Notifier; /** @@ -16,13 +14,15 @@ * @package Conifer */ abstract class EmailNotifier { - use SendsEmail; + use SendsEmail; - /** - * Get the destination email address(es) - * - * @return mixed the email(s) to send to, as a comma-separated string - * or array - */ - abstract public function to(); + /** + * Get the destination email address(es) + * + * @return mixed the email(s) to send to, as a comma-separated string + * or array + */ + abstract public function to(); } + + diff --git a/lib/Conifer/Notifier/SendsEmail.php b/lib/Conifer/Notifier/SendsEmail.php index c88a99f..46c8a01 100644 --- a/lib/Conifer/Notifier/SendsEmail.php +++ b/lib/Conifer/Notifier/SendsEmail.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Notifier; /** @@ -19,118 +17,120 @@ * @package Conifer */ trait SendsEmail { - /** - * MUST return the destination email address(es) - * - * @return string|array - */ - abstract public function to(); + /** + * MUST return the destination email address(es) + * + * @return string|array + */ + abstract public function to(); - /** - * Send a UTF-8-encoded HTML email - * - * @param array|string $to array or comma-separated list of email addresses - * to send to - * @param string $subject the email subject - * @param string $message the body of the email - * @param array $headers any additional headers to set - * @return bool whether the messages were sent successfully - */ - public function send_html_message( - $to, - string $subject, - string $message, - array $headers = [] - ): bool { - return wp_mail($to, mb_convert_encoding($subject, 'UTF-8'), $message, array_merge([ - 'Content-Type: text/html; charset=UTF-8', - ], $headers)); - } + /** + * Send a UTF-8-encoded HTML email + * + * @param array|string $to array or comma-separated list of email addresses + * to send to + * @param string $subject the email subject + * @param string $message the body of the email + * @param array $headers any additional headers to set + * @return bool whether the messages were sent successfully + */ + public function send_html_message( + $to, + string $subject, + string $message, + array $headers = [] + ) : bool { + return wp_mail($to, mb_convert_encoding($subject, 'UTF-8'), $message, array_merge([ + 'Content-Type: text/html; charset=UTF-8', + ], $headers)); + } - /** - * Send a UTF-8-encoded plaintext email - * - * @param array|string $to array or comma-separated list of email addresses - * to send to - * @param string $subject the email subject - * @param string $message the body of the email - * @param array $headers any additional headers to set - * @return bool whether the messages were sent successfully - */ - public function send_plaintext_message( - $to, - string $subject, - string $message, - array $headers = [] - ): bool { - return wp_mail($to, mb_convert_encoding($subject, 'UTF-8'), $message, $headers); - } - - /** - * Alias of notify_html - */ - public function notify(...$args ) { - return $this->notify_html(...$args); - } + /** + * Send a UTF-8-encoded plaintext email + * + * @param array|string $to array or comma-separated list of email addresses + * to send to + * @param string $subject the email subject + * @param string $message the body of the email + * @param array $headers any additional headers to set + * @return bool whether the messages were sent successfully + */ + public function send_plaintext_message( + $to, + string $subject, + string $message, + array $headers = [] + ) : bool { + return wp_mail($to, mb_convert_encoding($subject, 'UTF-8'), $message, $headers); + } - /** - * Send an HTML notification email - * - * @param string $subject the email subject - * @param string $message the body of the email - * @param array $headers any additional headers to set - * @return bool whether the messages were sent successfully - */ - public function notify_html( - string $subject, - string $message, - array $headers = [] - ): bool { - return $this->send_html_message( - $this->get_valid_to_address(), - $subject, - $message, - $headers - ); - } + /** + * Alias of notify_html + */ + public function notify(...$args) { + return $this->notify_html(...$args); + } - /** - * Send a plaintext notification email - * - * @param string $subject the email subject - * @param string $message the body of the email - * @param array $headers any additional headers to set - * @return bool whether the messages were sent successfully - */ - public function notify_plaintext( - string $subject, - string $message, - array $headers = [] - ): bool { - return $this->send_plaintext_message( - $this->get_valid_to_address(), - $subject, - $message, - $headers - ); - } + /** + * Send an HTML notification email + * + * @param string $subject the email subject + * @param string $message the body of the email + * @param array $headers any additional headers to set + * @return bool whether the messages were sent successfully + */ + public function notify_html( + string $subject, + string $message, + array $headers = [] + ) : bool { + return $this->send_html_message( + $this->get_valid_to_address(), + $subject, + $message, + $headers + ); + } - /** - * Call the user-defined to() method, and throw an exception if returned - * value is invalid - * - * @throws \LogicException if to() returns the wrong type - */ - protected function get_valid_to_address(): string|array { - $to = $this->to(); + /** + * Send a plaintext notification email + * + * @param string $subject the email subject + * @param string $message the body of the email + * @param array $headers any additional headers to set + * @return bool whether the messages were sent successfully + */ + public function notify_plaintext( + string $subject, + string $message, + array $headers = [] + ) : bool { + return $this->send_plaintext_message( + $this->get_valid_to_address(), + $subject, + $message, + $headers + ); + } - // Warn the (dev) user extending this class that they're doing it wrong - if (!is_string($to) && !is_array($to)) { - throw new \LogicException( - static::class . '::to() must return a string or array' - ); - } + /** + * Call the user-defined to() method, and throw an exception if returned + * value is invalid + * + * @throws \LogicException if to() returns the wrong type + */ + protected function get_valid_to_address() { + $to = $this->to(); - return $to; + // Warn the (dev) user extending this class that they're doing it wrong + if (!is_string($to) && !is_array($to)) { + throw new \LogicException( + static::class . '::to() must return a string or array' + ); } + + return $to; + } } + + diff --git a/lib/Conifer/Notifier/SimpleNotifier.php b/lib/Conifer/Notifier/SimpleNotifier.php index 8945314..661adf8 100644 --- a/lib/Conifer/Notifier/SimpleNotifier.php +++ b/lib/Conifer/Notifier/SimpleNotifier.php @@ -20,8 +20,6 @@ * ``` */ -declare(strict_types=1); - namespace Conifer\Notifier; /** @@ -32,21 +30,29 @@ * @package Conifer */ class SimpleNotifier extends EmailNotifier { - /** - * Constructor. Pass the to email here. - * - * @param array|string $to the email addresses to send to. - * Can be a comma-separated string or an array - */ - public function __construct( - protected array|string $to - ) { - } + /** + * The email address(es) to send to + * + * @var string|array + */ + protected $to; + + /** + * Constructor. Pass the to email here. + * + * @param string|array $to the email addresses to send to. + * Can be a comma-separated string or an array + */ + public function __construct($to) { + $this->to = $to; + } - /** - * Get the admin email address configured in General Settings - */ - public function to(): array|string { - return $this->to; - } + /** + * Get the admin email address configured in General Settings + */ + public function to() { + return $this->to; + } } + + diff --git a/lib/Conifer/Post/BlogPost.php b/lib/Conifer/Post/BlogPost.php index 19307b2..d06fd5f 100644 --- a/lib/Conifer/Post/BlogPost.php +++ b/lib/Conifer/Post/BlogPost.php @@ -1,5 +1,4 @@ <?php - /** * BlogPost class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Post; use DateTime; @@ -18,26 +15,26 @@ * Class for encapsulating WP posts of type "post" */ class BlogPost extends Post { - const POST_TYPE = 'post'; - - const NUM_RELATED_POSTS = 10; - - /** - * Posts related via category to this one. - * - * @var \Timber\PostCollectionInterface - */ - protected $related_posts; - - /** - * Get all months for which a published blog post exists - * - * @return array an array of formatted month strings - */ - public static function get_all_published_months(): array { - global $wpdb; - - $sql = <<<_SQL_ + const POST_TYPE = 'post'; + + const NUM_RELATED_POSTS = 10; + + /** + * Posts related via category to this one. + * + * @var \Timber\PostCollectionInterface + */ + protected $related_posts; + + /** + * Get all months for which a published blog post exists + * + * @return array an array of formatted month strings + */ + public static function get_all_published_months() : array { + global $wpdb; + + $sql = <<<_SQL_ SELECT DISTINCT DATE_FORMAT(post_date, '%Y') AS y, DATE_FORMAT(post_date, '%m') AS m, DATE_FORMAT(post_date, '%Y-%m') AS formatted_month, @@ -47,55 +44,57 @@ public static function get_all_published_months(): array { _SQL_; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - return $wpdb->get_results( $sql, ARRAY_A ); - } - - /** - * Get all years for which a published blog post exists - * - * @return array an array of formatted year strings - */ - public static function get_all_published_years(): array { - global $wpdb; - - $sql = <<<_SQL_ + return $wpdb->get_results( $sql, ARRAY_A ); + } + + /** + * Get all years for which a published blog post exists + * + * @return array an array of formatted year strings + */ + public static function get_all_published_years() : array { + global $wpdb; + + $sql = <<<_SQL_ SELECT DISTINCT YEAR(post_date) FROM {$wpdb->posts} WHERE post_type = 'post' AND post_status = 'publish' ORDER BY post_date DESC _SQL_; // phpcs:ignore WordPress.DB.PreparedSQL.NotPrepared - return $wpdb->get_col( $sql, ARRAY_A ); - } - - /** - * Get related posts of the same type - * - * @param int $numPosts the number of posts to fetch. - * @return array an array of Conifer\Post\Post objects - */ - public function get_related( $numPosts = self::NUM_RELATED_POSTS ) { - if (!isset($this->related_posts)) { - // Get term_ids to query by - $categoryIds = array_map(fn($cat ) => $cat->id, $this->categories()); - - $this->related_posts = Timber::get_posts([ - // posts of this same type only - 'post_type' => $this->post_type, - // limit number of posts - 'posts_per_page' => $numPosts, - // exclude this post - 'post__not_in' => [ $this->ID ], - // query by shared categories - 'tax_query' => [ - [ + return $wpdb->get_col( $sql, ARRAY_A ); + } + + /** + * Get related posts of the same type + * + * @param int $numPosts the number of posts to fetch. + * @return array an array of Conifer\Post\Post objects + */ + public function get_related( $numPosts = self::NUM_RELATED_POSTS ) { + if (!isset($this->related_posts)) { + // Get term_ids to query by + $categoryIds = array_map(function($cat) { + return $cat->id; + }, $this->categories()); + + $this->related_posts = Timber::get_posts([ + // posts of this same type only + 'post_type' => $this->post_type, + // limit number of posts + 'posts_per_page' => $numPosts, + // exclude this post + 'post__not_in' => [$this->ID], + // query by shared categories + 'tax_query' => [ + [ 'taxonomy' => 'category', 'terms' => $categoryIds, - ], - ], - ])->to_array(); - } - - return $this->related_posts; + ], + ], + ]); } + + return $this->related_posts; + } } diff --git a/lib/Conifer/Post/FrontPage.php b/lib/Conifer/Post/FrontPage.php index e47bd1c..af455a0 100644 --- a/lib/Conifer/Post/FrontPage.php +++ b/lib/Conifer/Post/FrontPage.php @@ -1,27 +1,23 @@ <?php - /** * Home page class */ -declare(strict_types=1); - namespace Conifer\Post; -use Timber\Timber; - /** * Class to represent the home page. * * @package Conifer */ class FrontPage extends Page { - /** - * Get the FrontPage instance. - * - * @return \Timber\Post a FrontPage object - */ - public static function get() { - return Timber::get_post(get_option('page_on_front')); - } + /** + * Get the FrontPage instance. + * + * @return \Conifer\Post\FrontPage a FrontPage object + */ + public static function get() { + return new static(get_option('page_on_front')); + } } + diff --git a/lib/Conifer/Post/HasCustomAdminColumns.php b/lib/Conifer/Post/HasCustomAdminColumns.php index e7bef1c..f282ad9 100644 --- a/lib/Conifer/Post/HasCustomAdminColumns.php +++ b/lib/Conifer/Post/HasCustomAdminColumns.php @@ -1,5 +1,4 @@ <?php - /** * Powerful utility trait for adding custom columns in the WP Admin * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Post; /** @@ -16,95 +13,96 @@ * the ability to declaratively add custom admin columns. */ trait HasCustomAdminColumns { - /** - * Add a custom column to the admin for the given post type, with content provided - * through a callback. - * - * @param string $key the $columns array key to add - * @param string $label label for the column header - * @param callable|null $getValue (Optional) a callback to get the value to - * display in the custom column for. If not given, the column will - * display the value of the `meta` field whose `meta_key` is equal to `$key`. - * a given post. Takes a post ID as its sole parameter. - */ - public static function add_admin_column($key, $label, callable $getValue = null ): void { - $postType = static::_post_type(); - - if ($postType === 'page' || $postType === 'post') { - // e.g. manage_pages_columns - $addHook = sprintf('manage_%ss_columns', $postType); - - // e.g. manage_pages_custom_column - $displayHook = sprintf('manage_%ss_custom_column', $postType); - - } else { - // e.g. manage_my_post_type_posts_columns - $addHook = sprintf('manage_%s_posts_columns', $postType); - - // e.g. manage_my_post_type_posts_custom_column - $displayHook = sprintf('manage_%s_posts_custom_column', $postType); - } - - // Add the column to the admin - add_filter($addHook, function (array $columns ) use ($key, $label ): array { - $columns[$key] = $label; - return $columns; - }); - - // If no callback is given, infer a sensible default from the key. - $getValue ??= static::value_getter($key); - - // register a callback to display the value for this column - add_action($displayHook, function ($column, $id ) use ($key, $getValue ): void { - if ( $column === $key ) { - // NOTE: THE USER IS RESPONSIBLE FOR ESCAPING USER INPUT AS NECESSARY - // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped - echo $getValue( (int) $id); - } - }, 10, 2); + /** + * Add a custom column to the admin for the given post type, with content provided + * through a callback. + * + * @param string $key the $columns array key to add + * @param string $label label for the column header + * @param callable $getValue (Optional) a callback to get the value to + * display in the custom column for. If not given, the column will + * display the value of the `meta` field whose `meta_key` is equal to `$key`. + * a given post. Takes a post ID as its sole parameter. + */ + public static function add_admin_column($key, $label, callable $getValue = null) { + $postType = static::_post_type(); + + if ($postType === 'page' || $postType === 'post') { + // e.g. manage_pages_columns + $addHook = "manage_{$postType}s_columns"; + + // e.g. manage_pages_custom_column + $displayHook = "manage_{$postType}s_custom_column"; + + } else { + // e.g. manage_my_post_type_posts_columns + $addHook = "manage_{$postType}_posts_columns"; + + // e.g. manage_my_post_type_posts_custom_column + $displayHook = "manage_{$postType}_posts_custom_column"; } - /** - * Get a function to run based on the meta $key. - * - * @param string $key the column key whose value we need to get when rendering a custom column - * @return callable - */ - private static function value_getter($key ): callable { - $keyToGetterMapping = [ - '_wp_page_template' => static::page_template_name(...), - ]; - - return $keyToGetterMapping[$key] ?? static::post_meta_getter($key); - } - - /** - * Basic get_post_meta-like fallback - * - * @param string $key the column key - */ - private static function post_meta_getter($key ) { - return function (int $id ) use ($key ) { - $post = new static($id); - return $post->meta($key); - }; - } - - /** - * Get the page template given a post ID - * - * @param int $id the post ID - * @return string the page template name, as declared in the template header comment, or "Default Template" - */ - private static function page_template_name(int $id ): string { - // get mapping of Template File => Template Name - static $templates = null; - $templates = $templates !== null ? $templates : array_flip(get_page_templates()); - - // get the template file for this page - $templateFile = get_post_meta($id, '_wp_page_template', true) !== null ? get_post_meta($id, '_wp_page_template', true) : ''; - - // return the template name for this page - return $templates[$templateFile] ?? 'Default Template'; - } + // Add the column to the admin + add_filter($addHook, function(array $columns) use ($key, $label) { + $columns[$key] = $label; + return $columns; + }); + + // If no callback is given, infer a sensible default from the key. + $getValue = $getValue ?? static::value_getter($key); + + // register a callback to display the value for this column + add_action($displayHook, function($column, $id) use ($key, $getValue) { + if ( $column === $key ) { + // NOTE: THE USER IS RESPONSIBLE FOR ESCAPING USER INPUT AS NECESSARY + // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + echo $getValue( (int) $id); + } + }, 10, 2); + } + + /** + * Get a function to run based on the meta $key. + * + * @param string $key the column key whose value we need to get when rendering + * a custom column + * @return callable + */ + private static function value_getter($key) : callable { + $keyToGetterMapping = [ + '_wp_page_template' => [static::class, 'page_template_name'], + ]; + + return $keyToGetterMapping[$key] ?? static::post_meta_getter($key); + } + + /** + * Basic get_post_meta-like fallback + * + * @param string $key the column key + */ + private static function post_meta_getter($key) { + return function(int $id) use ($key) { + $post = new static($id); + return $post->meta($key); + }; + } + + /** + * Get the page template given a post ID + * + * @param int $id the post ID + * @return string the page template name, as declared in the template header comment, or "Default Template" + */ + private static function page_template_name(int $id) : string { + // get mapping of Template File => Template Name + static $templates = null; + $templates = $templates ?: array_flip(get_page_templates()); + + // get the template file for this page + $templateFile = get_post_meta($id, '_wp_page_template', true) ?: ''; + + // return the template name for this page + return $templates[$templateFile] ?? 'Default Template'; + } } diff --git a/lib/Conifer/Post/HasCustomAdminFilters.php b/lib/Conifer/Post/HasCustomAdminFilters.php index b0dbe22..9be1e1e 100644 --- a/lib/Conifer/Post/HasCustomAdminFilters.php +++ b/lib/Conifer/Post/HasCustomAdminFilters.php @@ -1,5 +1,4 @@ <?php - /** * Powerful utility trait for adding custom filters in the WP Admin * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Post; use WP_Query; @@ -19,133 +16,145 @@ * Declaratively add custom admin filter options. */ trait HasCustomAdminFilters { - /** - * Add a custom filter to the admin for the given post type, with custom - * query behavior provided through a callback. - * - * @param string $name the form input name for the filter - * @param array $options the options to display in the filter dropdown. - * Optional for defining taxonomy filters. If `$name` is a taxonomy, - * `$options` defaults to all non-empty terms in the taxonomy, plus an - * "Any $name" option. - * @param callable $queryModifier a callback to mutate the WP_Query object - * at query time. - * - * Callback params: - * - * * `WP_Query` `$query` the query being executed - * * `string` `$value` the filter value selected by the admin user - * - * The `$queryModifier` param is optional for cases such as querying by - * taxonomy term, in which case WP adds the term to the query automatically. - */ - public static function add_admin_filter( - string $name, - array $options = [], - callable $queryModifier = null - ): void { - // safelist $name as a query_var - add_filter('query_vars', fn(array $vars ): array => array_merge($vars, [ $name ])); - - add_action('restrict_manage_posts', function () use ($name, $options ): void { - - if ($options === [] && taxonomy_exists($name)) { - // no options specified, but this is a taxonomy filter, - // so just use all the terms - $label = static::get_taxonomy_label($name); - $initialOptions = [ '' => 'Any ' . $label ]; - - $terms = get_terms([ 'taxonomy' => $name ]); - $options = array_reduce($terms, fn(array $options, WP_Term $term ): array => array_merge($options, [ $term->slug => $term->name ]), $initialOptions); - } - - // we only want to render the filter menu if we're on the - // edit screen for the given post type - if ( static::allow_custom_filtering() ) { - - static::render_custom_filter_select([ - 'name' => $name, - 'options' => $options, - 'filtered_value' => get_query_var($name), - ]); - } - }); - - // in some cases, such as querying by custom taxonomies that already - // appear in post queries automatically, we don't need to hook into - // `pre_get_posts` at all, and as such don't need a custom $queryModifier - if (is_callable($queryModifier)) { - add_action('pre_get_posts', function (WP_Query $query ) use ( - $name, - $queryModifier - ): void { - if (static::querying_by_custom_filter($name, $query)) { - // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification - $queryModifier($query, get_query_var($name)); - } - }); + /** + * Add a custom filter to the admin for the given post type, with custom + * query behavior provided through a callback. + * + * @param string $name the form input name for the filter + * @param array $options the options to display in the filter dropdown. + * Optional for defining taxonomy filters. If `$name` is a taxonomy, + * `$options` defaults to all non-empty terms in the taxonomy, plus an + * "Any $name" option. + * @param callable $queryModifier a callback to mutate the WP_Query object + * at query time. + * + * Callback params: + * + * * `WP_Query` `$query` the query being executed + * * `string` `$value` the filter value selected by the admin user + * + * The `$queryModifier` param is optional for cases such as querying by + * taxonomy term, in which case WP adds the term to the query automatically. + */ + public static function add_admin_filter( + string $name, + array $options = [], + callable $queryModifier = null + ) { + // safelist $name as a query_var + add_filter('query_vars', function(array $vars) use ($name) { + return array_merge($vars, [$name]); + }); + + add_action('restrict_manage_posts', function() use ($name, $options) { + + if (empty($options) && taxonomy_exists($name)) { + // no options specified, but this is a taxonomy filter, + // so just use all the terms + $label = static::get_taxonomy_label($name); + $initialOptions = ['' => "Any {$label}"]; + + $terms = get_terms(['taxonomy' => $name]); + $options = array_reduce($terms, function( + array $options, + WP_Term $term + ) { + return array_merge($options, [$term->slug => $term->name]); + }, $initialOptions); + } + + // we only want to render the filter menu if we're on the + // edit screen for the given post type + if ( static::allow_custom_filtering() ) { + + static::render_custom_filter_select([ + 'name' => $name, + 'options' => $options, + 'filtered_value' => get_query_var($name), + ]); + } + }); + + // in some cases, such as querying by custom taxonomies that already + // appear in post queries automatically, we don't need to hook into + // `pre_get_posts` at all, and as such don't need a custom $queryModifier + if (is_callable($queryModifier)) { + add_action('pre_get_posts', function(WP_Query $query) use ( + $name, + $queryModifier + ) { + if (static::querying_by_custom_filter($name, $query)) { + // phpcs:ignore WordPress.CSRF.NonceVerification.NoNonceVerification + $queryModifier($query, get_query_var($name)); } + }); } - - /** - * Add a custom admin filter for a taxonomy. - * - * @param string $tax the taxonomy name to filter by - */ - public static function add_taxonomy_admin_filter( - string $tax - ): void { - $taxLabel = static::get_taxonomy_label($tax); - $initialOptions = [ '' => 'Any ' . $taxLabel ]; - - $options = array_reduce(get_terms($tax), fn(array $_options, WP_Term $term ): array => array_merge($_options, [ $term->slug => $term->name ]), $initialOptions); - - static::add_admin_filter($tax, $options); - } - - /** - * Render the <select> element for an arbitrary custom admin filter. - * Override this to customize the dropdown further. - * - * @param array $data the view data - */ - protected static function render_custom_filter_select( array $data ) { - Timber::render( 'admin/custom-filter-select.twig', $data ); - } - - /** - * Whether to show the custom filter on the edit screen for the given post type. - */ - protected static function allow_custom_filtering(): bool { - return ($GLOBALS['post_type'] ?? null) === static::_post_type() - && ($GLOBALS['pagenow'] ?? null) === 'edit.php'; - } - - /** - * Whether the user is currently trying to query by the custom filter, - * according to GET params and the current post type; determines whether - * the current WP_Query needs to be modified. - * - * @param string $name the filter name, i.e. the <select> element's - * `name` attribute - * @param WP_Query $query the current WP_Query object - */ - protected static function querying_by_custom_filter( - string $name, - WP_Query $query - ): bool { - return static::allow_custom_filtering() - && ($query->query_vars['post_type'] ?? null) === static::_post_type() - && !empty(get_query_var($name)); - } - - /** - * Get the `singular_name` label for a taxonomy - * - * @param string $tax the taxonomy whose label you want - * @return string the singular label - */ - protected static function get_taxonomy_label(string $tax ): string { - return get_taxonomy_labels(get_taxonomy($tax))->singular_name ?? ''; - } + } + + /** + * Add a custom admin filter for a taxonomy. + * + * @param string $tax the taxonomy name to filter by + */ + public static function add_taxonomy_admin_filter( + string $tax + ) { + $taxLabel = static::get_taxonomy_label($tax); + $initialOptions = ['' => "Any {$taxLabel}"]; + + $options = array_reduce(get_terms($tax), function( + array $_options, + WP_Term $term + ) : array { + return array_merge($_options, [$term->slug => $term->name]); + }, $initialOptions); + + static::add_admin_filter($tax, $options); + } + + /** + * Render the <select> element for an arbitrary custom admin filter. + * Override this to customize the dropdown further. + * + * @param array $data the view data + */ + protected static function render_custom_filter_select( array $data ) { + Timber::render( 'admin/custom-filter-select.twig', $data ); + } + + /** + * Whether to show the custom filter on the edit screen for the given post type. + */ + protected static function allow_custom_filtering() : bool { + return ($GLOBALS['post_type'] ?? null) === static::_post_type() + && ($GLOBALS['pagenow'] ?? null) === 'edit.php'; + } + + /** + * Whether the user is currently trying to query by the custom filter, + * according to GET params and the current post type; determines whether + * the current WP_Query needs to be modified. + * + * @param string $name the filter name, i.e. the <select> element's + * `name` attribute + * @param WP_Query $query the current WP_Query object + */ + protected static function querying_by_custom_filter( + string $name, + WP_Query $query + ) : bool { + return static::allow_custom_filtering() + && ($query->query_vars['post_type'] ?? null) === static::_post_type() + && !empty(get_query_var($name)); + } + + /** + * Get the `singular_name` label for a taxonomy + * + * @param string $tax the taxonomy whose label you want + * @return string the singular label + */ + protected static function get_taxonomy_label(string $tax) : string { + return get_taxonomy_labels(get_taxonomy($tax))->singular_name ?? ''; + } } diff --git a/lib/Conifer/Post/HasTerms.php b/lib/Conifer/Post/HasTerms.php index 3bf7602..57c736a 100644 --- a/lib/Conifer/Post/HasTerms.php +++ b/lib/Conifer/Post/HasTerms.php @@ -1,11 +1,8 @@ <?php - /** * Retrieve tags and categories from posts */ -declare(strict_types=1); - namespace Conifer\Post; use Timber\Term; @@ -18,216 +15,234 @@ * @package Conifer */ trait HasTerms { - /** - * Get all published posts of this type, grouped by terms of $taxonomy - * - * @param string $taxonomy the name of the taxonomy to group by, - * e.g. `"category"` - * @param array $terms The list of specific terms to filter by. Each item - * in the array can be any of the following: - * * a term ID (int or numeric string) - * * a term slug (string) - * * a WP_Term object - * * a Timber\Term object - * Defaults to all terms within $taxonomy. - * @param array $postQueryArgs additional query filters to merge into the - * array passed to `Timber::get_posts()`. Defaults to an empty array. - * @return array an array like: - * ```php - * [ - * [ 'term' => { Category 1 WP_Term object }, 'posts' => [...], - * [ 'term' => { Category 2 WP_Term object }, 'posts' => [...], - * ] - * ``` - */ - public static function get_all_grouped_by_term( - string $taxonomy, - array $terms = [], - array $postQueryArgs = [] - ): array { - // ensure we have a list of taxonomy terms - $terms = !empty($terms) ? $terms : Timber::get_terms([ - 'taxonomy' => $taxonomy, - 'hide_empty' => true, - ]); - - // convert each term ID/slug/obj to a Timber\Term - $timberTerms = array_map(fn($termIdent ) => - // Pass through already-instantiated Timber\Term objects. - // This allows for a polymorphic list of terms! ✨ - is_a($termIdent, Term::class) + /** + * Get all published posts of this type, grouped by terms of $taxonomy + * + * @param string $taxonomy the name of the taxonomy to group by, + * e.g. `"category"` + * @param array $terms The list of specific terms to filter by. Each item + * in the array can be any of the following: + * * a term ID (int or numeric string) + * * a term slug (string) + * * a WP_Term object + * * a Timber\Term object + * Defaults to all terms within $taxonomy. + * @param array $postQueryArgs additional query filters to merge into the + * array passed to `Timber::get_posts()`. Defaults to an empty array. + * @return array an array like: + * ```php + * [ + * [ 'term' => { Category 1 WP_Term object }, 'posts' => [...], + * [ 'term' => { Category 2 WP_Term object }, 'posts' => [...], + * ] + * ``` + */ + public static function get_all_grouped_by_term( + string $taxonomy, + array $terms = [], + array $postQueryArgs = [] + ) : array { + // ensure we have a list of taxonomy terms + $terms = $terms ?: Timber::get_terms([ + 'taxonomy' => $taxonomy, + 'hide_empty' => true, + ]); + + // convert each term ID/slug/obj to a Timber\Term + $timberTerms = array_map(function($termIdent) { + // Pass through already-instantiated Timber\Term objects. + // This allows for a polymorphic list of terms! ✨ + return is_a($termIdent, Term::class) ? $termIdent - : Timber::get_term($termIdent), $terms); - - // reduce each term in $taxonomy to an array containing: - // * the term - // * the term's corresponding posts - return array_reduce($timberTerms, function ( - array $grouped, - Term $term - ) use ($postQueryArgs ): array { - // Because the count may be different from the denormalized term count, - // since this may be a special query, we need to check if this term is - // actually populated/empty. - $posts = $term->posts( - $postQueryArgs - ); - if ($posts) { - // Group this term with its respective posts. - $grouped[] = [ - 'term' => $term, - 'posts' => $posts, - ]; - } - - // return the grouped posts so far - return $grouped; - }, []); - } - - /** - * Register a taxonomy for this post type - * - * @example - * ```php - * Post::register_taxonomy('sign', [ - * 'plural_label' => 'Signs', - * 'labels' => [ - * 'add_new_item' => 'Divine New Sign' - * ] - * ]); - * - * // equivalent to: - * register_taxonomy('sign', 'person', [ - * 'labels' => [ - * 'name' => 'Signs', - * 'singular_name' => 'Sign', // inferred from taxonomy name - * 'add_new_item' => 'Divine New Sign', // overridden directly w/ labels.add_new_item - * 'menu_naem' => 'View Signs' // inferred from plural_label - * // ... other singular/plural labels are inferred in the same way - * ] - * ]); - * ``` - * @param string $name the name of the taxonomy. Must be all lower-case, with - * no spaces. - * @param array<string, mixed> $options any valid array of options to `register_taxonomy()`, - * plus an optional "plural_label" index. It produces a more comprehensive - * array of labels before passing it to `register_taxonomy()`. - * @param bool $omitPostType whether to omit the post type in the declaration. - * If true, passes `null` as the `$object_type` argument to - * `register_taxonomy()`, which is useful for declaring taxonomies across - * post types. Defaults to `false`. - * @see https://codex.wordpress.org/Function_Reference/register_taxonomy - */ - public static function register_taxonomy( - string $name, - array $options = [], - bool $omitPostType = false - ): void { - $options['labels'] ??= []; - - // For singular label, fallback on taxonomy name - $singular = $options['labels']['singular_name'] - // convert underscore_inflection to Words Separated By Spaces - // TODO separate this into a utility method - ?? implode(' ', array_map(ucfirst(...), explode('_', $name))); - - // Unless there's an explicity plural_label, follow the same default logic - // as register_post_type() - $plural = $options['plural_label'] - ?? $options['label'] - ?? $options['labels']['name'] - ?? $singular . 's'; // pluralize singular naively - - // this isn't meaningful to WP, just remove it - unset($options['plural_label']); - - $options['labels']['name'] ??= $plural; - - // omit $object_type option in taxonomy declaration? - $postType = $omitPostType ? null : self::_post_type(); - - $options['labels']['singular_name'] = $singular; - - $options['labels']['menu_name'] ??= $plural; - - $options['labels']['all_items'] ??= 'All ' . $plural; - - $options['labels']['edit_item'] ??= 'Edit ' . $singular; - - $options['labels']['view_item'] ??= 'View ' . $singular; - - $options['labels']['update_item'] ??= 'Update ' . $singular; - - $options['labels']['add_new_item'] ??= 'Add New ' . $singular; - - $options['labels']['new_item_name'] ??= sprintf('New %s Name', $singular); - - $options['labels']['parent_item'] ??= 'Parent ' . $singular; - - $options['labels']['parent_item_colon'] ??= sprintf('Parent %s:', $singular); - - $options['labels']['search_items'] ??= 'Search ' . $plural; - - $options['labels']['popular_items'] = $options['labels']['r_items'] - ?? 'Popular ' . $plural; - - $options['labels']['separate_items_with_commas'] ??= sprintf('Separate %s with commas', $plural); - - $options['labels']['add_or_remove_items'] ??= 'Add or remove ' . $plural; - - $options['labels']['choose_from_most_used'] ??= 'Choose from the most used ' . $plural; - - $options['labels']['not_found'] ??= sprintf('No %s found', $plural); - - $options['labels']['back_to_items'] ??= '← Back to ' . $plural; - - // Honor custom statuses in term counts - if (is_array($options['statuses_toward_count'] ?? null)) { - $statuses = $options['statuses_toward_count']; - - // Include "publish" status in query? - $includePublished = $statuses['publish'] ?? null; - if ($includePublished !== false) { - // user explicitly remove publish from counted statuses - $statuses = array_unique(array_merge([ 'publish' ], $statuses)); - } - - unset($statuses['publish']); - - $options['update_count_callback'] = function ($terms ) use ($statuses ): void { - foreach ($terms as $term) { - static::count_statuses_toward_term_count(Timber::get_term($term), $statuses); - } - }; + : Timber::get_term($termIdent); + }, $terms); + + // reduce each term in $taxonomy to an array containing: + // * the term + // * the term's corresponding posts + return array_reduce($timberTerms, function( + array $grouped, + Term $term + ) use ($postQueryArgs) : array { + // Because the count may be different from the denormalized term count, + // since this may be a special query, we need to check if this term is + // actually populated/empty. + $posts = $term->posts( + $postQueryArgs + ); + if ($posts) { + // Group this term with its respective posts. + $grouped[] = [ + 'term' => $term, + 'posts' => $posts, + ]; + } + + // return the grouped posts so far + return $grouped; + }, []); + } + + /** + * Register a taxonomy for this post type + * + * @example + * ```php + * Post::register_taxonomy('sign', [ + * 'plural_label' => 'Signs', + * 'labels' => [ + * 'add_new_item' => 'Divine New Sign' + * ] + * ]); + * + * // equivalent to: + * register_taxonomy('sign', 'person', [ + * 'labels' => [ + * 'name' => 'Signs', + * 'singular_name' => 'Sign', // inferred from taxonomy name + * 'add_new_item' => 'Divine New Sign', // overridden directly w/ labels.add_new_item + * 'menu_naem' => 'View Signs' // inferred from plural_label + * // ... other singular/plural labels are inferred in the same way + * ] + * ]); + * ``` + * @param string $name the name of the taxonomy. Must be all lower-case, with + * no spaces. + * @param array $options any valid array of options to `register_taxonomy()`, + * plus an optional "plural_label" index. It produces a more comprehensive + * array of labels before passing it to `register_taxonomy()`. + * @param bool $omitPostType whether to omit the post type in the declaration. + * If true, passes `null` as the `$object_type` argument to + * `register_taxonomy()`, which is useful for declaring taxonomies across + * post types. Defaults to `false`. + * @see https://codex.wordpress.org/Function_Reference/register_taxonomy + */ + public static function register_taxonomy( + string $name, + array $options = [], + bool $omitPostType = false + ) { + $options['labels'] = $options['labels'] ?? []; + + // For singular label, fallback on taxonomy name + $singular = $options['labels']['singular_name'] + // convert underscore_inflection to Words Separated By Spaces + // TODO separate this into a utility method + ?? implode(' ', array_map(function(string $word) { + return ucfirst($word); + }, explode('_', $name))); + + // Unless there's an explicity plural_label, follow the same default logic + // as register_post_type() + $plural = $options['plural_label'] + ?? $options['label'] + ?? $options['labels']['name'] + ?? $singular . 's'; // pluralize singular naively + + // this isn't meaningful to WP, just remove it + unset($options['plural_label']); + + $options['labels']['name'] = $options['labels']['name'] ?? $plural; + + // omit $object_type option in taxonomy declaration? + $postType = $omitPostType ? null : self::_post_type(); + + $options['labels']['singular_name'] = $singular; + + $options['labels']['menu_name'] = $options['labels']['menu_name'] + ?? $plural; + + $options['labels']['all_items'] = $options['labels']['all_items'] + ?? "All {$plural}"; + + $options['labels']['edit_item'] = $options['labels']['edit_item'] + ?? "Edit {$singular}"; + + $options['labels']['view_item'] = $options['labels']['view_item'] + ?? "View {$singular}"; + + $options['labels']['update_item'] = $options['labels']['update_item'] + ?? "Update {$singular}"; + + $options['labels']['add_new_item'] = $options['labels']['add_new_item'] + ?? "Add New {$singular}"; + + $options['labels']['new_item_name'] = $options['labels']['new_item_name'] + ?? "New {$singular} Name"; + + $options['labels']['parent_item'] = $options['labels']['parent_item'] + ?? "Parent {$singular}"; + + $options['labels']['parent_item_colon'] = $options['labels']['parent_item_colon'] + ?? "Parent {$singular}:"; + + $options['labels']['search_items'] = $options['labels']['search_items'] + ?? "Search {$plural}"; + + $options['labels']['popular_items'] = $options['labels']['r_items'] + ?? "Popular {$plural}"; + + $options['labels']['separate_items_with_commas'] = $options['labels']['separate_items_with_commas'] + ?? "Separate {$plural} with commas"; + + $options['labels']['add_or_remove_items'] = $options['labels']['add_or_remove_items'] + ?? "Add or remove {$plural}"; + + $options['labels']['choose_from_most_used'] = $options['labels']['choose_from_most_used'] + ?? "Choose from the most used {$plural}"; + + $options['labels']['not_found'] = $options['labels']['not_found'] + ?? "No {$plural} found"; + + $options['labels']['back_to_items'] = $options['labels']['back_to_items'] + ?? "← Back to {$plural}"; + + // Honor custom statuses in term counts + if (is_array($options['statuses_toward_count'] ?? null)) { + $statuses = $options['statuses_toward_count']; + + // Include "publish" status in query? + $includePublished = $statuses['publish'] ?? null; + if ($includePublished !== false) { + // user explicitly remove publish from counted statuses + $statuses = array_unique(array_merge(['publish'], $statuses)); + } + unset($statuses['publish']); + + $options['update_count_callback'] = function($terms) use ($statuses) { + foreach ($terms as $term) { + static::count_statuses_toward_term_count(Timber::get_term($term), $statuses); } - - register_taxonomy($name, $postType, $options); + }; } - /** - * Keep term counts up to date, taking into account posts in $status - * - * @param Term $term the Term instance whose count we want to update - * @param array $statuses - */ - public static function count_statuses_toward_term_count(Term $term, array $statuses ): void { - global $wpdb; - - // Get all posts in $statuses, plus all published posts - $inStatus = $term->posts([ - 'post_status' => $statuses, - 'post_type' => static::POST_TYPE, - 'posts_per_page' => -1, - ]); - - if (is_array($inStatus)) { - // increment count by the number of term posts in $statuses - $wpdb->update( - $wpdb->term_taxonomy, - [ 'count' => count($inStatus) ], - [ 'term_taxonomy_id' => $term->term_taxonomy_id ] - ); - } + register_taxonomy($name, $postType, $options); + } + + /** + * Keep term counts up to date, taking into account posts in $status + * + * @param Timber\Term the Term instance whose count we want to update + * @param string $taxonomy the taxonomy whose terms we want to affect + */ + public static function count_statuses_toward_term_count(Term $term, array $statuses) { + global $wpdb; + + // Get all posts in $statuses, plus all published posts + $inStatus = $term->posts([ + 'post_status' => $statuses, + 'post_type' => static::POST_TYPE, + 'posts_per_page' => -1, + ]); + + if (is_array($inStatus)) { + // increment count by the number of term posts in $statuses + $wpdb->update( + $wpdb->term_taxonomy, + ['count' => count($inStatus)], + ['term_taxonomy_id' => $term->term_taxonomy_id] + ); } + } } + diff --git a/lib/Conifer/Post/Image.php b/lib/Conifer/Post/Image.php index 8b829e1..bddb754 100644 --- a/lib/Conifer/Post/Image.php +++ b/lib/Conifer/Post/Image.php @@ -1,11 +1,8 @@ <?php - /** * Manage image sizes */ -declare(strict_types=1); - namespace Conifer\Post; use Timber\Image as TimberImage; @@ -17,127 +14,136 @@ * @package Conifer */ class Image extends TimberImage { - /** - * Image sizes declared to WordPress, including default ones - * - * @var array - */ - protected static $declared_sizes = []; - - /** - * Thin wrapper around add_image_size(). Remembers arguments so that the newly declared size - * can be looked up later using get_sizes(). - * - * @link https://developer.wordpress.org/reference/functions/add_image_size/ WordPress Codec: add_image_size - * @param string $name the name of the custom size to declare - * @param int $width the width to declare for this size - * @param int $height the height to declare for this size - * @param boolean $crop whether to create versions of newly uploaded pics cropped to this size - */ - public static function add_size( $name, $width, $height = false, $crop = false ): void { - add_image_size( $name, $width, $height, $crop ); - static::$declared_sizes[$name] = [ - 'name' => $name, - 'width' => $width, - 'height' => $height, - 'crop' => $crop, - ]; - } - - /** - * Get all images sizes, including default ones - * - * @return array - */ - public static function get_sizes(): array { - $sizes = [ - 'thumbnail' => [ + /** + * Image sizes declared to WordPress, including default ones + * + * @var array + */ + protected static $declared_sizes = []; + + /** + * Thin wrapper around add_image_size(). Remembers arguments so that the newly declared size + * can be looked up later using get_sizes(). + * + * @link https://developer.wordpress.org/reference/functions/add_image_size/ WordPress Codec: add_image_size + * @param string $name the name of the custom size to declare + * @param int $width the width to declare for this size + * @param int $height the height to declare for this size + * @param boolean $crop whether to create versions of newly uploaded pics cropped to this size + */ + public static function add_size( $name, $width, $height = false, $crop = false ) { + add_image_size( $name, $width, $height, $crop ); + static::$declared_sizes[$name] = [ + 'name' => $name, + 'width' => $width, + 'height' => $height, + 'crop' => $crop, + ]; + } + + /** + * Get all images sizes, including default ones + * + * @return array + */ + public static function get_sizes() { + $sizes = [ + 'thumbnail' => [ 'name' => 'thumbnail', 'width' => get_option('thumbnail_size_w'), 'height' => get_option('thumbnail_size_h'), - ], - 'medium' => [ + ], + 'medium' => [ 'name' => 'medium', 'width' => get_option('medium_size_w'), 'height' => get_option('medium_size_h'), - ], - 'medium_large' => [ + ], + 'medium_large' => [ 'name' => 'medium_large', 'width' => get_option('medium_large_size_w'), 'height' => get_option('medium_large_size_h'), - ], - 'large' => [ + ], + 'large' => [ 'name' => 'large', 'width' => get_option('large_size_w'), 'height' => get_option('large_size_h'), - ], - ]; - - if (!isset(static::$declared_sizes['thumbnail'])) { - // default sizes aren't all set; set them now - static::$declared_sizes = array_merge(static::$declared_sizes, $sizes); - } + ], + ]; - return static::$declared_sizes; + if (!isset(static::$declared_sizes['thumbnail'])) { + // default sizes aren't all set; set them now + static::$declared_sizes = array_merge(static::$declared_sizes, $sizes); } - /** - * Get dimension info for the custom image size $size - * - * @param array $size an array containing at least: "name", "width", and "height". - * @return array - */ - public static function get_size( $size ) { - $sizes = static::get_sizes(); + return static::$declared_sizes; + } - return $sizes[$size] ?? []; - } + /** + * Get dimension info for the custom image size $size + * + * @param array $size an array containing at least: "name", "width", and "height". + * @return array + */ + public static function get_size( $size ) { + $sizes = static::get_sizes(); - /** - * Get the aspect ratio of the underlying image file. - * - * @return mixed image aspect ratio as a float, or null if the image does not exist - */ - public function aspect() { - if (file_exists($this->file_loc)) { - return parent::aspect(); - } + if (isset($sizes[$size])) { + return $sizes[$size]; } - /** - * Get the declared width of this image, optionally specific to the image size $size - * - * @param string $customSize if specified - * @return int - */ - public function width( $customSize = false ): int { - $width = $customSize && static::get_size($customSize) ? static::get_size($customSize)['width'] : parent::width(); + return []; + } + + /** + * Get the aspect ratio of the underlying image file. + * + * @return mixed image aspect ratio as a float, or null if the image does not exist + */ + public function aspect() { + if (file_exists($this->file_loc)) { + return parent::aspect(); + } + } + + /** + * Get the declared width of this image, optionally specific to the image size $size + * + * @param string $customSize if specified + * @return int + */ + public function width( $customSize = false ) : int { + if ($customSize && static::get_size($customSize)) { + $width = static::get_size($customSize)['width']; + } else { + $width = parent::width(); + } - return (int) $width; + return (int) $width; + } + + /** + * Get the declared height of this image, optionally specific to the image size $size + * + * @param string $customSize if specified + * @return int + */ + public function height( $customSize = false ) { + if (!file_exists($this->file_loc)) { + return null; } - /** - * Get the declared height of this image, optionally specific to the image size $size - * - * @param string $customSize if specified - * @return int - */ - public function height( $customSize = false ) { - if (!file_exists($this->file_loc)) { - return null; - } - - $originalWidth = (int) parent::width(); - $width = $this->width($customSize); - - if ($width !== $originalWidth) { - // distinct custom dimensions; calculate new based on aspect ratio - $height = floor( $width / $this->aspect() ); - } else { - // not a custom size; just return the original height - $height = (int) parent::height(); - } - - return $height; + $originalWidth = (int) parent::width(); + $width = $this->width($customSize); + + if ($width !== $originalWidth) { + // distinct custom dimensions; calculate new based on aspect ratio + $height = floor( $width / $this->aspect() ); + } else { + // not a custom size; just return the original height + $height = (int) parent::height(); } + + return $height; + } } + diff --git a/lib/Conifer/Post/Page.php b/lib/Conifer/Post/Page.php index da2db4c..f3bd5b4 100644 --- a/lib/Conifer/Post/Page.php +++ b/lib/Conifer/Post/Page.php @@ -1,5 +1,4 @@ <?php - /** * Conifer\Post\Page class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Post; use Timber\Post as TimberPost; @@ -22,45 +19,48 @@ * @package Conifer */ class Page extends Post { - const POST_TYPE = 'page'; + const POST_TYPE = 'page'; - /** - * Get the top-level title to display from the nav structure, fall back - * on this Page object's title it it's outside the nav hierarchy. - * - * @param \Conifer\Navigation\Menu $menu the menu to look at to determine the title - * @return string the title to display - */ - public function get_title_from_nav_or_post( Menu $menu ): string { - return $menu->get_current_top_level_item()->title ?? $this->title(); - } + /** + * Get the top-level title to display from the nav structure, fall back + * on this Page object's title it it's outside the nav hierarchy. + * + * @param \Conifer\Navigation\Menu $menu the menu to look at to determine the title + * @return string the title to display + */ + public function get_title_from_nav_or_post( Menu $menu ) : string { + return $menu->get_current_top_level_item( $this )->title + ?? $this->title(); + } - /** - * Get the Blog Landing Page. - * - * @return \Conifer\Post\Page - */ - public static function get_blog_page(): TimberPost { - return Timber::get_post( get_option('page_for_posts') ); - } + /** + * Get the Blog Landing Page. + * + * @return \Conifer\Post\Page + */ + public static function get_blog_page() : TimberPost { + return Timber::get_post( get_option('page_for_posts') ); + } - /** - * Get a page by its template filename, relative to the theme root. - * - * @param string $template - * @param array extra query params to be merged in with the posts query - * to be performed. - * @return TimberPost the first page found matching the template, or null if no such page exists - */ - public static function get_by_template(string $template, array $query = [] ) { - return Timber::get_post(array_merge($query, [ - 'post_type' => 'page', - 'meta_query' => [ + /** + * Get a page by its template filename, relative to the theme root. + * + * @param string $template + * @param array extra query params to be merged in with the posts query + * to be performed. + * @return null|Page the first page found matching the template, or null if no such page exists + */ + public static function get_by_template(string $template, array $query = []) { + return Timber::get_post(array_merge($query, [ + 'post_type' => 'page', + 'meta_query' => [ [ - 'key' => '_wp_page_template', - 'value' => $template, - ], + 'key' => '_wp_page_template', + 'value' => $template, ], - ])); - } + ], + ])); + } } + + diff --git a/lib/Conifer/Post/Post.php b/lib/Conifer/Post/Post.php index bb0cb33..c3944ac 100644 --- a/lib/Conifer/Post/Post.php +++ b/lib/Conifer/Post/Post.php @@ -1,11 +1,8 @@ <?php - /** * High-level WP Post behavior */ -declare(strict_types=1); - namespace Conifer\Post; use Timber\Helper; @@ -19,408 +16,425 @@ * High-level behavior for WP Posts, on top of TimberPost class */ abstract class Post extends TimberPost { - use HasTerms; - use HasCustomAdminColumns; - use HasCustomAdminFilters; - use SupportsAdvancedSearch; - - const POST_TYPE = ''; + use HasTerms; + use HasCustomAdminColumns; + use HasCustomAdminFilters; + use SupportsAdvancedSearch; - const RELATED_POST_COUNT = 3; + const POST_TYPE = ''; - const LATEST_POST_COUNT = 3; + const RELATED_POST_COUNT = 3; - /** - * When instantiating TimberImages, create instances of this class - * - * @var string - * @codingStandardsIgnoreStart - */ - public $ImageClass = \Conifer\Post\Image::class; + const LATEST_POST_COUNT = 3; + /** + * When instantiating TimberImages, create instances of this class + * + * @var string + * @codingStandardsIgnoreStart + */ + public $ImageClass = '\Conifer\Post\Image'; /* @codingStandardsIgnoreEnd non-standard var case, needed by Timber */ - /** - * The default blog landing page URL - * - * @var string - */ - protected static $blog_url; - - /** - * The collection of related posts, via arbitrary taxonomies - * - * @var array - */ - protected $related_by = []; - - /** - * Related post counts, via arbitrary taxonomies - * - * @var array - */ - protected $related_post_counts = []; - - /** - * Register this post type given the high-level label options. - * - * @example - * ```php - * Post::register_type('person', [ - * 'plural_label' => 'People', - * 'labels' => [ - * 'add_new_item' => 'Onboard New Person' - * ], - * ]); - * - * // equivalent to: - * register_post_type('person', [ - * 'label' => 'Person', // inferred from post_type, - * 'labels' => [ - * 'singular_name' => 'Person', // inferred from post_type - * 'add_new_item' => 'Onboard New Person', // overridden directly w/ labels.add_new_item - * 'view_items' => 'View People', // inferred from plural_label - * // ... other singular/plural labels are inferred in the same way - * ], - * ]); - * ``` - */ - public static function register_type(): void { - $options = static::type_options(); - - $options['labels'] ??= []; - - // For singular label, fallback on post type - $singular = $options['labels']['singular_name'] - // convert underscore_inflection to Words Separated By Spaces - // TODO separate this into a utility method - ?? implode(' ', array_map(ucfirst(...), explode('_', static::_post_type()))); - - // Unless there's an explicity plural_label, follow the same default logic - // as register_post_type() - $plural = $options['plural_label'] - ?? $options['label'] - ?? $options['labels']['name'] - ?? $singular . 's'; // pluralize singular naively - - // this isn't meaningful to WP, just remove it - unset($options['plural_label']); - - $options['labels']['name'] ??= $plural; - - $options['labels']['singular_name'] = $singular; - - $options['labels']['add_new_item'] ??= 'Add New ' . $singular; - - $options['labels']['edit_item'] ??= 'Edit ' . $singular; - - $options['labels']['new_item'] ??= 'New ' . $singular; - - $options['labels']['view_item'] ??= 'View ' . $singular; - - $options['labels']['view_items'] ??= 'View ' . $plural; - - $options['labels']['search_items'] ??= 'Search ' . $plural; - - $options['labels']['not_found'] ??= sprintf('No %s found', $plural); - - $options['labels']['not_found_in_trash'] ??= sprintf('No %s found in trash', $plural); - - $options['labels']['all_items'] ??= 'All ' . $plural; - - $options['labels']['archives'] ??= $singular . ' Archives'; - - $options['labels']['attributes'] ??= $singular . ' Attributes'; - - $options['labels']['insert_into_item'] ??= 'Insert into ' . $singular; - - $options['labels']['uploaded_to_this_item'] ??= 'Uploaded to this ' . $singular; - - register_post_type(static::_post_type(), $options); - } - - /** - * Default implementation of custom post type labels, - * for use in register_post_type(). - * - * @return array{} - */ - public static function type_options(): array { - return []; - } - - /** - * Child classes must declare their own post types - * - * @throws \RuntimeException if the POST_TYPE class constant is empty - * @return string - * @codingStandardsIgnoreStart PSR2.Methods.MethodDeclaration.Underscore - */ + /** + * The default blog landing page URL + * + * @var string + */ + protected static $blog_url; + + /** + * The collection of related posts, via arbitrary taxonomies + * + * @var array + */ + protected $related_by = []; + + /** + * Related post counts, via arbitrary taxonomies + * + * @var array + */ + protected $related_post_counts = []; + + /** + * Register this post type given the high-level label options. + * + * @example + * ```php + * Post::register_type('person', [ + * 'plural_label' => 'People', + * 'labels' => [ + * 'add_new_item' => 'Onboard New Person' + * ], + * ]); + * + * // equivalent to: + * register_post_type('person', [ + * 'label' => 'Person', // inferred from post_type, + * 'labels' => [ + * 'singular_name' => 'Person', // inferred from post_type + * 'add_new_item' => 'Onboard New Person', // overridden directly w/ labels.add_new_item + * 'view_items' => 'View People', // inferred from plural_label + * // ... other singular/plural labels are inferred in the same way + * ], + * ]); + * ``` + * @param array $options any valid array of options to `register_post_type()`, + * plus an optional "plural_label" index. It produces a more comprehensive + * array of labels before passing it to `register_post_type()`. + */ + public static function register_type() { + $options = static::type_options(); + + $options['labels'] = $options['labels'] ?? []; + + // For singular label, fallback on post type + $singular = $options['labels']['singular_name'] + // convert underscore_inflection to Words Separated By Spaces + // TODO separate this into a utility method + ?? implode(' ', array_map(function(string $word) { + return ucfirst($word); + }, explode('_', static::_post_type()))); + + // Unless there's an explicity plural_label, follow the same default logic + // as register_post_type() + $plural = $options['plural_label'] + ?? $options['label'] + ?? $options['labels']['name'] + ?? $singular . 's'; // pluralize singular naively + + // this isn't meaningful to WP, just remove it + unset($options['plural_label']); + + $options['labels']['name'] = $options['labels']['name'] ?? $plural; + + $options['labels']['singular_name'] = $singular; + + $options['labels']['add_new_item'] = $options['labels']['add_new_item'] + ?? "Add New $singular"; + + $options['labels']['edit_item'] = $options['labels']['edit_item'] + ?? "Edit $singular"; + + $options['labels']['new_item'] = $options['labels']['new_item'] + ?? "New $singular"; + + $options['labels']['view_item'] = $options['labels']['view_item'] + ?? "View $singular"; + + $options['labels']['view_items'] = $options['labels']['view_items'] + ?? "View $plural"; + + $options['labels']['search_items'] = $options['labels']['search_items'] + ?? "Search $plural"; + + $options['labels']['not_found'] = $options['labels']['not_found'] + ?? "No $plural found"; + + $options['labels']['not_found_in_trash'] = $options['labels']['not_found_in_trash'] + ?? "No $plural found in trash"; + + $options['labels']['all_items'] = $options['labels']['all_items'] + ?? "All $plural"; + + $options['labels']['archives'] = $options['labels']['archives'] + ?? "$singular Archives"; + + $options['labels']['attributes'] = $options['labels']['attributes'] + ?? "$singular Attributes"; + + $options['labels']['insert_into_item'] = $options['labels']['insert_into_item'] + ?? "Insert into $singular"; + + $options['labels']['uploaded_to_this_item'] = $options['labels']['uploaded_to_this_item'] + ?? "Uploaded to this $singular"; + + register_post_type(static::_post_type(), $options); + } + + /** + * Default implementation of custom post type labels, + * for use in register_post_type(). + * + * @return array + */ + public static function type_options() : array { + return []; + } + + /** + * Child classes must declare their own post types + * + * @throws \RuntimeException if the POST_TYPE class constant is empty + * @return string + * @codingStandardsIgnoreStart PSR2.Methods.MethodDeclaration.Underscore + */ protected static function _post_type() : string { // @codingStandardsIgnoreEnd - if (empty(static::POST_TYPE)) { - throw new \RuntimeException( - 'For some static methods to work correctly, you must define the ' - . static::class . '::POST_TYPE constant' - ); - } - - return static::POST_TYPE; + if (empty(static::POST_TYPE)) { + throw new \RuntimeException( + 'For some static methods to work correctly, you must define the ' + . static::class . '::POST_TYPE constant' + ); } - /** - * Get the latest posts - * - * @param int $count - * @return iterable - */ - public static function latest(int $count = self::LATEST_POST_COUNT ): iterable { - return Timber::get_posts([ - 'posts_per_page' => $count, - ]); - } - - - - /* - * Instance methods - */ - - - /** - * Place tighter restrictions on post types than Timber, - * forcing all concrete subclasses to implement this method. - */ - public function type(): string { - return static::_post_type(); - } - - /** - * Get all the posts matching the given query - * (defaults to the current/global WP query constraints) - * - * @param array|string $query any valid Timber query - * @return array an array of all matching post objects - */ - public static function get_all(array $query = [] ): iterable { + return static::POST_TYPE; + } + + /** + * Get the latest posts + * + * @return + */ + public static function latest(int $count = self::LATEST_POST_COUNT) : iterable { + return Timber::get_posts([ + 'posts_per_page' => $count, + ]); + } + + + + /* + * Instance methods + */ + + + /** + * Place tighter restrictions on post types than Timber, + * forcing all concrete subclasses to implement this method. + */ + public function type() : string { + return static::_post_type(); + } + + /** + * Get all the posts matching the given query + * (defaults to the current/global WP query constraints) + * + * @param array|string $query any valid Timber query + * @return array an array of all matching post objects + */ + public static function get_all(array $query = []) : iterable { // phpcs:disable WordPress.PHP.DevelopmentFunctions.error_log_trigger_error // phpcs:disable WordPress.Security.EscapeOutput.OutputNotEscaped - trigger_error( '[ Conifer ] Post::get_all() is deprecated in Conifer 1.0.0. Use Timber::get_posts() with Class Maps instead. https://timber.github.io/docs/v2/guides/class-maps' ); - - $class = static::class; - - // Avoid instantiating this (abstract) class, causing a Fatal Error. - // TODO figure out a more elegant way to do this?? - // Might have to rework this at the Timber level - // @see https://github.com/timber/timber/pull/1218 - if ($class === self::class) { - $class = TimberPost::class; - } else { - // we're NOT just defaulting to blog post, so get the post type to query - $query['post_type'] = static::_post_type(); - } - - $posts = Timber::get_posts($query, $class); + trigger_error( '[ Conifer ] Post::get_all() is deprecated in Conifer 1.0.0. Use Timber::get_posts() with Class Maps instead. https://timber.github.io/docs/v2/guides/class-maps' ); + + $class = static::class; + + // Avoid instantiating this (abstract) class, causing a Fatal Error. + // TODO figure out a more elegant way to do this?? + // Might have to rework this at the Timber level + // @see https://github.com/timber/timber/pull/1218 + if ($class === self::class) { + $class = TimberPost::class; + } else { + // we're NOT just defaulting to blog post, so get the post type to query + $query['post_type'] = static::_post_type(); + } - return $posts->to_array() ?? []; + return Timber::get_posts($query, $class) ?: []; + } + + /** + * Get the URL of the blog landing page + * (what WP calls the "post archive" page) + * + * @return string the URL + */ + public static function get_blog_url() { + if ( ! static::$blog_url ) { + // haven't fetched the URL yet...go get it + $page = Page::get_blog_page(); + + // cache it + static::$blog_url = $page->link(); } - /** - * Get the URL of the blog landing page - * (what WP calls the "post archive" page) - * - * @return string the URL - */ - public static function get_blog_url() { - if ( ! static::$blog_url ) { - // haven't fetched the URL yet...go get it - $page = Page::get_blog_page(); - - // cache it - static::$blog_url = $page->link(); - } - - return static::$blog_url; + return static::$blog_url; + } + + /** + * Check whether a post by the given ID exists + * + * @param int $id the post ID to check for + * @return boolean true if the post exists, false otherwise + */ + public static function exists( $id ) { + $post = get_post($id); + + // support calling Post::exists() directly (not on subclasses) + if (static::class === self::class) { + return !empty($post); } - /** - * Check whether a post by the given ID exists - * - * @param int $id the post ID to check for - * @return boolean true if the post exists, false otherwise - */ - public static function exists( $id ) { - $post = get_post($id); - - // support calling Post::exists() directly (not on subclasses) - if (static::class === self::class) { - return !empty($post); - } - - return $post && $post->post_type === static::_post_type(); + return $post && $post->post_type === static::_post_type(); + } + + /** + * Create a new post from an array of data + * + * @param array $data key/value pairs to populate the post and post meta + * tables. The following keys are special, and their corresponding values + * will end up in the wp_posts table: + * + * * post_author + * * post_date + * * post_date_gmt + * * post_content + * * post_content_filtered + * * post_title + * * post_excerpt + * * post_status + * * comment_status + * * ping_status + * * post_password + * * post_name + * * to_ping + * * pinged + * * post_modified + * * post_modified_gmt + * * post_parent + * * menu_order + * * post_mime_type + * * guid + * * post_category + * * tags_input + * * tax_input + * + * The keys "ID" and "post_type" are blacklisted and will be ignored. + * The value for "ID" is generated on post creation by WordPress/MySQL; + * The value for "post_type" will always come from $this->get_post_type(). + * + * All others key/value pairs are considered metadata and end up in wp_postmeta. + * @return \Project\Post + */ + public static function create(array $data) : Post { + // blacklist ID and post_type; we get these automagically + unset($data['ID']); + unset($data['post_type']); + + $postFields = [ + 'post_author', + 'post_date', + 'post_date_gmt', + 'post_content', + 'post_content_filtered', + 'post_title', + 'post_excerpt', + 'post_status', + 'comment_status', + 'ping_status', + 'post_password', + 'post_name', + 'to_ping', + 'pinged', + 'post_modified', + 'post_modified_gmt', + 'post_parent', + 'menu_order', + 'post_mime_type', + 'guid', + 'post_category', + 'tags_input', + 'tax_input', + ]; + + // compute the data to go in the wp_posts table + $postData = array_intersect_key( + $data, + array_flip($postFields) + ); + + // $data becomes post meta data + foreach ($postData as $key => $value) { + unset($data[$key]); } - /** - * Create a new post from an array of data - * - * @param array<string, mixed> $data key/value pairs to populate the post and post meta - * tables. The following keys are special, and their corresponding values - * will end up in the wp_posts table: - * - * * post_author - * * post_date - * * post_date_gmt - * * post_content - * * post_content_filtered - * * post_title - * * post_excerpt - * * post_status - * * comment_status - * * ping_status - * * post_password - * * post_name - * * to_ping - * * pinged - * * post_modified - * * post_modified_gmt - * * post_parent - * * menu_order - * * post_mime_type - * * guid - * * post_category - * * tags_input - * * tax_input - * - * The keys "ID" and "post_type" are blacklisted and will be ignored. - * The value for "ID" is generated on post creation by WordPress/MySQL; - * The value for "post_type" will always come from $this->get_post_type(). - * - * All others key/value pairs are considered metadata and end up in wp_postmeta. - * @return int|TimberPost|\WP_Error - */ - public static function create(array $data ) { - // blacklist ID and post_type; we get these automagically - unset($data['ID']); - unset($data['post_type']); - - $postFields = [ - 'post_author', - 'post_date', - 'post_date_gmt', - 'post_content', - 'post_content_filtered', - 'post_title', - 'post_excerpt', - 'post_status', - 'comment_status', - 'ping_status', - 'post_password', - 'post_name', - 'to_ping', - 'pinged', - 'post_modified', - 'post_modified_gmt', - 'post_parent', - 'menu_order', - 'post_mime_type', - 'guid', - 'post_category', - 'tags_input', - 'tax_input', - ]; - - // compute the data to go in the wp_posts table - $postData = array_intersect_key( - $data, - array_flip($postFields) - ); - - // $data becomes post meta data - foreach (array_keys($postData) as $key) { - unset($data[$key]); - } - - // merge the metadata and post type in with any post "proper" data - $id = wp_insert_post(array_merge($postData, [ - 'post_type' => static::_post_type(), - 'meta_input' => $data, - ])); - - if (is_wp_error($id)) { - return $id; - } - - // return a new instance of the called class - return Timber::get_post($id); + // merge the metadata and post type in with any post "proper" data + $id = wp_insert_post(array_merge($postData, [ + 'post_type' => static::_post_type(), + 'meta_input' => $data, + ])); + + if (is_wp_error($id)) { + return $id; } - /** - * Get related Posts of the same post type, who share terms in $taxonomy with - * this Post. - * - * @param string $taxonomy the taxonomy to associate with, e.g. "category" - * @param int $postCount Optional. The number of posts to get. Defaults to 3. - * @return Post[] an array of Post objects - */ - public function get_related_by_taxonomy( - string $taxonomy, - int $postCount = self::RELATED_POST_COUNT - ): iterable { - // Get any previously queried related posts - $relatedPosts = $this->related_by[$taxonomy] ?? []; - $relatedPostCount = $this->related_post_counts[$taxonomy] ?? null; - - if (count($relatedPosts) < $postCount && !isset($relatedPostCount)) { - // There may be more related posts than previously queried; look for them - $termIds = array_map(fn(Term $term ) => $term->id, $this->terms($taxonomy)); - - $this->related_by[$taxonomy] = Timber::get_posts([ - 'post_type' => static::_post_type(), - 'post__not_in' => [ $this->ID ], - 'posts_per_page' => $postCount, - 'tax_query' => [ - [ + // return a new instance of the called class + return Timber::get_post($id); + } + + /** + * Get related Posts of the same post type, who share terms in $taxonomy with + * this Post. + * + * @param string $taxonomy the taxonomy to associate with, e.g. "category" + * @param int $postCount Optional. The number of posts to get. Defaults to 3. + * @return Post[] an array of Post objects + */ + public function get_related_by_taxonomy( + string $taxonomy, + int $postCount = self::RELATED_POST_COUNT + ) : iterable { + // Get any previously queried related posts + $relatedPosts = $this->related_by[$taxonomy] ?? []; + $relatedPostCount = $this->related_post_counts[$taxonomy] ?? null; + + if (count($relatedPosts) < $postCount && !isset($relatedPostCount)) { + // There may be more related posts than previously queried; look for them + $termIds = array_map(function(Term $term) { + return $term->id; + }, $this->terms($taxonomy)); + + $this->related_by[$taxonomy] = Timber::get_posts([ + 'post_type' => static::_post_type(), + 'post__not_in' => [$this->ID], + 'posts_per_page' => $postCount, + 'tax_query' => [ + [ 'taxonomy' => $taxonomy, 'terms' => $termIds, - ], - ], - ])->to_array(); - - $newCount = count($this->related_by[$taxonomy]); - if ($newCount < $relatedPostCount) { - // Our query fewer than $postCount posts, so we know this is the - // exact number of related posts for this taxonomy. Save this count - // for future calls. - $this->related_post_counts[$taxonomy] = $newCount; - } - } - - return array_slice($this->related_by[$taxonomy], 0, $postCount); - } - - /** - * Get related Posts of the same post type, who share categories with - * this Post. - * - * @param int $postCount Optional. The number of posts to get. Defaults to 3. - * @return Post[] an array of Post objects - */ - public function get_related_by_category( - int $postCount = self::RELATED_POST_COUNT - ): iterable { - return $this->get_related_by_taxonomy('category', $postCount); + ], + ], + ])->to_array(); + + $newCount = count($this->related_by[$taxonomy]); + if ($newCount < $relatedPostCount) { + // Our query fewer than $postCount posts, so we know this is the + // exact number of related posts for this taxonomy. Save this count + // for future calls. + $this->related_post_counts[$taxonomy] = $newCount; + } } - /** - * Get related Posts of the same post type, who share tags with - * this Post. - * - * @param int $postCount Optional. The number of posts to get. Defaults to 3. - * @return Post[] an array of Post objects - */ - public function get_related_by_tag( - int $postCount = self::RELATED_POST_COUNT - ): iterable { - return $this->get_related_by_taxonomy('post_tag', $postCount); - } + return array_slice($this->related_by[$taxonomy], 0, $postCount); + } + + /** + * Get related Posts of the same post type, who share categories with + * this Post. + * + * @param int $postCount Optional. The number of posts to get. Defaults to 3. + * @return Post[] an array of Post objects + */ + public function get_related_by_category( + int $postCount = self::RELATED_POST_COUNT + ) : iterable { + return $this->get_related_by_taxonomy('category', $postCount); + } + + /** + * Get related Posts of the same post type, who share tags with + * this Post. + * + * @param int $postCount Optional. The number of posts to get. Defaults to 3. + * @return Post[] an array of Post objects + */ + public function get_related_by_tag( + int $postCount = self::RELATED_POST_COUNT + ) : iterable { + return $this->get_related_by_taxonomy('post_tag', $postCount); + } } + diff --git a/lib/Conifer/Post/SupportsAdvancedSearch.php b/lib/Conifer/Post/SupportsAdvancedSearch.php index 121473d..4fa521d 100644 --- a/lib/Conifer/Post/SupportsAdvancedSearch.php +++ b/lib/Conifer/Post/SupportsAdvancedSearch.php @@ -1,18 +1,15 @@ <?php -declare(strict_types=1); - /* @codingStandardsIgnoreFile */ namespace Conifer\Post; use Conifer\Query\ClauseGenerator; trait SupportsAdvancedSearch { - /** - * @param array[] $config - */ - public static function configure_advanced_search(array $config): void { - add_filter('posts_clauses', function(array $clauses, $query) use($config): array { + public static function configure_advanced_search(array $config) { + //$modifier = new QueryModifier($GLOBALS['wpdb']); + + add_filter('posts_clauses', function(array $clauses, $query) use($config) { global $wpdb; //debug($query->meta_query->queries); @@ -30,12 +27,14 @@ public static function configure_advanced_search(array $config): void { // customize only queries for post_types that appear in config $searchCustomizations = array_filter( $config, - fn(array $searchConfig): bool => array_intersect( - $queryingPostTypes, - $searchConfig['post_type'] - ) !== []); - - if ($searchCustomizations === []) { + function($searchConfig) use($queryingPostTypes) { + return !empty(array_intersect( + $queryingPostTypes, + $searchConfig['post_type'] + )); + }); + + if (empty($searchCustomizations)) { // no advanced search customizations apply to this query return $clauses; } @@ -45,20 +44,28 @@ public static function configure_advanced_search(array $config): void { // ->add_join('postmeta', 'posts.ID = postmeta.post_id') $clauses['join'] .= - sprintf(' LEFT JOIN %s meta_search', $wpdb->postmeta) - . sprintf(' ON ( %s.ID = meta_search.post_id ) ', $wpdb->posts); + " LEFT JOIN {$wpdb->postmeta} meta_search" + . " ON ( {$wpdb->posts}.ID = meta_search.post_id ) "; // map -> wildcard - $terms = array_map(fn(string $term): string => sprintf('%%%s%%', $term), $query->query_vars['search_terms']); - - $whereClauses = array_map(function(array $postTypeSearch) use($wpdb, $terms, $query): string { - $titleComparisons = array_map(fn(string $term): string => $wpdb->prepare(sprintf('%s.post_title LIKE %%s', $wpdb->posts), $term), $terms); + $terms = array_map(function(string $term) : string { + return "%{$term}%"; + }, $query->query_vars['search_terms']); + + $whereClauses = array_map(function(array $postTypeSearch) use($wpdb, $terms, $query) { + $titleComparisons = array_map(function(string $term) use($wpdb) : string { + return $wpdb->prepare("{$wpdb->posts}.post_title LIKE %s", $term); + }, $terms); $titleClause = '(' . implode(' OR ', $titleComparisons) . ')'; - $excerptComparisons = array_map(fn(string $term): string => $wpdb->prepare(sprintf('%s.post_excerpt LIKE %%s', $wpdb->posts), $term), $terms); + $excerptComparisons = array_map(function(string $term) use($wpdb) : string { + return $wpdb->prepare("{$wpdb->posts}.post_excerpt LIKE %s", $term); + }, $terms); $excerptClause = '(' . implode(' OR ', $excerptComparisons) . ')'; - $contentComparisons = array_map(fn(string $term): string => $wpdb->prepare(sprintf('%s.post_content LIKE %%s', $wpdb->posts), $term), $terms); + $contentComparisons = array_map(function(string $term) use($wpdb) : string { + return $wpdb->prepare("{$wpdb->posts}.post_content LIKE %s", $term); + }, $terms); $contentClause = '(' . implode(' OR ', $contentComparisons) . ')'; $metaKeyComparisons = array_map(function($key) use($wpdb) : string { @@ -74,7 +81,7 @@ public static function configure_advanced_search(array $config): void { $op = '='; } - return $wpdb->prepare(sprintf('(meta_search.meta_key %s %%s)', $op), $key['key']); + return $wpdb->prepare("(meta_search.meta_key {$op} %s)", $key['key']); } return ''; @@ -82,7 +89,9 @@ public static function configure_advanced_search(array $config): void { $metaKeyClause = '(' . implode(' OR ', $metaKeyComparisons) . ')'; - $metaValueComparisons = array_map(fn(string $term) => $wpdb->prepare('(meta_value LIKE %s)', $term), $terms); + $metaValueComparisons = array_map(function(string $term) use($wpdb) { + return $wpdb->prepare('(meta_value LIKE %s)', $term); + }, $terms); $metaValueClause = '(' . implode(' OR ', $metaValueComparisons) . ')'; $metaClause = ' (' . implode(' AND ', [$metaKeyClause, $metaValueClause]) . ')'; @@ -102,7 +111,9 @@ public static function configure_advanced_search(array $config): void { $postTypes = is_array($queryPostType) ? $queryPostType : [$queryPostType]; - $postTypeCriteria = array_map(fn(string $type) => $wpdb->prepare('%s', $type), $postTypes); + $postTypeCriteria = array_map(function(string $type) use($wpdb) { + return $wpdb->prepare('%s', $type); + }, $postTypes); // get post status from current query $queryStatuses = $postTypeSearch['post_status'] ?? ['publish']; @@ -112,7 +123,9 @@ public static function configure_advanced_search(array $config): void { $postStatuses = is_array($queryStatuses) ? $queryStatuses : [$queryStatuses]; - $postStatusCriteria = array_map(fn(string $type) => $wpdb->prepare('%s', $type), $postStatuses); + $postStatusCriteria = array_map(function(string $type) use($wpdb) { + return $wpdb->prepare('%s', $type); + }, $postStatuses); $queryStatusClause = ' AND wp_posts.post_status IN (' . implode(', ', $postStatusCriteria) diff --git a/lib/Conifer/Shortcode/AbstractBase.php b/lib/Conifer/Shortcode/AbstractBase.php index 29463f6..4d0ac96 100644 --- a/lib/Conifer/Shortcode/AbstractBase.php +++ b/lib/Conifer/Shortcode/AbstractBase.php @@ -1,11 +1,8 @@ <?php - /** * Declarative-style WP shortcodes */ -declare(strict_types=1); - namespace Conifer\Shortcode; /** @@ -15,27 +12,29 @@ * @package Conifer */ abstract class AbstractBase { - /** - * Register a shortcode with the given "tag". - * Tells WP to call render() to render the shortcode content. - * - * @param string $tag The tag to be used to write the actual shortcode - */ - public static function register(string $tag ): void { - add_shortcode( $tag, function ($args = [], string $html = '' ): string { - $shortcode = new static(); - // coerce args to an array - return $shortcode->render(!empty($args) ? $args : [], $html); - }); - } + /** + * Register a shortcode with the given "tag". + * Tells WP to call render() to render the shortcode content. + * + * @param string $tag The tag to be used to write the actual shortcode + */ + public static function register( $tag ) { + add_shortcode( $tag, function($args = [], string $html = '') { + $shortcode = new static(); + // coerce args to an array + return $shortcode->render($args ?: [], $html); + }); + } - /** - * Output the result of this shortcode - * - * @param array $atts the standard WP shortcode attributes - */ - abstract public function render( - array $atts = [], - string $content = '' - ): string; + /** + * Output the result of this shortcode + * + * @param array $atts the standard WP shortcode attributes + */ + abstract public function render( + array $atts = [], + string $content = '' + ) : string; } + + diff --git a/lib/Conifer/Shortcode/Button.php b/lib/Conifer/Shortcode/Button.php index 461fdac..3da6168 100644 --- a/lib/Conifer/Shortcode/Button.php +++ b/lib/Conifer/Shortcode/Button.php @@ -1,11 +1,8 @@ <?php - /** * Custom buttons inside RTEs! */ -declare(strict_types=1); - namespace Conifer\Shortcode; use DOMDocument; @@ -20,38 +17,38 @@ * @package Groot */ class Button extends AbstractBase { - const DEFAULT_BUTTON_CLASS = 'btn'; - - /** - * Get the HTML for rendering the button link - * - * @param array $atts key/value attribute pairs specified by the shortcode. - * Acceptable params: - * - * * `class`: the class to add to the `<a>` tag (default is `"btn"`) - * @param string $html the raw markup between the start/end shortcode tags. - * @return string the modified <a> tag HTML - */ - public function render(array $atts = [], string $html = '' ): string { - if ( $html !== '' ) { - $dom = new DOMDocument(); - - // prevent doctype, html/body tags from being added - $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); - - // get the first <a> in the markup - $link = $dom->getElementsByTagName('a')->item(0); - - if ($link) { - $link->setAttribute( - 'class', - $atts['class'] ?? static::DEFAULT_BUTTON_CLASS - ); - // update markup - $html = $dom->saveHTML(); - } - } - - return trim($html); + const DEFAULT_BUTTON_CLASS = 'btn'; + + /** + * Get the HTML for rendering the button link + * + * @param array $atts key/value attribute pairs specified by the shortcode. + * Acceptable params: + * + * * `class`: the class to add to the `<a>` tag (default is `"btn"`) + * @param string $html the raw markup between the start/end shortcode tags. + * @return string the modified <a> tag HTML + */ + public function render(array $atts = [], string $html = '') : string { + if ( $html ) { + $dom = new DOMDocument(); + + // prevent doctype, html/body tags from being added + $dom->loadHTML($html, LIBXML_NOERROR | LIBXML_HTML_NOIMPLIED | LIBXML_HTML_NODEFDTD); + + // get the first <a> in the markup + $link = $dom->getElementsByTagName('a')->item(0); + + if ($link) { + $link->setAttribute( + 'class', + $atts['class'] ?? static::DEFAULT_BUTTON_CLASS + ); + // update markup + $html = $dom->saveHTML(); + } } + + return trim($html); + } } diff --git a/lib/Conifer/Site.php b/lib/Conifer/Site.php index 5c1fe1a..5df56c5 100644 --- a/lib/Conifer/Site.php +++ b/lib/Conifer/Site.php @@ -1,11 +1,8 @@ <?php - /** * Central Site class */ -declare(strict_types=1); - namespace Conifer; use Timber\Timber; @@ -34,767 +31,774 @@ * @package Conifer */ class Site extends TimberSite { - const DEFAULT_TWIG_EXTENSIONS = [ + const DEFAULT_TWIG_EXTENSIONS = [ StringLoaderExtension::class, + ]; + + /** + * An array of directories where Conifer will look for JavaScript files + * + * @var array + */ + protected $script_directory_cascade; + + /** + * An array of directories where Conifer will look for stylesheets + * + * @var array + */ + protected $style_directory_cascade; + + /** + * An array of directories where Conifer will look for Twig views + * + * @var array + */ + protected $view_directory_cascade; + + /** + * Assets version timestamp, used for cache-busting + * Array: key=filename, value=timestamp + * + * @var array + */ + protected $assets_version; + + /** + * User-defined admin hotkeys + * + * @var array + */ + protected $custom_admin_hotkeys; + + /** + * Construct a Conifer Site object. + * + * @example + * ```php + * use Conifer\Site; + * + * // non-multisite setup: + * $site = new Site(); + * + * // multisite setup: + * $site = new Site(1); + * ``` + * @param string|int $identifier the WP site name or ID + */ + public function __construct($identifier = null) { + parent::__construct($identifier); + + // establish some sensible default script directories + $this->script_directory_cascade = [ + get_stylesheet_directory() . '/js/', + get_stylesheet_directory() . '/dist/', + // TODO set up a bootstrap file for symbol discovery + // https://phpstan.org/user-guide/discovering-symbols + WP_PLUGIN_DIR . '/conifer/assets/js/', + WPMU_PLUGIN_DIR . '/conifer/assets/js/', ]; - /** - * An array of directories where Conifer will look for JavaScript files - * - * @var array - */ - protected $script_directory_cascade; - - /** - * An array of directories where Conifer will look for stylesheets - * - * @var array - */ - protected $style_directory_cascade; - - /** - * An array of directories where Conifer will look for Twig views - * - * @var array - */ - protected $view_directory_cascade; - - /** - * Assets version timestamp, used for cache-busting - * Array: key=filename, value=timestamp - * - * @var array - */ - protected $assets_version; - - /** - * User-defined admin hotkeys - * - * @var array - */ - protected $custom_admin_hotkeys = []; - - /** - * Construct a Conifer Site object. - * - * @example - * ```php - * use Conifer\Site; - * - * // non-multisite setup: - * $site = new Site(); - * - * // multisite setup: - * $site = new Site(1); - * ``` - * @param string|int $identifier the WP site name or ID - */ - public function __construct($identifier = null ) { - parent::__construct($identifier); - - // establish some sensible default script directories - $this->script_directory_cascade = [ - get_stylesheet_directory() . '/js/', - get_stylesheet_directory() . '/dist/', - // TODO set up a bootstrap file for symbol discovery - // https://phpstan.org/user-guide/discovering-symbols - WP_PLUGIN_DIR . '/conifer/assets/js/', - WPMU_PLUGIN_DIR . '/conifer/assets/js/', - ]; - - $this->style_directory_cascade = [ get_stylesheet_directory() . '/' ]; - - // check theme for view files, then plugin - $this->view_directory_cascade = [ - get_stylesheet_directory() . '/views/', - realpath(__DIR__ . '/../../views/'), - ]; - } - - /** - * Configure any WordPress hooks and register site-wide components, such as - * nav menus - * - * @param callable $userDefinedConfig a callback for configuring this Site - * from theme code. - * @param boole $configureDefaults whether to run Conifer's default - * configuration code. Defaults to `true`. - * @return Conifer\Site the Site object it was called on - */ - public function configure( - callable $userDefinedConfig = null, - bool $configureDefaults = true - ): Site { - // unless the user has explicitly disabled the defaults, configure them - if ($configureDefaults) { - $this->configure_defaults(); - } - - if (is_callable($userDefinedConfig)) { - // Set up user-defined configuration - $userDefinedConfig->call($this); - } - - return $this; - } - - /** - * Configure useful defaults for Twig functions/filters, - * custom image sizes, shortcodes, etc. - */ - public function configure_defaults(): void { - add_filter('timber/context', $this->add_to_context(...)); - - $this->configure_default_classmaps(); - $this->configure_twig_view_cascade(); - $this->configure_default_twig_extensions(); - $this->add_default_twig_helpers(); - $this->configure_default_admin_dashboard_widgets(); - $this->enable_admin_hotkeys(); + $this->style_directory_cascade = [get_stylesheet_directory() . '/']; - Button::register('button'); - - Integrations\YoastIntegration::demote_metabox(); - // TODO moar integrations! - } + // check theme for view files, then plugin + $this->view_directory_cascade = [ + get_stylesheet_directory() . '/views/', + realpath(__DIR__ . '/../../views/'), + ]; - /** - * Register default Post Class Maps for default Conifer classes - * - * @todo Terms/Users - */ - public function configure_default_classmaps(): void { - add_filter('timber/post/classmap', fn(array $map ): array => array_merge($map, [ + $this->custom_admin_hotkeys = []; + } + + /** + * Configure any WordPress hooks and register site-wide components, such as + * nav menus + * + * @param callable $userDefinedConfig a callback for configuring this Site + * from theme code. + * @param boole $configureDefaults whether to run Conifer's default + * configuration code. Defaults to `true`. + * @return Conifer\Site the Site object it was called on + */ + public function configure( + callable $userDefinedConfig = null, + bool $configureDefaults = true + ) : Site { + // unless the user has explicitly disabled the defaults, configure them + if ($configureDefaults) { + $this->configure_defaults(); + } + + if (is_callable($userDefinedConfig)) { + // Set up user-defined configuration + $userDefinedConfig->call($this); + } + + return $this; + } + + /** + * Configure useful defaults for Twig functions/filters, + * custom image sizes, shortcodes, etc. + */ + public function configure_defaults() { + add_filter('timber/context', [$this, 'add_to_context']); + + $this->configure_default_classmaps(); + $this->configure_twig_view_cascade(); + $this->configure_default_twig_extensions(); + $this->add_default_twig_helpers(); + $this->configure_default_admin_dashboard_widgets(); + $this->enable_admin_hotkeys(); + + Button::register('button'); + + Integrations\YoastIntegration::demote_metabox(); + // TODO moar integrations! + } + + /** + * Register default Post Class Maps for default Conifer classes + * + * @todo Terms/Users + */ + public function configure_default_classmaps() { + add_filter('timber/post/classmap', function(array $map) : array { + return array_merge($map, [ // For pages, instantiate a FrontPage for the globally configured home page, // otherwise return a regular Page. - 'page' => function (WP_Post $page ): string { - static $homeId; - $homeId ??= get_option('page_on_front'); - return $page->ID === $homeId ? FrontPage::class : Page::class; + 'page' => function(WP_Post $page) { + static $homeId; + $homeId = $homeId ?? get_option('page_on_front'); + return $page->ID === $homeId ? FrontPage::class : Page::class; }, 'post' => BlogPost::class, - ])); - } - - - /** - * Register a script within the script cascade path. Calls `wp_register_script` - * transparently, except that it defaults to registering in the footer instead - * of the header. - * - * @param string $scriptHandle the script handle to register - * @param string $fileName the file to search for in the script cascade path - * @param array $dependencies an array of registered dependency handles - * @param array|string|bool|null $version the version of the script to append to - * the URL rendered in the <script> tag. Accepts any valid value of the $ver - * argument to `wp_register_script`, plus the literal value `true`, which - * tells Conifer to look for an assets version file to use for cache-busting. - * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset - * file version relative to the theme folder path. - * Defaults to `true`. - * @param bool $inFooter whether to register this script in the footer. Unlike - * the same argument to the core `wp_register_script` function, this defaults - * to `true`. - */ - public function register_script( - string $scriptName, - string $fileName, - array $dependencies = [], - $version = true, - bool $inFooter = true - ): void { - if (is_array($version) && isset($version['file'])) { - // use defined asset version file for cache-busting in the theme build process - $version = $this->get_assets_version($version['file']); - } elseif ($version === true) { - // use automatic any automatic cache-busting in the theme build process - $version = $this->get_assets_version(); - } - - wp_register_script( - $scriptName, - $this->get_script_uri($fileName), - $dependencies, - $version, - $inFooter - ); - } - - /** - * Enqueue a script within the script cascade path. Calls wp_enqueue_script - * transparently, except that it defaults to enqueueing in the footer instead - * of the header. - * - * @param string $scriptHandle the script handle to register and enqueue - * @param string $fileName the file to search for in the script cascade path. - * Defaults to the empty string, but is required if the script has not - * already been registered. - * @param array $dependencies an array of registered dependency handles - * @param array|string|bool|null $version the version of the script to append to - * the URL rendered in the <script> tag. Accepts any valid value of the $ver - * argument to `wp_enqueue_script`, plus the literal value `true`, which - * tells Conifer to look for an assets version file to use for cache-busting. - * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset - * file version relative to the theme folder path. - * Defaults to `true`. - * @param bool $inFooter whether to enqueue this script in the footer. Unlike - * the same argument to the core `wp_enqueue_script` function, this defaults - * to `true`. - */ - public function enqueue_script( - string $scriptName, - string $fileName = '', - array $dependencies = [], - $version = true, - bool $inFooter = true - ): void { - - if (is_array($version) && isset($version['file'])) { - // use defined asset version file for cache-busting in the theme build process - $version = $this->get_assets_version($version['file']); - } elseif ($version === true) { - // use automatic any automatic cache-busting in the theme build process - $version = $this->get_assets_version(); - } - - wp_enqueue_script( - $scriptName, - $this->get_script_uri($fileName), - $dependencies, - $version, - $inFooter - ); - } - - /** - * Register a stylesheet within the style cascade path. Calls - * `wp_register_style` transparently. - * - * @param string $stylesheetHandle the style handle to register - * @param string $fileName the file to search for in the style cascade path - * @param array $dependencies an array of registered dependency handles - * @param array|string|bool|null $version the version of the style to append to - * the URL rendered in the <link> tag. Accepts any valid value of the $ver - * argument to `wp_register_style`, plus the literal value `true`, which - * tells Conifer to look for an assets version file to use for cache-busting. - * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset - * file version relative to the theme folder path. - * Defaults to `true`. - * @param bool $media the media for which this stylesheet has been defined; - * passed transparently to `wp_register_style`. Defaults to "all" (as does - * `wp_register_style` itself). - */ - public function register_style( - string $stylesheetName, - string $fileName, - array $dependencies = [], - $version = true, - string $media = 'all' - ): void { - if (is_array($version) && isset($version['file'])) { - // use defined asset version file for cache-busting in the theme build process - $version = $this->get_assets_version($version['file']); - } elseif ($version === true) { - // use automatic any automatic cache-busting in the theme build process - $version = $this->get_assets_version(); - } - - wp_register_style( - $stylesheetName, - $this->get_stylesheet_uri($fileName), - $dependencies, - $version, - $media - ); - } - - /** - * Enqueue a stylesheet within the style cascade path. Calls - * `wp_enqueue_style` transparently. - * - * @param string $stylesheetHandle the style handle to register and enqueue - * @param string $fileName the file to search for in the style cascade path. - * Defaults to the empty string, but is required if the style has not - * already been registered. - * @param array $dependencies an array of registered dependency handles - * @param array|string|bool|null $version the version of the style to append to - * the URL rendered in the <link> tag. Accepts any valid value of the $ver - * argument to `wp_enqueue_style`, plus the literal value `true`, which - * tells Conifer to look for an assets version file to use for cache-busting. - * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset - * file version relative to the theme folder path. - * Defaults to `true`. - * @param string $media the media for which this stylesheet has been defined. - * Passed transparently to `wp_enqueue_style`. Defaults to "all" (as does - * `wp_enqueue_style` itself). - */ - public function enqueue_style( - string $stylesheetName, - string $fileName = '', - array $dependencies = [], - $version = true, - string $media = 'all' - ): void { - if (is_array($version) && isset($version['file'])) { - // use defined asset version file for cache-busting in the theme build process - $version = $this->get_assets_version($version['file']); - } elseif ($version === true) { - // use automatic any automatic cache-busting in the theme build process - $version = $this->get_assets_version(); - } - - wp_enqueue_style( - $stylesheetName, - $this->get_stylesheet_uri($fileName), - $dependencies, - $version, - $media - ); - } - - /** - * Get the Timber context, optionally with extra data to add within the - * current scope. - * - * @param array $with data to merge into the context array - * @return array the merged data - * @example - * // get the default context data - * $data = $site->context(); - * - * // get the default context data, plus some extra stuff - * $data = $site->context([ - * 'post' => $post, - * 'whatevs' => 'CUZ THIS IS MY UNITED STATES OF WHATEVER', - * ]); - */ - public function context(array $with = [] ): array { - return array_merge(Timber::context(), $with); - } - - /** - * Get the current Timber context, with the "post" index set to $post - * - * @deprecated v0.7.0 - * @param Conifer\Post $post the current Post object - * @return array the Timber context - */ - public function get_context_with_post( Post $post ) { + ]); + }); + } + + + /** + * Register a script within the script cascade path. Calls `wp_register_script` + * transparently, except that it defaults to registering in the footer instead + * of the header. + * + * @param string $scriptHandle the script handle to register + * @param string $fileName the file to search for in the script cascade path + * @param array $dependencies an array of registered dependency handles + * @param array|string|bool|null $version the version of the script to append to + * the URL rendered in the <script> tag. Accepts any valid value of the $ver + * argument to `wp_register_script`, plus the literal value `true`, which + * tells Conifer to look for an assets version file to use for cache-busting. + * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset + * file version relative to the theme folder path. + * Defaults to `true`. + * @param bool $inFooter whether to register this script in the footer. Unlike + * the same argument to the core `wp_register_script` function, this defaults + * to `true`. + */ + public function register_script( + string $scriptName, + string $fileName, + array $dependencies = [], + $version = true, + bool $inFooter = true + ) { + if (is_array($version) && isset($version['file'])) { + // use defined asset version file for cache-busting in the theme build process + $version = $this->get_assets_version($version['file']); + } elseif ($version === true) { + // use automatic any automatic cache-busting in the theme build process + $version = $this->get_assets_version(); + } + + wp_register_script( + $scriptName, + $this->get_script_uri($fileName), + $dependencies, + $version, + $inFooter + ); + } + + /** + * Enqueue a script within the script cascade path. Calls wp_enqueue_script + * transparently, except that it defaults to enqueueing in the footer instead + * of the header. + * + * @param string $scriptHandle the script handle to register and enqueue + * @param string $fileName the file to search for in the script cascade path. + * Defaults to the empty string, but is required if the script has not + * already been registered. + * @param array $dependencies an array of registered dependency handles + * @param array|string|bool|null $version the version of the script to append to + * the URL rendered in the <script> tag. Accepts any valid value of the $ver + * argument to `wp_enqueue_script`, plus the literal value `true`, which + * tells Conifer to look for an assets version file to use for cache-busting. + * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset + * file version relative to the theme folder path. + * Defaults to `true`. + * @param bool $inFooter whether to enqueue this script in the footer. Unlike + * the same argument to the core `wp_enqueue_script` function, this defaults + * to `true`. + */ + public function enqueue_script( + string $scriptName, + string $fileName = '', + array $dependencies = [], + $version = true, + bool $inFooter = true + ) { + + if (is_array($version) && isset($version['file'])) { + // use defined asset version file for cache-busting in the theme build process + $version = $this->get_assets_version($version['file']); + } elseif ($version === true) { + // use automatic any automatic cache-busting in the theme build process + $version = $this->get_assets_version(); + } + + wp_enqueue_script( + $scriptName, + $this->get_script_uri($fileName), + $dependencies, + $version, + $inFooter + ); + } + + /** + * Register a stylesheet within the style cascade path. Calls + * `wp_register_style` transparently. + * + * @param string $stylesheetHandle the style handle to register + * @param string $fileName the file to search for in the style cascade path + * @param array $dependencies an array of registered dependency handles + * @param array|string|bool|null $version the version of the style to append to + * the URL rendered in the <link> tag. Accepts any valid value of the $ver + * argument to `wp_register_style`, plus the literal value `true`, which + * tells Conifer to look for an assets version file to use for cache-busting. + * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset + * file version relative to the theme folder path. + * Defaults to `true`. + * @param bool $media the media for which this stylesheet has been defined; + * passed transparently to `wp_register_style`. Defaults to "all" (as does + * `wp_register_style` itself). + */ + public function register_style( + string $stylesheetName, + string $fileName, + array $dependencies = [], + $version = true, + string $media = 'all' + ) { + if (is_array($version) && isset($version['file'])) { + // use defined asset version file for cache-busting in the theme build process + $version = $this->get_assets_version($version['file']); + } elseif ($version === true) { + // use automatic any automatic cache-busting in the theme build process + $version = $this->get_assets_version(); + } + + wp_register_style( + $stylesheetName, + $this->get_stylesheet_uri($fileName), + $dependencies, + $version, + $media + ); + } + + /** + * Enqueue a stylesheet within the style cascade path. Calls + * `wp_enqueue_style` transparently. + * + * @param string $stylesheetHandle the style handle to register and enqueue + * @param string $fileName the file to search for in the style cascade path. + * Defaults to the empty string, but is required if the style has not + * already been registered. + * @param array $dependencies an array of registered dependency handles + * @param array|string|bool|null $version the version of the style to append to + * the URL rendered in the <link> tag. Accepts any valid value of the $ver + * argument to `wp_enqueue_style`, plus the literal value `true`, which + * tells Conifer to look for an assets version file to use for cache-busting. + * Pass an array ['file' => 'my-assets-version-text'] to get a custom asset + * file version relative to the theme folder path. + * Defaults to `true`. + * @param string $media the media for which this stylesheet has been defined. + * Passed transparently to `wp_enqueue_style`. Defaults to "all" (as does + * `wp_enqueue_style` itself). + */ + public function enqueue_style( + string $stylesheetName, + string $fileName = '', + array $dependencies = [], + $version = true, + string $media = 'all' + ) { + if (is_array($version) && isset($version['file'])) { + // use defined asset version file for cache-busting in the theme build process + $version = $this->get_assets_version($version['file']); + } elseif ($version === true) { + // use automatic any automatic cache-busting in the theme build process + $version = $this->get_assets_version(); + } + + wp_enqueue_style( + $stylesheetName, + $this->get_stylesheet_uri($fileName), + $dependencies, + $version, + $media + ); + } + + /** + * Get the Timber context, optionally with extra data to add within the + * current scope. + * + * @param array $with data to merge into the context array + * @return array the merged data + * @example + * // get the default context data + * $data = $site->context(); + * + * // get the default context data, plus some extra stuff + * $data = $site->context([ + * 'post' => $post, + * 'whatevs' => 'CUZ THIS IS MY UNITED STATES OF WHATEVER', + * ]); + */ + public function context(array $with = []) : array { + return array_merge(Timber::context(), $with); + } + + /** + * Get the current Timber context, with the "post" index set to $post + * + * @deprecated v0.7.0 + * @param Conifer\Post $post the current Post object + * @return array the Timber context + */ + public function get_context_with_post( Post $post ) { // @codingStandardsIgnoreStart WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error('get_context_with_post is deprecated. Use context instead. https://coniferplug.in/site.html#timber-context-helper', E_USER_DEPRECATED); // @codingStandardsIgnoreEnd - $context = Timber::context(); - $context['post'] = $post; - return $context; - } - - /** - * Get the current Timber context, with the "posts" index set to $posts - * - * @deprecated v0.7.0 - * @param array $posts an array of Conifer\Post objects - * @return array the Timber context - */ - public function get_context_with_posts( array $posts ) { + $context = Timber::context(); + $context['post'] = $post; + return $context; + } + + /** + * Get the current Timber context, with the "posts" index set to $posts + * + * @deprecated v0.7.0 + * @param array $posts an array of Conifer\Post objects + * @return array the Timber context + */ + public function get_context_with_posts( array $posts ) { // @codingStandardsIgnoreStart WordPress.PHP.DevelopmentFunctions.error_log_trigger_error trigger_error('get_context_with_post is deprecated. Use context instead. https://coniferplug.in/site.html#timber-context-helper', E_USER_DEPRECATED); // @codingStandardsIgnoreEnd - $context = Timber::context(); - $context['posts'] = $posts; - return $context; - } - - /** - * Add arbitrary data to the site-wide context array - * - * @param array<string, mixed> $context the default context - * @return array the updated context - */ - public function add_to_context( array $context ): array { - $context['site'] = $this; - $context['primary_menu'] = Timber::get_menu('primary'); - $context['body_classes'] = get_body_class(); - $context['search_query'] = get_search_query(); - return $context; - } - - - - /* - * Twig Helper Methods - */ - - /** - * Add a Twig helper that implements Twig filters and/or functions to the - * Twig environment that Timber uses to render views. - * - * @param Twig\HelperInterface $helper any instance of a - * Twig\HelperInterface that implements the functions/filters to add - */ - public function add_twig_helper(Twig\HelperInterface $helper ): void { - add_filter('timber/twig', fn(Environment $twig ): \Twig\Environment => $this->get_twig_with_helper($twig, $helper)); - } - - /** - * Add any filters/functions implemented by `$helper` to the Twig instance - * `$twig`. - * - * @param Environment $twig the Twig environment to add to - * @param Twig\HelperInterface $helper the helper instance that implements the - * filters/functions to add - * @return Environment - */ - public function get_twig_with_helper( - Environment $twig, - Twig\HelperInterface $helper - ): Environment { - // add Twig filters - foreach ( $helper->get_filters() as $name => $callable ) { - $filter = new TwigFilter( $name, $callable ); - $twig->addFilter( $filter ); + $context = Timber::context(); + $context['posts'] = $posts; + return $context; + } + + /** + * Add arbitrary data to the site-wide context array + * + * @param array $context the default context + * @return array the updated context + */ + public function add_to_context( array $context ) : array { + $context['site'] = $this; + $context['primary_menu'] = Timber::get_menu('primary'); + $context['body_classes'] = get_body_class(); + $context['search_query'] = get_search_query(); + return $context; + } + + + + /* + * Twig Helper Methods + */ + + /** + * Add a Twig helper that implements Twig filters and/or functions to the + * Twig environment that Timber uses to render views. + * + * @param Twig\HelperInterface $helper any instance of a + * Twig\HelperInterface that implements the functions/filters to add + */ + public function add_twig_helper(Twig\HelperInterface $helper) { + add_filter('timber/twig', function(Environment $twig) use ($helper) { + return $this->get_twig_with_helper($twig, $helper); + }); + } + + /** + * Add any filters/functions implemented by `$helper` to the Twig instance + * `$twig`. + * + * @param Environment $twig the Twig environment to add to + * @param Twig\HelperInterface $helper the helper instance that implements the + * filters/functions to add + * @return Environment + */ + public function get_twig_with_helper( + Environment $twig, + Twig\HelperInterface $helper + ) : Environment { + // add Twig filters + foreach ( $helper->get_filters() as $name => $callable ) { + $filter = new TwigFilter( $name, $callable ); + $twig->addFilter( $filter ); + } + + // add Twig functions + foreach ( $helper->get_functions() as $name => $callable ) { + $function = new TwigFunction( $name, $callable ); + $twig->addFunction( $function ); + } + + return $twig; + } + + /** + * Tell Timber/Twig which directories to look in for Twig view files. + * + * @see set_view_directory_cascade + */ + public function configure_twig_view_cascade() { + add_filter('timber/locations', function($dirs) { + $dirList = array_merge($this->get_view_directory_cascade(), $dirs); + + // The timber/loader/paths filter wants an array of arrays + return array_map(function($x) { + return is_array($x) ? $x : [$x]; + }, $dirList); + }); + } + + /** + * Load Twig's String Loader and Debug extensions + */ + public function configure_default_twig_extensions() { + add_filter('timber/twig', function(Environment $twig) { + $loadedExtensions = array_keys($twig->getExtensions()); + + // load default extensions unless they've been loaded already + // Note: in order for Twig_Extension_Debug's dump() function to work, + // the WP_DEBUG constant must be set to true in wp-config.php + foreach (static::DEFAULT_TWIG_EXTENSIONS as $extClass) { + if (!in_array($extClass, $loadedExtensions, true)) { + $twig->addExtension(new $extClass()); } - - // add Twig functions - foreach ( $helper->get_functions() as $name => $callable ) { - $function = new TwigFunction( $name, $callable ); - $twig->addFunction( $function ); + } + + return $twig; + }); + } + + /** + * Tell Conifer to add its default Twig functions when loading + * the Twig environment, before rendering a view + */ + public function add_default_twig_helpers() { + $this->add_twig_helper(new Twig\WordPressHelper()); + $this->add_twig_helper(new Twig\ImageHelper()); + $this->add_twig_helper(new Twig\NumberHelper()); + $this->add_twig_helper(new Twig\TextHelper()); + $this->add_twig_helper(new Twig\TermHelper()); + $this->add_twig_helper(new Twig\FormHelper()); + } + + /** + * Add a Conifer helper widget to the admin dashboard + */ + public function configure_default_admin_dashboard_widgets() { + add_action('wp_dashboard_setup', function() { + // TODO widget API? + wp_add_dashboard_widget( + 'conifer_guide', + __('Welcome to Conifer'), + function() { + Timber::render('admin/welcome-to-conifer-widget.twig'); } - - return $twig; - } - - /** - * Tell Timber/Twig which directories to look in for Twig view files. - * - * @see set_view_directory_cascade - */ - public function configure_twig_view_cascade(): void { - add_filter('timber/locations', function ($dirs ): array { - $dirList = array_merge($this->get_view_directory_cascade(), $dirs); - - // The timber/loader/paths filter wants an array of arrays - return array_map(fn($x ): array => is_array($x) ? $x : [ $x ], $dirList); - }); - } - - /** - * Load Twig's String Loader and Debug extensions - */ - public function configure_default_twig_extensions(): void { - add_filter('timber/twig', function (Environment $twig ): \Twig\Environment { - $loadedExtensions = array_keys($twig->getExtensions()); - - // load default extensions unless they've been loaded already - // Note: in order for Twig_Extension_Debug's dump() function to work, - // the WP_DEBUG constant must be set to true in wp-config.php - foreach (static::DEFAULT_TWIG_EXTENSIONS as $extClass) { - if (!in_array($extClass, $loadedExtensions, true)) { - $twig->addExtension(new $extClass()); - } - } - - return $twig; - }); - } - - /** - * Tell Conifer to add its default Twig functions when loading - * the Twig environment, before rendering a view - */ - public function add_default_twig_helpers(): void { - $this->add_twig_helper(new Twig\WordPressHelper()); - $this->add_twig_helper(new Twig\ImageHelper()); - $this->add_twig_helper(new Twig\NumberHelper()); - $this->add_twig_helper(new Twig\TextHelper()); - $this->add_twig_helper(new Twig\TermHelper()); - $this->add_twig_helper(new Twig\FormHelper()); - } - - /** - * Add a Conifer helper widget to the admin dashboard - */ - public function configure_default_admin_dashboard_widgets(): void { - add_action('wp_dashboard_setup', function (): void { - // TODO widget API? - wp_add_dashboard_widget( - 'conifer_guide', - __('Welcome to Conifer'), - function (): void { - Timber::render('admin/welcome-to-conifer-widget.twig'); - } - ); - }); - } - - /** - * Remove the Conifer widget from the dashboard - */ - public function remove_conifer_widget(): void { - add_action('wp_dashboard_setup', function (): void { - // TODO widget API? - remove_meta_box('conifer_guide', 'dashboard', 'normal'); - }); - } - - /** - * Enable hotkey-based navigation on the WP dashboard - */ - public function enable_admin_hotkeys(): void { - add_action('admin_enqueue_scripts', function (): void { - $this->enqueue_script('conifer-admin-hotkeys', 'admin/conifer-admin.js'); - - // allow overriding/customizing hotkeys - wp_localize_script( - 'conifer-admin-hotkeys', - 'CUSTOM_HOTKEY_LOCATIONS', - $this->get_custom_admin_hotkeys() - ); - }); - } - - /** - * Get user-defined admin hotkeys that will override defaults - */ - public function get_custom_admin_hotkeys(): array { - return $this->custom_admin_hotkeys; - } - - /** - * Override the default admin hotkeys - */ - public function set_custom_admin_hotkeys(array $hotkeys ): void { - $this->custom_admin_hotkeys = $hotkeys; - } - - /** - * Disable hotkey-based navigation on the WP dashboard - */ - public function disable_admin_hotkeys(): void { - add_action('admin_enqueue_scripts', function (): void { - wp_dequeue_script('conifer-admin-hotkeys'); - }); - } - - - /** - * Get the array of directories where Twig should look for view files. - * - * @return array - */ - public function get_view_directory_cascade(): array { - return $this->view_directory_cascade; - } - - /** - * Get the array of directories where Conifer will look for JavaScript files - * when `Site::enqueue_script()` is called. - * - * @return array - */ - public function get_script_directory_cascade(): array { - return $this->script_directory_cascade; - } - - /** - * Get the array of directories where Conifer will look for CSS files - * when `Site::enqueue_style()` is called. - * - * @return array - */ - public function get_style_directory_cascade(): array { - return $this->style_directory_cascade; - } - - /** - * Set the array of directories where Twig should look for view files - * when `render` or `compile` is called. - * - * *NOTE: This will have no effect without also running - * `configure_twig_view_cascade`, or equivalent.* - * - * @param array the list of directories to check. - */ - public function set_view_directory_cascade(array $cascade ): void { - $this->view_directory_cascade = $cascade; - } - - /** - * Set the array of directories where Conifer will look for CSS files - * when `Site::enqueue_style()` is called. - * - * @param array the list of directories to check. Conifer checks directories - * in the order declared. - */ - public function set_script_directory_cascade(array $cascade ): void { - $this->script_directory_cascade = $cascade; - } - - /** - * Set the array of directories where Conifer will look for CSS files - * when `Site::enqueue_style()` is called. - * - * @param array the list of directories to check. Conifer checks directories - * in the order declared. - */ - public function set_style_directory_cascade(array $cascade ): void { - $this->style_directory_cascade = $cascade; - } - - /** - * Get the full URI for a script file. Returns the URI for the first file - * it finds in the script directory cascade. - * - * @param string $file the base file name - * @return the script's full URI. If $file is not found in any - * directory, returns the empty string. - */ - public function get_script_uri( string $file ): string { - $path = $this->find_file($file, $this->script_directory_cascade); - if ($path !== '') { - return URLHelper::file_system_to_url($path); - } - - return ''; - } - - /** - * Get the full URI for a stylesheet. Returns the URI for the first file - * it finds in the style directory cascade. - * - * @param string $file the base file name - * @return the stylesheet's full URI. If $file is not found in any - * directory, returns the empty string. - */ - public function get_stylesheet_uri( string $file ): string { - $path = $this->find_file($file, $this->style_directory_cascade); - if ($path !== '') { - return URLHelper::file_system_to_url($path); - } - - return ''; - } - - /** - * Search an arbitrary list of directories for $file and return the first - * existent file path found - * - * @param string $file the filename to search for in $dirs - * @param array $dirs an array of directories to search for $file - * @return the path of the first file found. If $file is not found in any - * directory, returns the empty string. - */ - public function find_file(string $file, array $dirs ): string { - foreach ($dirs as $dir) { - // add trailing slash if necessary - if (!str_ends_with((string) $dir, '/')) { - $dir .= '/'; - } - - if (file_exists($dir . $file)) { - return $dir . $file; - } - } - - return ''; - } - - /** - * Get the build-tool-generated hash for assets - * - * @param string $filepath Optional filepath whose contents will be used - * in the cache-busting query string. Defaults to "assets.version" - * @return the hash for - */ - public function get_assets_version($filepath = 'assets.version' ): string { - - $this->assets_version ??= []; - - if ( - !isset($this->assets_version[$filepath]) - && is_readable($this->get_theme_file($filepath)) - ) { + ); + }); + } + + /** + * Remove the Conifer widget from the dashboard + */ + public function remove_conifer_widget() { + add_action('wp_dashboard_setup', function() { + // TODO widget API? + remove_meta_box('conifer_guide', 'dashboard', 'normal'); + }); + } + + /** + * Enable hotkey-based navigation on the WP dashboard + */ + public function enable_admin_hotkeys() { + add_action('admin_enqueue_scripts', function() { + $this->enqueue_script('conifer-admin-hotkeys', 'admin/conifer-admin.js'); + + // allow overriding/customizing hotkeys + wp_localize_script( + 'conifer-admin-hotkeys', + 'CUSTOM_HOTKEY_LOCATIONS', + $this->get_custom_admin_hotkeys() + ); + }); + } + + /** + * Get user-defined admin hotkeys that will override defaults + */ + public function get_custom_admin_hotkeys() : array { + return $this->custom_admin_hotkeys; + } + + /** + * Override the default admin hotkeys + */ + public function set_custom_admin_hotkeys(array $hotkeys) { + $this->custom_admin_hotkeys = $hotkeys; + } + + /** + * Disable hotkey-based navigation on the WP dashboard + */ + public function disable_admin_hotkeys() { + add_action('admin_enqueue_scripts', function() { + wp_dequeue_script('conifer-admin-hotkeys'); + }); + } + + + /** + * Get the array of directories where Twig should look for view files. + * + * @return array + */ + public function get_view_directory_cascade() : array { + return $this->view_directory_cascade; + } + + /** + * Get the array of directories where Conifer will look for JavaScript files + * when `Site::enqueue_script()` is called. + * + * @return array + */ + public function get_script_directory_cascade() : array { + return $this->script_directory_cascade; + } + + /** + * Get the array of directories where Conifer will look for CSS files + * when `Site::enqueue_style()` is called. + * + * @return array + */ + public function get_style_directory_cascade() : array { + return $this->style_directory_cascade; + } + + /** + * Set the array of directories where Twig should look for view files + * when `render` or `compile` is called. + * + * *NOTE: This will have no effect without also running + * `configure_twig_view_cascade`, or equivalent.* + * + * @param array the list of directories to check. + */ + public function set_view_directory_cascade(array $cascade) { + $this->view_directory_cascade = $cascade; + } + + /** + * Set the array of directories where Conifer will look for CSS files + * when `Site::enqueue_style()` is called. + * + * @param array the list of directories to check. Conifer checks directories + * in the order declared. + */ + public function set_script_directory_cascade(array $cascade) { + $this->script_directory_cascade = $cascade; + } + + /** + * Set the array of directories where Conifer will look for CSS files + * when `Site::enqueue_style()` is called. + * + * @param array the list of directories to check. Conifer checks directories + * in the order declared. + */ + public function set_style_directory_cascade(array $cascade) { + $this->style_directory_cascade = $cascade; + } + + /** + * Get the full URI for a script file. Returns the URI for the first file + * it finds in the script directory cascade. + * + * @param string $file the base file name + * @return the script's full URI. If $file is not found in any + * directory, returns the empty string. + */ + public function get_script_uri( string $file ) : string { + $path = $this->find_file($file, $this->script_directory_cascade); + if ($path) { + return URLHelper::file_system_to_url($path); + } + + return ''; + } + + /** + * Get the full URI for a stylesheet. Returns the URI for the first file + * it finds in the style directory cascade. + * + * @param string $file the base file name + * @return the stylesheet's full URI. If $file is not found in any + * directory, returns the empty string. + */ + public function get_stylesheet_uri( string $file ) : string { + $path = $this->find_file($file, $this->style_directory_cascade); + if ($path) { + return URLHelper::file_system_to_url($path); + } + + return ''; + } + + /** + * Search an arbitrary list of directories for $file and return the first + * existent file path found + * + * @param string $file the filename to search for in $dirs + * @param array $dirs an array of directories to search for $file + * @return the path of the first file found. If $file is not found in any + * directory, returns the empty string. + */ + public function find_file(string $file, array $dirs) : string { + foreach ($dirs as $dir) { + // add trailing slash if necessary + if (substr($dir, -1) !== '/') { + $dir .= '/'; + } + + if (file_exists($dir . $file)) { + return $dir . $file; + } + } + + return ''; + } + + /** + * Get the build-tool-generated hash for assets + * + * @param string $filepath Optional filepath whose contents will be used + * in the cache-busting query string. Defaults to "assets.version" + * @return the hash for + */ + public function get_assets_version($filepath = 'assets.version') : string { + + $this->assets_version = $this->assets_version ?? []; + + if ( + !isset($this->assets_version[$filepath]) + && is_readable($this->get_theme_file($filepath)) + ) { // phpcs:ignore WordPress.WP.AlternativeFunctions - $version = trim(file_get_contents($this->get_theme_file($filepath))); - - $this->assets_version[$filepath] = $version; - + $version = trim(file_get_contents($this->get_theme_file($filepath))); + + $this->assets_version[$filepath] = $version; + + } + + return $this->assets_version[$filepath] ?? ''; + } + + /** + * Get the filepath to the assets version file + * + * @return string the absolute path to the assets version file + */ + public function get_assets_version_filepath() : string { + return $this->get_theme_file('assets.version'); + } + + /** + * Get an arbitrary file, relative to the theme directory + * + * @return string the absolute path to the file + */ + public function get_theme_file(string $file) : string { + // ensure leading slash + if ($file[0] !== '/') { + $file = "/$file"; + } + return get_stylesheet_directory() . $file; + } + + /** + * Disable all comment functionality across the site. + */ + public function disable_comments() { + add_action('admin_init', function() { + global $pagenow; + + if ($pagenow === 'edit-comments.php') { + // TODO https://github.com/sitecrafting/conifer/issues/139 + // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect + wp_redirect(admin_url()); + exit; + } + + // Remove comments metabox from dashboard + remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); + + // Disable support for comments and trackbacks in post types + foreach (get_post_types() as $post_type) { + if (post_type_supports($post_type, 'comments')) { + remove_post_type_support($post_type, 'comments'); + remove_post_type_support($post_type, 'trackbacks'); } - - return $this->assets_version[$filepath] ?? ''; - } - - /** - * Get the filepath to the assets version file - * - * @return string the absolute path to the assets version file - */ - public function get_assets_version_filepath(): string { - return $this->get_theme_file('assets.version'); - } - - /** - * Get an arbitrary file, relative to the theme directory - * - * @return string the absolute path to the file - */ - public function get_theme_file(string $file ): string { - // ensure leading slash - if ($file[0] !== '/') { - $file = '/' . $file; - } - - return get_stylesheet_directory() . $file; - } - - /** - * Disable all comment functionality across the site. - */ - public function disable_comments(): void { - add_action('admin_init', function (): void { - global $pagenow; - - if ($pagenow === 'edit-comments.php') { - // TODO https://github.com/sitecrafting/conifer/issues/139 - // phpcs:ignore WordPress.Security.SafeRedirect.wp_redirect_wp_redirect - wp_redirect(admin_url()); - exit; - } - - // Remove comments metabox from dashboard - remove_meta_box('dashboard_recent_comments', 'dashboard', 'normal'); - - // Disable support for comments and trackbacks in post types - foreach (get_post_types() as $post_type) { - if (post_type_supports($post_type, 'comments')) { - remove_post_type_support($post_type, 'comments'); - remove_post_type_support($post_type, 'trackbacks'); - } - } - }); - - // hide comment menu item from WP Dashboard menu - add_action('admin_menu', function (): void { - remove_menu_page('edit-comments.php'); - }); - - // hide comment menu items in WP Admin bar - add_action('wp_before_admin_bar_render', function (): void { - global $wp_admin_bar; - $wp_admin_bar->remove_menu('comments'); - }); - - // hide comments column in WP Admin - add_filter('manage_page_columns', function (array $columns ): array { - unset($columns['comments']); - return $columns; - }); - - // hide all existing comments - add_filter('comments_array', '__return_empty_array'); - - // Close comments on the frontend - add_filter('comments_open', '__return_false', 20); - add_filter('pings_open', '__return_false', 20); - } + } + }); + + // hide comment menu item from WP Dashboard menu + add_action('admin_menu', function() { + remove_menu_page('edit-comments.php'); + }); + + // hide comment menu items in WP Admin bar + add_action('wp_before_admin_bar_render', function() { + global $wp_admin_bar; + $wp_admin_bar->remove_menu('comments'); + }); + + // hide comments column in WP Admin + add_filter('manage_page_columns', function(array $columns) { + unset($columns['comments']); + return $columns; + }); + + // hide all existing comments + add_filter('comments_array', '__return_empty_array'); + + // Close comments on the frontend + add_filter('comments_open', '__return_false', 20); + add_filter('pings_open', '__return_false', 20); + } } diff --git a/lib/Conifer/Twig/FormHelper.php b/lib/Conifer/Twig/FormHelper.php index a1d574a..a0f35ae 100644 --- a/lib/Conifer/Twig/FormHelper.php +++ b/lib/Conifer/Twig/FormHelper.php @@ -1,11 +1,8 @@ <?php - /** * Custom Twig filters for front-end forms */ -declare(strict_types=1); - namespace Conifer\Twig; use Conifer\Form\AbstractBase as Form; @@ -17,104 +14,105 @@ * @author Coby Tamayo */ class FormHelper implements HelperInterface { - /** - * Get the Twig functions to register - * - * @return array an associative array of callback functions, keyed by name - */ - public function get_filters(): array { - return [ - 'field_class' => $this->get_field_class(...), - 'error_messages_for' => $this->get_error_messages_for(...), - 'err' => $this->get_error_messages_for(...), - 'checked_attr' => $this->checked_attr(...), - 'selected_attr' => $this->selected_attr(...), - ]; - } + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_filters() : array { + return [ + 'field_class' => [$this, 'get_field_class'], + 'error_messages_for' => [$this, 'get_error_messages_for'], + 'err' => [$this, 'get_error_messages_for'], + 'checked_attr' => [$this, 'checked_attr'], + 'selected_attr' => [$this, 'selected_attr'], + ]; + } - /** - * Does not supply any additional Twig functions. - * - * @return array{} - */ - public function get_functions(): array { - return []; - } + /** + * Does not supply any additional Twig functions. + * + * @return array + */ + public function get_functions() : array { + return []; + } - /** - * Get the class to render for the form field, based on its error state - * - * @param \Conifer\Form\AbstractBase $form a form object - * @param string $fieldName the name of the field being rendered - * @param string $errorClass the class to give this field if it has errors - * @return string the HTML class(es) to render - */ - public function get_field_class( - Form $form, - string $fieldName, - string $errorClass = 'error' - ): string { - return $form->get_errors_for($fieldName) - ? $errorClass - : ''; - } + /** + * Get the class to render for the form field, based on its error state + * + * @param \Conifer\Form\AbstractBase $form a form object + * @param string $fieldName the name of the field being rendered + * @param string $errorClass the class to give this field if it has errors + * @return string the HTML class(es) to render + */ + public function get_field_class( + Form $form, + string $fieldName, + string $errorClass = 'error' + ) : string { + return $form->get_errors_for($fieldName) + ? $errorClass + : ''; + } - /** - * Get the error messages for a specific field only, as a concatenated string - * with line breaks between by default - * - * @param Form $form the Form being processed - * @param string $fieldName the name of the field whose error messages we want - * @param string $separator the separator to place between multiple errors - * @return string - */ - public function get_error_messages_for( - Form $form, - string $fieldName, - string $separator = '<br>' - ): string { - return implode($separator, $form->get_error_messages_for($fieldName)); - } + /** + * Get the error messages for a specific field only, as a concatenated string + * with line breaks between by default + * + * @param Form $form the Form being processed + * @param string $fieldName the name of the field whose error messages we want + * @param string $separator the separator to place between multiple errors + * @return string + */ + public function get_error_messages_for( + Form $form, + string $fieldName, + string $separator = '<br>' + ) : string { + return implode($separator, $form->get_error_messages_for($fieldName)); + } - /** - * Return the `checked` attribute for a given form input, given the - * (hydrated) form and the field name, and optionally the value to check - * against. - * - * @param Form $form the Form object containing the field in question - * @param string $fieldName the `name` of the field - * @param string $value (optional) the value to check against. This is - * necessary e.g. for radio inputs, where there's more than one possible - * value. - * @return string literally `" checked "` if the field (optionally the one - * matching `$value`) was checked, or the empty string - */ - public function checked_attr( - Form $form, - string $fieldName, - string $value = null - ): string { - return $form->checked($fieldName, $value) ? ' checked ' : ''; - } + /** + * Return the `checked` attribute for a given form input, given the + * (hydrated) form and the field name, and optionally the value to check + * against. + * + * @param Form $form the Form object containing the field in question + * @param string $fieldName the `name` of the field + * @param string $value (optional) the value to check against. This is + * necessary e.g. for radio inputs, where there's more than one possible + * value. + * @return string literally `" checked "` if the field (optionally the one + * matching `$value`) was checked, or the empty string + */ + public function checked_attr( + Form $form, + string $fieldName, + string $value = null + ) : string { + return $form->checked($fieldName, $value) ? ' checked ' : ''; + } - /** - * Return the `selected` attribute for a given form input, given the - * (hydrated) form and the field name, and optionally the value to check - * against. - * - * @param Form $form the Form object containing the field in question - * @param string $fieldName the `name` of the field - * @param string $value (optional) the value to check against. This is - * necessary e.g. for radio inputs, where there's more than one possible - * value. - * @return string literally `" selected "` if the field (optionally the one - * matching `$value`) was selected, or the empty string - */ - public function selected_attr( - Form $form, - string $fieldName, - string $value - ): string { - return $form->selected($fieldName, $value) ? ' selected ' : ''; - } + /** + * Return the `selected` attribute for a given form input, given the + * (hydrated) form and the field name, and optionally the value to check + * against. + * + * @param Form $form the Form object containing the field in question + * @param string $fieldName the `name` of the field + * @param string $value (optional) the value to check against. This is + * necessary e.g. for radio inputs, where there's more than one possible + * value. + * @return string literally `" selected "` if the field (optionally the one + * matching `$value`) was selected, or the empty string + */ + public function selected_attr( + Form $form, + string $fieldName, + string $value + ) : string { + return $form->selected($fieldName, $value) ? ' selected ' : ''; + } } + diff --git a/lib/Conifer/Twig/HelperInterface.php b/lib/Conifer/Twig/HelperInterface.php index 65654a9..ccf7141 100644 --- a/lib/Conifer/Twig/HelperInterface.php +++ b/lib/Conifer/Twig/HelperInterface.php @@ -1,11 +1,8 @@ <?php - /** * Interface for declarative, OO Twig functions and filters */ -declare(strict_types=1); - namespace Conifer\Twig; /** @@ -77,19 +74,19 @@ * @author Coby Tamayo */ interface HelperInterface { - /** - * Get the Twig functions implemented by this helper, keyed by the function - * name to call from Twig views - * - * @return array - */ - public function get_functions(): array; + /** + * Get the Twig functions implemented by this helper, keyed by the function + * name to call from Twig views + * + * @return array + */ + public function get_functions() : array; - /** - * Get the Twig filters implemented by this helper, keyed by the filter - * name to call from Twig views - * - * @return array - */ - public function get_filters(): array; + /** + * Get the Twig filters implemented by this helper, keyed by the filter + * name to call from Twig views + * + * @return array + */ + public function get_filters() : array; } diff --git a/lib/Conifer/Twig/ImageHelper.php b/lib/Conifer/Twig/ImageHelper.php index 5f93132..2aceb7c 100644 --- a/lib/Conifer/Twig/ImageHelper.php +++ b/lib/Conifer/Twig/ImageHelper.php @@ -1,14 +1,10 @@ <?php - /** * Custom Twig functions for dealing with images */ -declare(strict_types=1); - namespace Conifer\Twig; -use Closure; use Timber\ImageHelper as TimberImageHelper; /** @@ -17,125 +13,123 @@ * @package Conifer */ class ImageHelper implements HelperInterface { - /** - * Does not supply any additional Twig functions. - * - * @return Closure[] an associative array of callback functions, keyed by name - */ - public function get_functions(): array { - return [ - 'generate_retina_srcset' => $this->generate_retina_srcset(...), - ]; + /** + * Does not supply any additional Twig functions. + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_functions() : array { + return [ + 'generate_retina_srcset' => [$this, 'generate_retina_srcset'], + ]; + } + + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_filters() : array { + return [ + 'src_to_retina' => [$this, 'src_to_retina'], + 'src_to_retina_at_multiplier' => [$this, 'src_to_retina_at_multiplier'], + ]; + } + + /** + * Convert the image URL `$src` to its retina equivalent + * + * @param `$src` the original src URL + * @return string the retina version of `$src` + */ + public function src_to_retina(string $src) : string { + // greedily find the last dot + return preg_replace('~(\.[a-z]+)$~i', '@2x$1', $src); + } + + /** + * Convert the image URL `$src` to its retina equivalent at given multiplier. + * Makes sure the file exists. + * + * @param `$src` the original src URL + * @param `$multiplier` the multiplier for the retina image + * @return string the retina src + */ + public function src_to_retina_at_multiplier(?string $src, int $multiplier = 2) : string { + + if (!$src) { + return ''; } - /** - * Get the Twig functions to register - * - * @return array<string, Closure> an associative array of callback functions, keyed by name - */ - public function get_filters(): array { - return [ - 'src_to_retina' => $this->src_to_retina(...), - 'src_to_retina_at_multiplier' => $this->src_to_retina_at_multiplier(...), - ]; - } + // get the path of the original file + $file = TimberImageHelper::get_server_location($src); - /** - * Convert the image URL `$src` to its retina equivalent - * - * @param string $src the original src URL - * @return string the retina version of `$src` - */ - public function src_to_retina(string $src ): string { - // greedily find the last dot - return preg_replace('~(\.[a-z]+)$~i', '@2x$1', $src); + // bail if the file doesnt exist. No point continuing on. + if (!file_exists($file)) { + return ''; } - /** - * Convert the image URL `$src` to its retina equivalent at given multiplier. - * Makes sure the file exists. - * - * @param ?string $src the original src URL - * @param int $multiplier the multiplier for the retina image - * @return string the retina src - */ - public function src_to_retina_at_multiplier(?string $src, int $multiplier = 2 ): string { + // return base $src if multiplier less than 2 + if ($multiplier < 2) { + return $src; + } - if (!$src) { - return ''; - } + $image = preg_replace('~(\.[a-z]+)$~i', "@{$multiplier}x$1", $src); - // get the path of the original file - $file = TimberImageHelper::get_server_location($src); + $file = TimberImageHelper::get_server_location($image); - // bail if the file doesnt exist. No point continuing on. - if (!file_exists($file)) { - return ''; - } + // return image src if file exists + if (file_exists($file)) { + return $image; + } - // return base $src if multiplier less than 2 - if ($multiplier < 2) { - return $src; - } + return ''; + + } + + /** + * Convert the image URL `$src` srcset string up to given size multiplier. + * Will return srcset with files that exist. + * + * @param `$src` the original src URL + * @param `$max_multiplier` the max multiplier for the set + * @return string the retina srcset + */ + public function generate_retina_srcset(?string $src, int $max_multiplier = 2) : string { + if (!$src) { + return ''; + } - $image = preg_replace('~(\.[a-z]+)$~i', sprintf('@%dx$1', $multiplier), $src); + $set = []; - $file = TimberImageHelper::get_server_location($image); + // bail if $max_multiplier < 2. We don't need srcset. + if ($max_multiplier < 2) { + return ''; + } - // return image src if file exists - if (file_exists($file)) { - return $image; - } + // get the path of the original file + $file = TimberImageHelper::get_server_location($src); - return ''; + // bail if the file doesnt exist. No point continuing on. + if (!file_exists($file)) { + return ''; } - /** - * Convert the image URL `$src` srcset string up to given size multiplier. - * Will return srcset with files that exist. - * - * @param ?string $src the original src URL - * @param int $max_multiplier the max multiplier for the set - * @return string the retina srcset - */ - public function generate_retina_srcset(?string $src, int $max_multiplier = 2 ): string { - - if (!$src) { - return ''; - } - - $set = []; - - // bail if $max_multiplier < 2. We don't need srcset. - if ($max_multiplier < 2) { - return ''; - } - - // get the path of the original file - $file = TimberImageHelper::get_server_location($src); - - // bail if the file doesnt exist. No point continuing on. - if (!file_exists($file)) { - return ''; - } - - // add to the set - $set[] = $src; - - // add additional retna image sizes if they exist - $count = 2; - do { - $image = preg_replace('~(\.[a-z]+)$~i', '@' . $count . 'x$1', $src); - $file = TimberImageHelper::get_server_location($image); - - if (file_exists($file)) { - $set[] = $image . sprintf(' %dx', $count); - } - - ++$count; - } while ($count < $max_multiplier); - - // return srcset or empty string if retina images don't exist - return count($set)>1 ? 'srcset="' . implode(', ', $set) . '"': ''; - } + // add to the set + array_push($set, $src); + + // add additional retna image sizes if they exist + $count = 2; + do { + $image = preg_replace('~(\.[a-z]+)$~i', '@' . $count . 'x$1', $src); + $file = TimberImageHelper::get_server_location($image); + + if (file_exists($file)) { + array_push($set, $image . " {$count}x"); + } + $count++; + } while ($count < $max_multiplier); + // return srcset or empty string if retina images don't exist + return count($set)>1 ? 'srcset="' . implode(', ', $set) . '"': ''; + } } diff --git a/lib/Conifer/Twig/NumberHelper.php b/lib/Conifer/Twig/NumberHelper.php index 7286ead..9759360 100644 --- a/lib/Conifer/Twig/NumberHelper.php +++ b/lib/Conifer/Twig/NumberHelper.php @@ -1,11 +1,8 @@ <?php - /** * Custom Twig filters for formatting numbers */ -declare(strict_types=1); - namespace Conifer\Twig; /** @@ -14,44 +11,44 @@ * @package Conifer */ class NumberHelper implements HelperInterface { - /** - * Get the Twig functions to register - * - * @return \Closure[] an associative array of callback functions, keyed by name - */ - public function get_filters(): array { - return [ - 'us_phone' => $this->us_phone(...), - ]; - } - - /** - * Does not supply any additional Twig functions. - * - * @return array{} - */ - public function get_functions(): array { - return []; + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_filters() : array { + return [ + 'us_phone' => [$this, 'us_phone'], + ]; + } + + /** + * Does not supply any additional Twig functions. + * + * @return array + */ + public function get_functions() : array { + return []; + } + + /** + * Filter any 10-digit number into a formatted US phone number + * + * @param string $phone a string of digits. Must be a 10-character string of + * digits (with an optional leading "1") or it won't filter anything. + * @return string the formatted phone number + */ + public function us_phone( $phone ) { + // Capture pieces of the number + $matches = []; + preg_match( '/^1?(\d\d\d)(\d\d\d)(\d\d\d\d)$/', $phone, $matches ); + + // If we have the correct number of digits, format it out... + if ( count($matches) === 4 ) { + $phone = "({$matches[1]}) {$matches[2]}-{$matches[3]}"; } - /** - * Filter any 10-digit number into a formatted US phone number - * - * @param string $phone a string of digits. Must be a 10-character string of - * digits (with an optional leading "1") or it won't filter anything. - * @return string the formatted phone number - */ - public function us_phone( $phone ) { - // Capture pieces of the number - $matches = []; - preg_match( '/^1?(\d\d\d)(\d\d\d)(\d\d\d\d)$/', $phone, $matches ); - - // If we have the correct number of digits, format it out... - if ( count($matches) === 4 ) { - $phone = sprintf('(%s) %s-%s', $matches[1], $matches[2], $matches[3]); - } - - // Return the phone number, formatted or not - return $phone; - } + // Return the phone number, formatted or not + return $phone; + } } diff --git a/lib/Conifer/Twig/TermHelper.php b/lib/Conifer/Twig/TermHelper.php index 5f07958..67599f6 100644 --- a/lib/Conifer/Twig/TermHelper.php +++ b/lib/Conifer/Twig/TermHelper.php @@ -1,11 +1,8 @@ <?php - /** * Twig filters for WP terms */ -declare(strict_types=1); - namespace Conifer\Twig; use Timber\Term; @@ -17,44 +14,44 @@ * @package Conifer */ class TermHelper implements HelperInterface { - /** - * Get the Twig functions to register - * - * @return array an associative array of callback functions, keyed by name - */ - public function get_filters(): array { - return [ - 'term_item_class' => $this->term_item_class(...), - ]; - } - - /** - * Does not supply any additional Twig functions. - * - * @return array{} - */ - public function get_functions(): array { - return []; - } - - /** - * Filters the given term into a class for an <li>; considers $term "current" if - * $currentPostOrArchive is a TimberTerm instance (meaning we're on an archive page - * for that term), and it represents the same term as $term. - * - * @param Term $term the term for the <li> currently being rendered - * @param TimberCoreInterface $currentPostOrArchive the post or term representing - * the current archive page (e.g. a category listing) - * @return string the formatted phone number - */ - public function term_item_class( Term $term, TimberCoreInterface $currentPostOrArchive ): string { - // If $postOrArchive is a TimberTerm instance, we're on an archive page for that term. - // In that case, compare it to $term to get the class(es) for the <li> being rendered - $currentTermItem = ($currentPostOrArchive instanceof Term) - && $term->ID === $currentPostOrArchive->ID; - - return $currentTermItem - ? 'current-menu-item' - : ''; - } + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_filters() : array { + return [ + 'term_item_class' => [$this, 'term_item_class'], + ]; + } + + /** + * Does not supply any additional Twig functions. + * + * @return array + */ + public function get_functions() : array { + return []; + } + + /** + * Filters the given term into a class for an <li>; considers $term "current" if + * $currentPostOrArchive is a TimberTerm instance (meaning we're on an archive page + * for that term), and it represents the same term as $term. + * + * @param TimberTerm $term the term for the <li> currently being rendered + * @param TimberCoreInterface $currentPostOrArchive the post or term representing + * the current archive page (e.g. a category listing) + * @return string the formatted phone number + */ + public function term_item_class( Term $term, TimberCoreInterface $currentPostOrArchive ) { + // If $postOrArchive is a TimberTerm instance, we're on an archive page for that term. + // In that case, compare it to $term to get the class(es) for the <li> being rendered + $currentTermItem = ($currentPostOrArchive instanceof Term) + && $term->ID === $currentPostOrArchive->ID; + + return $currentTermItem + ? 'current-menu-item' + : ''; + } } diff --git a/lib/Conifer/Twig/TextHelper.php b/lib/Conifer/Twig/TextHelper.php index 9120774..a3cfaf4 100644 --- a/lib/Conifer/Twig/TextHelper.php +++ b/lib/Conifer/Twig/TextHelper.php @@ -1,128 +1,125 @@ <?php - /** * Custom Twig filters for manipulating text */ -declare(strict_types=1); - namespace Conifer\Twig; -use Closure; - /** * Twig Wrapper for helpful linguistic filters, such as pluralize * * @package Conifer */ class TextHelper implements HelperInterface { - /** - * Plural inflections for English words - * TODO public static function add_plurals(array $plurals) - * - * @var array<string, string> - */ - protected static array $plurals = [ + /** + * Plural inflections for English words + * TODO public static function add_plurals(array $plurals) + * + * @var array + */ + protected static $plurals = [ 'person' => 'people', + ]; + + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_filters() : array { + return [ + 'oxford_comma' => [$this, 'oxford_comma'], + 'pluralize' => [$this, 'pluralize'], + 'capitalize_each' => [$this, 'capitalize_each'], ]; + } - /** - * Get the Twig functions to register - * - * @return array<string, Closure> an associative array of callback functions, keyed by name - */ - public function get_filters(): array { - return [ - 'oxford_comma' => $this->oxford_comma(...), - 'pluralize' => $this->pluralize(...), - 'capitalize_each' => $this->capitalize_each(...), - ]; - } + /** + * Does not supply any additional Twig functions. + * + * @return array + */ + public function get_functions() : array { + return []; + } - /** - * Does not supply any additional Twig functions. - * - * @return array{} - */ - public function get_functions(): array { - return []; + /** + * Pluralize the given noun, if $n is anything other than 1 + * + * @param string $noun the noun to pluralize (or not) + * @param int $n the number of the given nouns, so that we know whether to pluralize + * @return string the noun, pluralized or not according to $n + */ + public function pluralize( $noun, $n ) { + if ($n !== 1) { + $noun = isset(static::$plurals[$noun]) + ? static::$plurals[$noun] + : $noun . 's'; } - /** - * Pluralize the given noun, if $n is anything other than 1 - * - * @param string $noun the noun to pluralize (or not) - * @param int $n the number of the given nouns, so that we know whether to pluralize - * @return string the noun, pluralized or not according to $n - */ - public function pluralize( $noun, $n ) { - if ($n !== 1) { - $noun = static::$plurals[$noun] ?? $noun . 's'; - } - - return $noun; - } + return $noun; + } - /** - * Returns a human-readable list of things. Uses the Oxford comma convention - * for listing three or more things. - * - * @param array<int, mixed> $items an array of strings - * @return string - */ - public function oxford_comma( array $items ) { - switch (count($items)) { - case 0: - $list = ''; + /** + * Returns a human-readable list of things. Uses the Oxford comma convention + * for listing three or more things. + * + * @param array $items an array of strings + * @return string + */ + public function oxford_comma( array $items ) { + switch (count($items)) { + case 0: + $list = ''; break; - case 1: - $list = $items[0]; + case 1: + $list = $items[0]; break; - case 2: - $list = $items[0] . ' and ' . $items[1]; + case 2: + $list = $items[0] . ' and ' . $items[1]; break; - default: - $last = array_splice($items, -1)[0]; - $list = implode(', ', $items) . ', and ' . $last; - } - - return $list; + default: + $last = array_splice($items, -1)[0]; + $list = implode(', ', $items) . ', and ' . $last; } - /** - * Capitalize each word in the given $phrase, other than "small" words such - * as "a," "the," etc. - * - * @param string $phrase a string to capitalize each word of - * @param array<string, mixed> $options an optional array of options including: - * - `small_words`: words not to capitalize. Defaults to the normal rules - * of the English language. - * - `split_by`: the delimiter for splitting out words when calling - * `explode()`. Defaults to a single space, i.e. `" "`. - * * @return string the capitalized string, e.g. "The Old Man and the Sea" - */ - public function capitalize_each(string $phrase, array $options = [] ): string { + return $list; + } + + /** + * Capitalize each word in the given $phrase, other than "small" words such + * as "a," "the," etc. + * + * @param string $phrase a string to capitalize each word of + * @param array $options an optional array of options including: + * - `small_words`: words not to capitalize. Defaults to the normal rules + * of the English language. + * - `split_by`: the delimiter for splitting out words when calling + * `explode()`. Defaults to a single space, i.e. `" "`. + * @return the capitalized string, e.g. "The Old Man and the Sea" + */ + public function capitalize_each(string $phrase, array $options = []) : string { // @codingStandardsIgnoreStart WordPress.Arrays.ArrayDeclarationSpacing $smallWords = $options['small_words'] ?? [ - 'a', 'and', 'the', 'or', 'of', 'as', 'for', 'but', 'yet', 'so', 'at', - 'around', 'by', 'after', 'along', 'from', 'on', 'to', 'with', 'without', - ]; + 'a', 'and', 'the', 'or', 'of', 'as', 'for', 'but', 'yet', 'so', 'at', + 'around', 'by', 'after', 'along', 'from', 'on', 'to', 'with', 'without', + ]; $words = explode(($options['split_by'] ?? ' '), $phrase); // capitalize each word $capitalizedWords = array_map( - function(string $word, int $i) use ($smallWords) : string { - // always capitalize the first word; capitalize other "big" words - $capitalize = $i === 0 || !in_array(strtolower($word), $smallWords, true); - return $capitalize ? ucfirst($word) : lcfirst($word); - }, + function(string $word, int $i) use ($smallWords) : string { + // always capitalize the first word; capitalize other "big" words + $capitalize = $i === 0 || !in_array(strtolower($word), $smallWords, true); + return $capitalize ? ucfirst($word) : lcfirst($word); + }, $words, array_keys($words) - ); + ); return implode(' ', $capitalizedWords); } diff --git a/lib/Conifer/Twig/WordPressHelper.php b/lib/Conifer/Twig/WordPressHelper.php index 2ac9fe5..3ff11a0 100644 --- a/lib/Conifer/Twig/WordPressHelper.php +++ b/lib/Conifer/Twig/WordPressHelper.php @@ -1,14 +1,10 @@ <?php - /** * General-purpose WordPress functions in Twig */ -declare(strict_types=1); - namespace Conifer\Twig; -use Closure; use Conifer\Post\BlogPost; use Conifer\Post\Post; @@ -21,48 +17,61 @@ * @package Conifer */ class WordPressHelper implements HelperInterface { - /** - * Get the Twig functions to register - * - * @return array<string, Closure> an associative array of callback functions, keyed by name - */ - public function get_functions(): array { - return [ - 'get_search_form' => fn() => get_search_form( false ), - 'get_blog_url' => Post::get_blog_url(...), - 'img_url' => fn($file ): string => get_stylesheet_directory_uri() . '/img/' . $file, - 'wp_nav_menu' => function ( $args ): string|false { - ob_start(); - wp_nav_menu( $args ); - return ob_get_clean(); - }, - 'paginate_links' => fn($args = [] ) => paginate_links($args), - /** - * Twig function for getting a global WP option - */ - 'get_option' => fn($name ) => get_option($name), - /** - * Like get_option, but applies ACF filters, e.g. if need to return an object. Only works with ACF-configured option fields. - */ - 'get_theme_setting' => function ($name ) { + /** + * Get the Twig functions to register + * + * @return array an associative array of callback functions, keyed by name + */ + public function get_functions() : array { + return [ + 'get_search_form' => function() { + return get_search_form( false ); + }, + 'get_blog_url' => [ '\Conifer\Post\Post', 'get_blog_url' ], + 'img_url' => function( $file ) { + return get_stylesheet_directory_uri() . '/img/' . $file; + }, + 'wp_nav_menu' => function( $args ) { + ob_start(); + wp_nav_menu( $args ); + return ob_get_clean(); + }, + 'paginate_links' => function( $args = [] ) { + return paginate_links($args); + }, + /** + * Twig function for getting a global WP option + */ + 'get_option' => function($name) { + return get_option($name); + }, + /** + * Like get_option, but applies ACF filters, e.g. if need to return an object. Only works with ACF-configured option fields. + */ + 'get_theme_setting' => function($name) { + + if (function_exists('get_field')) { + return get_field($name, 'option'); + } else { + return ''; + } - if (function_exists('get_field')) { - return get_field($name, 'option'); - } else { - return ''; - } - }, - 'get_sidebar_widgets' => fn($name ) => Timber::get_widgets($name), - 'get_latest_posts' => BlogPost::latest(...), - ]; - } + }, + 'get_sidebar_widgets' => function($name) { + return Timber::get_widgets($name); + }, + 'get_latest_posts' => function(int $count = Post::LATEST_POST_COUNT) : iterable { + return BlogPost::latest($count); + }, + ]; + } - /** - * Does not supply any additional Twig filters. - * - * @return array{} - */ - public function get_filters(): array { - return []; - } + /** + * Does not supply any additional Twig filters. + * + * @return array + */ + public function get_filters() : array { + return []; + } } diff --git a/package.json b/package.json index c383ff7..75720d4 100644 --- a/package.json +++ b/package.json @@ -1,11 +1,9 @@ { "devDependencies": { - "vitepress": "^2.0.0-alpha.12", - "vue": "^3.5.22" + "vitepress": "^2.0.0-alpha.16", + "vue": "^3.5.29" }, "scripts": { - "docs:dev": "vitepress dev docs", - "docs:build": "vitepress build docs", - "docs:preview": "vitepress preview docs" + "docs:build": "vitepress build docs" } -} \ No newline at end of file +} diff --git a/phpcs.xml b/phpcs.xml index 99044cb..18b58f6 100644 --- a/phpcs.xml +++ b/phpcs.xml @@ -2,7 +2,9 @@ <ruleset name="WordPress-Conifer"> <description>A custom set of rules to check coding standards for Conifer.</description> - <arg name="tab-width" value="4" /> + <arg name="tab-width" value="2" /> + + <config name="installed_paths" value="vendor/wp-coding-standards/wpcs" /> <!-- Default settings for command line usage @@ -31,7 +33,7 @@ because WordPress-Extra already includes it. --> <rule ref="WordPress-Extra"> - <exclude name="Universal.Arrays.DisallowShortArraySyntax.Found" /> + <exclude name="Generic.Arrays.DisallowShortArraySyntax.Found" /> <!-- Do not check for proper WordPress file names. --> <exclude name="Generic.WhiteSpace.ArbitraryParenthesesSpacing.SpaceAfterOpen" /> @@ -66,6 +68,8 @@ <exclude name="WordPress.WhiteSpace.ControlStructureSpacing.NoSpaceBeforeCloseParenthesis" /> <exclude name="WordPress.WhiteSpace.OperatorSpacing.NoSpaceAfter" /> <exclude name="WordPress.WhiteSpace.OperatorSpacing.NoSpaceBefore" /> + <exclude name="WordPress.WhiteSpace.PrecisionAlignment" /> + </rule> <rule ref="Squiz.Commenting.ClassComment.Missing"> @@ -107,7 +111,7 @@ <!-- Hook Names While the WordPress Coding Standards state that hook names should be separated by - underscores, an opinionated approach used by plugins like Advanced Custom Fields is to use + underscores, an optionated approach used by plugins like Advanced Custom Fields is to use '/' to namespace hooks. @link https://github.com/WordPress-Coding-Standards/WordPress-Coding-Standards/wiki/Customizable-sniff-properties#custom-word-delimiters-in-hook-names --> @@ -126,14 +130,10 @@ <rule ref="Generic.WhiteSpace.ScopeIndent"> <properties> - <property name="exact" value="true"/> - <property name="indent" value="4"/> + <property name="exact" value="false"/> + <property name="indent" value="2"/> <property name="tabIndent" value="false"/> - <property name="ignoreIndentationTokens" type="array"> - <element value="T_HEREDOC"/> - <element value="T_NOWDOC"/> - <element value="T_INLINE_HTML"/> - </property> + <property name="ignoreIndentationTokens" type="array" value="T_HEREDOC,T_NOWDOC,T_INLINE_HTML"/> </properties> </rule> diff --git a/phpstan/bootstrap.php b/phpstan/bootstrap.php index 3de8ef2..64828eb 100644 --- a/phpstan/bootstrap.php +++ b/phpstan/bootstrap.php @@ -6,5 +6,5 @@ */ if (!defined('WP_PLUGIN_DIR')) { - define( 'WP_PLUGIN_DIR', '/app/wp/wp-content/plugins' ); -} + define('WP_PLUGIN_DIR', '/app/wp/wp-content/plugins'); +} \ No newline at end of file diff --git a/rector.php b/rector.php deleted file mode 100644 index b591a80..0000000 --- a/rector.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -declare(strict_types=1); - -use Rector\Config\RectorConfig; -use Rector\Exception\Configuration\InvalidConfigurationException; -use Rector\TypeDeclaration\Rector\StmtsAwareInterface\DeclareStrictTypesRector; - -// Docs: https://getrector.com/ -try { - return RectorConfig::configure() - ->withPaths([ - __DIR__.'/lib', - __DIR__.'/test', - ]) - // Will grab the PHP version out of the composer.json file so it doesn't need to be explicitly declared. - ->withPhpSets() - ->withPreparedSets( - codeQuality: true, - codingStyle: true, - typeDeclarations: true, - typeDeclarationDocblocks: true, - ) - ->withRules([ - DeclareStrictTypesRector::class, - ]) - ->withAttributesSets( - phpunit: true, - ); -} catch (InvalidConfigurationException $e) { - dd($e); -} diff --git a/test/bootstrap-integration.php b/test/bootstrap-integration.php index 021c378..8a4a0f3 100644 --- a/test/bootstrap-integration.php +++ b/test/bootstrap-integration.php @@ -1,18 +1,14 @@ <?php - -/** +/* * Conifer test suite bootstrap file; included before every unit test run * * @copyright 2020 SiteCrafting, Inc. * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - - require_once __DIR__ . '/../vendor/autoload.php'; if (is_dir(__DIR__ . '/wp-tests-lib')) { - require_once __DIR__ . '/wp-tests-lib/includes/functions.php'; - require_once __DIR__ . '/wp-tests-lib/includes/bootstrap.php'; + require_once __DIR__ . '/wp-tests-lib/includes/functions.php'; + require_once __DIR__ . '/wp-tests-lib/includes/bootstrap.php'; } diff --git a/test/bootstrap.php b/test/bootstrap.php index 3eea0b6..c10feb2 100644 --- a/test/bootstrap.php +++ b/test/bootstrap.php @@ -1,5 +1,4 @@ <?php - /** * Conifer test suite bootstrap file; included before every unit test run * @@ -8,14 +7,12 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - require_once __DIR__ . '/../vendor/autoload.php'; /* -* Define some WP constants that are referenced directly in Conifer -*/ + * Define some WP constants that are referenced directly in Conifer + */ define('ABSPATH', realpath(__DIR__ . '/../')); define('WP_PLUGIN_DIR', ABSPATH . '/wp-content/plugins'); define('WP_CONTENT_URL', 'http://appserver/wp-content'); @@ -25,28 +22,30 @@ * Define our own version of apply_filters_deprecated, rather than mocking, * so that we can raise warnings from our tests. */ -function apply_filters_deprecated(string $filter, $filterArgs ) { - deprecated_hook_notice('filter', $filter); +function apply_filters_deprecated($filter, $filterArgs) { + deprecated_hook_notice('filter', $filter); - return $filterArgs[0]; + return $filterArgs[0]; } -function do_action_deprecated(string $action ): void { - deprecated_hook_notice('action', $action); +function do_action_deprecated($action) { + deprecated_hook_notice('action', $action); } -function deprecated_hook_notice($type, string $hook ): void { - // Do some terrible horcrux-style dark magic shit +function deprecated_hook_notice($type, $hook) { + // Do some terrible horcrux-style dark magic shit // @codingStandardsIgnoreStart $wpMock = new ReflectionClass(WP_Mock::class); $mgrProp = $wpMock->getProperty('event_manager'); + $mgrProp->setAccessible(true); $mgr = $mgrProp->getValue(); $mgrReflection = new ReflectionClass($mgr); $callbacksProp = $mgrReflection->getProperty('callbacks'); + $callbacksProp->setAccessible(true); $callbacks = $callbacksProp->getValue($mgr); // were any filters added? - if ($callbacks && isset($callbacks[sprintf('%s::%s', $type, $hook)])) { - trigger_error($hook . ' is deprecated'); + if ($callbacks && isset($callbacks["$type::$hook"])) { + trigger_error("{$hook} is deprecated"); } } diff --git a/test/integration/Base.php b/test/integration/Base.php index 6c7b61b..f0b19bd 100644 --- a/test/integration/Base.php +++ b/test/integration/Base.php @@ -1,5 +1,4 @@ <?php - /** * Base class for Conifer test cases * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Integration; use WP_UnitTestCase; @@ -20,15 +17,15 @@ * complain about a lack of tests defined here. */ abstract class Base extends WP_UnitTestCase { - /** - * The Site instance representing the WP install we are testing - * - * @var Site - */ - protected $site; + /** + * The Site instance representing the WP install we are testing + * + * @var Site + */ + protected $site; - public function setUp(): void { - $this->site = new Site(); - $this->site->configure_defaults(); - } + public function setUp() { + $this->site = new Site(); + $this->site->configure_defaults(); + } } diff --git a/test/integration/PostTest.php b/test/integration/PostTest.php index eda6bd7..74665ba 100644 --- a/test/integration/PostTest.php +++ b/test/integration/PostTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Post class * @@ -7,8 +6,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Integration; use WP_Term; @@ -20,132 +17,130 @@ use Conifer\Post\BlogPost; class PostTest extends Base { - public $factory; - - public function test_create(): void { - $page = Page::create([ - 'post_title' => 'Hello', - 'post_name' => 'hello', - 'custom_field' => 'CUSTOM', - 'array_field' => [ 'hey', 'there' ], - 'ID' => 'this should have no effect', - 'post_type' => 'this should too', - ]); - - $this->assertInstanceOf(Page::class, $page); - $this->assertNotEmpty($page->id); - $this->assertEquals('Hello', $page->title()); - $this->assertEquals('hello', $page->slug); - $this->assertEquals('CUSTOM', $page->meta('custom_field')); - $this->assertEquals([ 'hey', 'there' ], $page->meta('array_field')); - - $this->assertEquals('page', $page->post_type); - } - - public function test_exists_on_existent_post(): void { - $post = BlogPost::create([ - 'post_title' => 'Cogito ergo sum', - ]); - - $this->assertTrue(Post::exists($post->ID)); - $this->assertTrue(BlogPost::exists($post->ID)); - $this->assertFalse(Page::exists($post->ID)); - } - - public function test_exists_on_existent_page(): void { - $page = Page::create([ - 'post_title' => 'Cogito ergo sum', - ]); - - $this->assertTrue(Post::exists($page->ID)); - $this->assertTrue(Page::exists($page->ID)); - $this->assertFalse(BlogPost::exists($page->ID)); - } - - public function test_exists_on_nonexistent_post(): void { - $this->assertFalse(Post::exists(99999)); - } - - public function test_get_blog_page(): void { - $page = Page::create([ - 'post_title' => 'News', - 'post_name' => 'news', - ]); - - update_option('page_for_posts', $page->id); - - $this->assertEquals($page->id, Page::get_blog_page()->id); - } - - public function test_get_blog_url(): void { - $page = Page::create([ - 'post_title' => 'News', - 'post_name' => 'news', - ]); - - update_option('page_for_posts', $page->id); - - // TODO figure out how to get permalinks working in the test env - $this->assertEquals( - sprintf('http://example.org/?page_id=%d', $page->id), - Page::get_blog_url() - ); + public function test_create() { + $page = Page::create([ + 'post_title' => 'Hello', + 'post_name' => 'hello', + 'custom_field' => 'CUSTOM', + 'array_field' => ['hey', 'there'], + 'ID' => 'this should have no effect', + 'post_type' => 'this should too', + ]); + + $this->assertInstanceOf(Page::class, $page); + $this->assertNotEmpty($page->id); + $this->assertEquals('Hello', $page->title()); + $this->assertEquals('hello', $page->slug); + $this->assertEquals('CUSTOM', $page->meta('custom_field')); + $this->assertEquals(['hey', 'there'], $page->meta('array_field')); + + $this->assertEquals('page', $page->post_type); + } + + public function test_exists_on_existent_post() { + $post = BlogPost::create([ + 'post_title' => 'Cogito ergo sum', + ]); + + $this->assertTrue(Post::exists($post->ID)); + $this->assertTrue(BlogPost::exists($post->ID)); + $this->assertFalse(Page::exists($post->ID)); + } + + public function test_exists_on_existent_page() { + $page = Page::create([ + 'post_title' => 'Cogito ergo sum', + ]); + + $this->assertTrue(Post::exists($page->ID)); + $this->assertTrue(Page::exists($page->ID)); + $this->assertFalse(BlogPost::exists($page->ID)); + } + + public function test_exists_on_nonexistent_post() { + $this->assertFalse(Post::exists(99999)); + } + + public function test_get_blog_page() { + $page = Page::create([ + 'post_title' => 'News', + 'post_name' => 'news', + ]); + + update_option('page_for_posts', $page->id); + + $this->assertEquals($page->id, Page::get_blog_page()->id); + } + + public function test_get_blog_url() { + $page = Page::create([ + 'post_title' => 'News', + 'post_name' => 'news', + ]); + + update_option('page_for_posts', $page->id); + + // TODO figure out how to get permalinks working in the test env + $this->assertEquals( + sprintf('http://example.org/?page_id=%d', $page->id), + Page::get_blog_url() + ); + } + + public function test_get_related_by_taxonomy() { + $awesome = $this->factory->term->create([ + 'name' => 'Awesome', + 'taxonomy' => 'category', + ]); + + $post = BlogPost::create([ + 'post_title' => 'My Post', + ]); + + wp_set_object_terms($post->id, [$awesome], 'category'); + + $ids = $this->factory->post->create_many(3, [ + 'post_type' => 'post', + ]); + foreach ($ids as $id) { + wp_set_object_terms($id, [$awesome], 'category'); } - public function test_get_related_by_taxonomy(): void { - $awesome = $this->factory->term->create([ - 'name' => 'Awesome', - 'taxonomy' => 'category', - ]); - - $post = BlogPost::create([ - 'post_title' => 'My Post', - ]); - - wp_set_object_terms($post->id, [ $awesome ], 'category'); - - $ids = $this->factory->post->create_many(3, [ - 'post_type' => 'post', - ]); - foreach ($ids as $id) { - wp_set_object_terms($id, [ $awesome ], 'category'); - } - - // Create uncategorized posts - $this->factory->post->create_many(2, [ - 'post_type' => 'post', - ]); - - $this->assertCount(3, $post->get_related_by_taxonomy('category')); - $this->assertCount(3, $post->get_related_by_taxonomy('category', 5)); - $this->assertCount(2, $post->get_related_by_taxonomy('category', 2)); - } - - public function test_get_by_template(): void { - $id = $this->factory->post->create([ - 'post_title' => 'My Custom Page', - 'post_status' => 'publish', - 'post_type' => 'page', - 'meta_input' => [ + // Create uncategorized posts + $this->factory->post->create_many(2, [ + 'post_type' => 'post', + ]); + + $this->assertCount(3, $post->get_related_by_taxonomy('category')); + $this->assertCount(3, $post->get_related_by_taxonomy('category', 5)); + $this->assertCount(2, $post->get_related_by_taxonomy('category', 2)); + } + + public function test_get_by_template() { + $id = $this->factory->post->create([ + 'post_title' => 'My Custom Page', + 'post_status' => 'publish', + 'post_type' => 'page', + 'meta_input' => [ '_wp_page_template' => 'my-template.php', - ], - ]); - - $this->assertEquals($id, Page::get_by_template('my-template.php')->id); - } - - public function test_get_by_template_with_query_params(): void { - $id = $this->factory->post->create([ - 'post_title' => 'My Custom Page', - 'post_status' => 'draft', - 'post_type' => 'page', - 'meta_input' => [ + ], + ]); + + $this->assertEquals($id, Page::get_by_template('my-template.php')->id); + } + + public function test_get_by_template_with_query_params() { + $id = $this->factory->post->create([ + 'post_title' => 'My Custom Page', + 'post_status' => 'draft', + 'post_type' => 'page', + 'meta_input' => [ '_wp_page_template' => 'my-template.php', - ], - ]); + ], + ]); - $this->assertEquals($id, Page::get_by_template('my-template.php', [ - 'post_status' => 'draft', - ])->id); - } + $this->assertEquals($id, Page::get_by_template('my-template.php', [ + 'post_status' => 'draft', + ])->id); + } } diff --git a/test/support/Person.php b/test/support/Person.php index be9f31c..bde27ed 100644 --- a/test/support/Person.php +++ b/test/support/Person.php @@ -4,8 +4,6 @@ * Support class for testing CPT registration */ -declare(strict_types=1); - namespace Conifer\Unit\Support; use Conifer\Post\Post; @@ -14,21 +12,16 @@ * Custom Person post type class */ class Person extends Post { - const POST_TYPE = 'person'; + const POST_TYPE = 'person'; - /** - * Return type options. - * - * @return array<string, array<string, string>|string> - */ - public static function type_options(): array { - return [ - 'plural_label' => 'People', - 'labels' => [ + public static function type_options() : array { + return [ + 'plural_label' => 'People', + 'labels' => [ 'singular_name' => 'Person', 'add_new_item' => 'Onboard New Person', 'insert_into_item' => 'Insert into description', // avoid "Insert into person" - ], - ]; - } + ], + ]; + } } diff --git a/test/unit/AdminNoticeTest.php b/test/unit/AdminNoticeTest.php index f455a2a..4d9e0b4 100644 --- a/test/unit/AdminNoticeTest.php +++ b/test/unit/AdminNoticeTest.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; use WP_Mock; @@ -17,130 +15,130 @@ use Conifer\Admin\Notice; class AdminNoticeTest extends Base { - protected function setUp(): void { - parent::setUp(); - Notice::clear_flash_notices(); - Notice::enable_flash_notices(); - } - - protected function tearDown(): void { - parent::tearDown(); - Notice::disable_flash_notices(); - Notice::clear_flash_notices(); - } - - public function test_success(): void { - $notice = new Notice('hello'); - $notice->success(); - - $this->assertEquals('notice notice-success', $notice->get_class()); - $this->expect_admin_notices_action_added(); - } - - public function test_info(): void { - $notice = new Notice('hello'); - $notice->info(); - - $this->assertEquals('notice notice-info', $notice->get_class()); - $this->expect_admin_notices_action_added(); - } - - public function test_warning(): void { - $notice = new Notice('hello'); - $notice->warning(); - - $this->assertEquals('notice notice-warning', $notice->get_class()); - $this->expect_admin_notices_action_added(); - } - - public function test_error(): void { - $notice = new Notice('hello'); - $notice->error(); - - $this->assertEquals('notice notice-error', $notice->get_class()); - $this->expect_admin_notices_action_added(); - } - - public function test_html(): void { - $notice = new Notice('message'); - - // notices are errors by default - $this->assertEquals( - '<div class="notice notice-error"><p>message</p></div>', - $notice->html() - ); - } - - public function test_optional_constructor_arg(): void { - $notice = new Notice('msg', 'example'); - - $this->assertEquals( - 'notice example', - $notice->get_class() - ); - } - - public function test_add_class(): void { - $notice = new Notice('msg'); - $notice->add_class('example'); - - $this->assertEquals( - 'notice example', - $notice->get_class() - ); - } - - public function test_add_class_with_duplicate(): void { - $notice = new Notice('msg'); - $notice->add_class('once'); - $notice->add_class('once'); - - $this->assertEquals( - 'notice once', - $notice->get_class() - ); - } - - public function test_get_flash_notices(): void { - $_SESSION['conifer_admin_notices'] = [ - [ + public function setUp(): void { + parent::setUp(); + Notice::clear_flash_notices(); + Notice::enable_flash_notices(); + } + + public function tearDown(): void { + parent::tearDown(); + Notice::disable_flash_notices(); + Notice::clear_flash_notices(); + } + + public function test_success() { + $notice = new Notice('hello'); + $notice->success(); + + $this->assertEquals('notice notice-success', $notice->get_class()); + $this->expect_admin_notices_action_added(); + } + + public function test_info() { + $notice = new Notice('hello'); + $notice->info(); + + $this->assertEquals('notice notice-info', $notice->get_class()); + $this->expect_admin_notices_action_added(); + } + + public function test_warning() { + $notice = new Notice('hello'); + $notice->warning(); + + $this->assertEquals('notice notice-warning', $notice->get_class()); + $this->expect_admin_notices_action_added(); + } + + public function test_error() { + $notice = new Notice('hello'); + $notice->error(); + + $this->assertEquals('notice notice-error', $notice->get_class()); + $this->expect_admin_notices_action_added(); + } + + public function test_html() { + $notice = new Notice('message'); + + // notices are errors by default + $this->assertEquals( + '<div class="notice notice-error"><p>message</p></div>', + $notice->html() + ); + } + + public function test_optional_constructor_arg() { + $notice = new Notice('msg', 'example'); + + $this->assertEquals( + 'notice example', + $notice->get_class() + ); + } + + public function test_add_class() { + $notice = new Notice('msg'); + $notice->add_class('example'); + + $this->assertEquals( + 'notice example', + $notice->get_class() + ); + } + + public function test_add_class_with_duplicate() { + $notice = new Notice('msg'); + $notice->add_class('once'); + $notice->add_class('once'); + + $this->assertEquals( + 'notice once', + $notice->get_class() + ); + } + + public function test_get_flash_notices() { + $_SESSION['conifer_admin_notices'] = [ + [ 'class' => 'notice notice-success', 'message' => 'all your base', - ], - [ + ], + [ 'class' => 'notice notice-error', 'message' => 'are belong to us', - ], - ]; - - $this->expect_admin_notices_action_added(); - - $notices = Notice::get_flash_notices(); - $this->assertEquals( - '<div class="notice notice-success"><p>all your base</p></div>', - $notices[0]->html() - ); - $this->assertEquals( - '<div class="notice notice-error"><p>are belong to us</p></div>', - $notices[1]->html() - ); - } - - public function test_get_flash_notices_invalid(): void { - $_SESSION['conifer_admin_notices'] = [ - false, - 'foobar', - [ 'message' => '' ], - [ + ], + ]; + + $this->expect_admin_notices_action_added(); + + $notices = Notice::get_flash_notices(); + $this->assertEquals( + '<div class="notice notice-success"><p>all your base</p></div>', + $notices[0]->html() + ); + $this->assertEquals( + '<div class="notice notice-error"><p>are belong to us</p></div>', + $notices[1]->html() + ); + } + + public function test_get_flash_notices_invalid() { + $_SESSION['conifer_admin_notices'] = [ + false, + 'foobar', + ['message' => ''], + [ 'message' => 'valid message, bad class', 'class' => 123, - ], - ]; + ], + ]; - $this->assertEquals([], Notice::get_flash_notices()); - } + $this->assertEquals([], Notice::get_flash_notices()); + } - protected function expect_admin_notices_action_added(): void { - WP_Mock::expectActionAdded('admin_notices', Functions::type('callable')); - } + protected function expect_admin_notices_action_added() { + WP_Mock::expectActionAdded('admin_notices', Functions::type('callable')); + } } diff --git a/test/unit/AdminPageTest.php b/test/unit/AdminPageTest.php index aae96d9..7c89de3 100644 --- a/test/unit/AdminPageTest.php +++ b/test/unit/AdminPageTest.php @@ -7,11 +7,8 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use WP_Mock\Functions; @@ -19,63 +16,63 @@ use Conifer\Admin\SubPage; class AdminPageTest extends Base { - private MockObject $page; - - protected function setUp(): void { - parent::setUp(); - - WP_Mock::userFunction('sanitize_key', [ - 'times' => 1, - 'args' => 'Hello', - 'return' => 'hello', - ]); - - $this->page = $this->getMockForAbstractClass(Page::class, [ 'Hello' ]); - } - - public function test_add(): void { - WP_Mock::expectActionAdded('admin_menu', Functions::type('callable')); - - // fluid interface - $this->assertEquals($this->page, $this->page->add()); - } - - public function test_add_sub_page(): void { - WP_Mock::userFunction('sanitize_key', [ - 'times' => 1, - 'args' => 'Hello Again', - 'return' => 'helloagain', - ]); - - WP_Mock::expectActionAdded('admin_menu', Functions::type('callable')); - - // register a mock of the abstract SubPage class, - // to get something we can instantiate in add_sub_page() - $this->getMockForAbstractClass(SubPage::class, [], 'SubPageMock', false); - - // fluid interface - $this->assertEquals($this->page, $this->page->add_sub_page( - 'SubPageMock', - 'Hello Again' - )); - } - - public function test_do_add(): void { - WP_Mock::userFunction('add_menu_page', [ - 'times' => 1, - 'args' => [ + private $page; + + public function setUp(): void { + parent::setUp(); + + WP_Mock::userFunction('sanitize_key', [ + 'times' => 1, + 'args' => 'Hello', + 'return' => 'hello', + ]); + + $this->page = $this->getMockForAbstractClass(Page::class, ['Hello']); + } + + public function test_add() { + WP_Mock::expectActionAdded('admin_menu', Functions::type('callable')); + + // fluid interface + $this->assertEquals($this->page, $this->page->add()); + } + + public function test_add_sub_page() { + WP_Mock::userFunction('sanitize_key', [ + 'times' => 1, + 'args' => 'Hello Again', + 'return' => 'helloagain', + ]); + + WP_Mock::expectActionAdded('admin_menu', Functions::type('callable')); + + // register a mock of the abstract SubPage class, + // to get something we can instantiate in add_sub_page() + $this->getMockForAbstractClass(SubPage::class, [], 'SubPageMock', false); + + // fluid interface + $this->assertEquals($this->page, $this->page->add_sub_page( + 'SubPageMock', + 'Hello Again' + )); + } + + public function test_do_add() { + WP_Mock::userFunction('add_menu_page', [ + 'times' => 1, + 'args' => [ 'Hello', 'Hello', 'manage_options', 'hello', Functions::type('callable'), null, - ], - ]); + ], + ]); - // fluid interface - $this->assertEquals($this->page, $this->page->do_add()); - } + // The filter shouldn't return anything, so if it does we know something broke. + $this->assertNull($this->page->do_add()); + } - // TODO test ::render() in an integration test + // TODO test ::render() in an integration test } diff --git a/test/unit/AdminSubPageTest.php b/test/unit/AdminSubPageTest.php index ca68f5b..a075426 100644 --- a/test/unit/AdminSubPageTest.php +++ b/test/unit/AdminSubPageTest.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; use WP_Mock; @@ -18,36 +16,37 @@ use Conifer\Admin\SubPage; class AdminSubPageTest extends Base { - public function test_do_add_sub_page(): void { - WP_Mock::userFunction('sanitize_key', [ - 'times' => 1, - 'args' => [ 'Hello' ], - 'return' => 'hello', - ]); - WP_Mock::userFunction('sanitize_key', [ - 'times' => 1, - 'args' => [ 'Hello Again' ], - 'return' => 'helloagain', - ]); - WP_Mock::userFunction('add_submenu_page', [ - 'times' => 1, - 'args' => [ + public function test_do_add_sub_page() { + WP_Mock::userFunction('sanitize_key', [ + 'times' => 1, + 'args' => ['Hello'], + 'return' => 'hello', + ]); + WP_Mock::userFunction('sanitize_key', [ + 'times' => 1, + 'args' => ['Hello Again'], + 'return' => 'helloagain', + ]); + WP_Mock::userFunction('add_submenu_page', [ + 'times' => 1, + 'args' => [ 'hello', 'Hello Again', 'Hello Again', 'manage_options', 'helloagain', Functions::type('callable'), - ], - ]); + ], + ]); - $page = $this->getMockForAbstractClass(Page::class, [ 'Hello' ]); + $page = $this->getMockForAbstractClass(Page::class, ['Hello']); - $subPage = $this->getMockForAbstractClass(SubPage::class, [ - $page, - 'Hello Again', - ]); - // assert fluent interface - $this->assertEquals($subPage, $subPage->do_add()); - } + $subPage = $this->getMockForAbstractClass(SubPage::class, [ + $page, + 'Hello Again', + ]); + + // The filter shouldn't return anything, so if it does we know something broke. + $this->assertNull($subPage->do_add()); + } } diff --git a/test/unit/AjaxHandlerTest.php b/test/unit/AjaxHandlerTest.php index bb4fc5c..877158f 100644 --- a/test/unit/AjaxHandlerTest.php +++ b/test/unit/AjaxHandlerTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\AjaxHandler\AbstractBase class * @@ -7,78 +6,70 @@ * @author Scott Dunham <sdunham@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use Conifer\AjaxHandler\AbstractBase; class AjaxHandlerTest extends Base { - // Best or GREATEST? - const BEST_BAND = 'Creed'; - - protected MockObject $handler; - - protected function setUp(): void { - parent::setUp(); - - // Mock the abstract base AJAX handler class so we can test against it - $ajaxHanderStub = $this->getMockForAbstractClass(AbstractBase::class, [ $this->get_request_array() ]); - - // Set up our stub's execute method to return the name of the best band - $ajaxHanderStub - ->expects($this->any()) - ->method('execute') - ->willReturn([ 'best_band' => self::BEST_BAND ]); - - // Save for later use in test function(s) - $this->handler = $ajaxHanderStub; - } - - public function test_send_json_response(): void { - // Tell PHPUnit to expect the following string to be - // output, proclaiming what should be obvious to all - $this->expectOutputString('{"best_band":"' . self::BEST_BAND . '"}'); - - // Call the mocked abstract execute method to get the - // raw response array of the AJAX handler function - // (this is protected, so we need to use reflection) - $response = $this->callProtectedMethod( - $this->handler, - 'execute', - [ $this->get_request_array() ] - ); - - // Mock the wp_send_json function to send a json_encoded - // version of the response from above - WP_Mock::userFunction('wp_send_json', [ - 'times' => 1, - 'return' => function ($response ): void { - echo json_encode($response); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode - }, - ]); - - // Call the protected send_json_response method to output the - // JSON string response we'd expect for this mocked AJAX call - $this->callProtectedMethod( - $this->handler, - 'send_json_response', - [ $response ] - ); - } - - /** - * Returns the request array. - * - * @return array<string, string> - */ - private function get_request_array(): array { - // AJAX handler classes require an action to be included - // with each request to be considered valid - return [ 'action' => 'best_band' ]; - } + // Best or GREATEST? + const BEST_BAND = 'Creed'; + + protected $handler; + + public function setUp(): void { + parent::setUp(); + + // Mock the abstract base AJAX handler class so we can test against it + $ajaxHanderStub = $this->getMockForAbstractClass(AbstractBase::class, [$this->get_request_array()]); + + // Set up our stub's execute method to return the name of the best band + $ajaxHanderStub + ->expects($this->any()) + ->method('execute') + ->willReturn(['best_band' => self::BEST_BAND]); + + // Save for later use in test function(s) + $this->handler = $ajaxHanderStub; + } + + public function test_send_json_response() { + // Tell PHPUnit to expect the following string to be + // output, proclaiming what should be obvious to all + $this->expectOutputString('{"best_band":"' . self::BEST_BAND . '"}'); + + // Call the mocked abstract execute method to get the + // raw response array of the AJAX handler function + // (this is protected, so we need to use reflection) + $response = $this->callProtectedMethod( + $this->handler, + 'execute', + [$this->get_request_array()] + ); + + // Mock the wp_send_json function to send a json_encoded + // version of the response from above + WP_Mock::userFunction('wp_send_json', [ + 'times' => 1, + 'return' => function($response) { + echo json_encode($response); // phpcs:ignore WordPress.WP.AlternativeFunctions.json_encode_json_encode + }, + ]); + + // Call the protected send_json_response method to output the + // JSON string response we'd expect for this mocked AJAX call + $this->callProtectedMethod( + $this->handler, + 'send_json_response', + [$response] + ); + } + + private function get_request_array() { + // AJAX handler classes require an action to be included + // with each request to be considered valid + return ['action' => 'best_band']; + } } diff --git a/test/unit/Base.php b/test/unit/Base.php index 1beb49d..375d682 100644 --- a/test/unit/Base.php +++ b/test/unit/Base.php @@ -1,5 +1,4 @@ <?php - /** * Base class for Conifer test cases * @@ -7,12 +6,9 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; use PHPUnit\Framework\TestCase; -use Timber\Timber; use Timber\User; use WP_Mock; use WP_Term; @@ -22,137 +18,138 @@ * complain about a lack of tests defined here. */ abstract class Base extends TestCase { - protected function setUp(): void { - WP_Mock::setUp(); + public function setUp(): void { + WP_Mock::setUp(); + } + + public function tearDown(): void { + WP_Mock::tearDown(); + } + + + /** + * Mock a call to WordPress's get_post + * + * @param array $props an array of WP_Post object properties + * must include a valid (that is, a numeric) post ID + * @throws \InvalidArgumentException if $props["ID"] is not numeric + */ + protected function mockPost(array $props, array $options = []) { + if (empty($props['ID']) || !is_numeric($props['ID'])) { + throw new \InvalidArgumentException('$props["ID"] must be numeric'); } - protected function tearDown(): void { - WP_Mock::tearDown(); + // allow specifying which class this post will be + $post = $this->getMockBuilder($options['class'] ?? \Timber\Post::class) + ->disableOriginalConstructor() + ->getMock(); + + foreach ($props as $prop => $value) { + $post->{$prop} = $value; } + WP_Mock::userFunction('get_post', array_merge([ + 'times' => 1, + 'args' => [$props['ID']], + 'return' => $post, + ])); + + return $post; + } + + /** + * Mock a call to WordPress's get_term + * + * @param array $props an array of WP_Term object properties + * must include a valid (that is, a numeric) term_id, and a taxonomy string, + * e.g.: + * + * ``` + * $props = ['term_id' => 123, 'taxonomy' => 'yeah-im-the-taxmaaaan']; + * ``` + * @param additional WP_Mock::userFunction objects to merge in. + * @throws \InvalidArgumentException if $props["ID"] is not numeric + */ + protected function mockTerm(array $props, array $options = []) { + if (empty($props['term_id']) || !is_numeric($props['term_id'])) { + throw new \InvalidArgumentException('$props["term_id"] must be numeric'); + } + if (empty($props['taxonomy']) || !is_string($props['taxonomy'])) { + throw new \InvalidArgumentException( + '$props["taxonomy"] must be a string' + ); + } - /** - * Mock a call to WordPress's get_post - * - * @param array<string, mixed> $props an array of WP_Post object properties - * must include a valid (that is, a numeric) post ID - * @throws \InvalidArgumentException if $props["ID"] is not numeric - * @param array<string, mixed> $options - */ - protected function mockPost(array $props, array $options = [] ) { - if (empty($props['ID']) || !is_numeric($props['ID'])) { - throw new \InvalidArgumentException('$props["ID"] must be numeric'); - } - - // allow specifying which class this post will be - $post = $this->getMockBuilder($options['class'] ?? \Timber\Post::class) - ->disableOriginalConstructor() - ->getMock(); - - foreach ($props as $prop => $value) { - $post->{$prop} = $value; - } - - WP_Mock::userFunction('get_post', [ - 'times' => 1, - 'args' => [ $props['ID'] ], - 'return' => $post, - ]); + $term = $this->getMockBuilder(WP_Term::class) + ->disableOriginalConstructor() + ->getMock(); - return $post; + foreach ($props as $prop => $value) { + $term->{$prop} = $value; } - /** - * Mock a call to WordPress's get_term - * - * @param array<string, mixed> $props an array of WP_Term object properties - * must include a valid (that is, a numeric) term_id, and a taxonomy string, - * e.g.: - * - * ``` - * $props = ['term_id' => 123, 'taxonomy' => 'yeah-im-the-taxmaaaan']; - * ``` - * @param additional WP_Mock::userFunction objects to merge in. - * @throws \InvalidArgumentException if $props["ID"] is not numeric - */ - protected function mockTerm(array $props, array $options = [] ) { - if (empty($props['term_id']) || !is_numeric($props['term_id'])) { - throw new \InvalidArgumentException('$props["term_id"] must be numeric'); - } - - if (empty($props['taxonomy']) || !is_string($props['taxonomy'])) { - throw new \InvalidArgumentException( - '$props["taxonomy"] must be a string' - ); - } - - $term = $this->getMockBuilder(WP_Term::class) - ->disableOriginalConstructor() - ->getMock(); - - foreach ($props as $prop => $value) { - $term->{$prop} = $value; - } - - WP_Mock::userFunction('get_term', array_merge([ - 'times' => 1, - 'args' => [ $props['term_id'], $props['taxonomy'] ], - 'return' => $term, - ], $options)); - - return $term; - } + WP_Mock::userFunction('get_term', array_merge([ + 'times' => 1, + 'args' => [$props['term_id'], $props['taxonomy']], + 'return' => $term, + ], $options)); - protected function getProtectedProperty($object_name, $name ) { - $reflection = new \ReflectionClass($object_name); - $property = $reflection->getProperty($name); + return $term; + } - return $property->getValue($object_name); - } + protected function getProtectedProperty($object, $name) { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($name); + $property->setAccessible(true); - protected function setProtectedProperty($object_name, $name, $value ): void { - $reflection = new \ReflectionClass($object_name); - $property = $reflection->getProperty($name); + return $property->getValue($object); + } - $property->setValue($object_name, $value); - } + protected function setProtectedProperty($object, $name, $value) { + $reflection = new \ReflectionClass($object); + $property = $reflection->getProperty($name); + $property->setAccessible(true); - protected function callProtectedMethod($object_name, $name, array $args = [] ) { - $reflection = new \ReflectionClass($object_name); - $method = $reflection->getMethod($name); + return $property->setValue($object, $value); + } - return $method->invokeArgs($object_name, $args); - } + protected function callProtectedMethod($object, $name, $args = []) { + $reflection = new \ReflectionClass($object); + $method = $reflection->getMethod($name); + $method->setAccessible(true); - protected function mockCurrentUser($id, $data = [], $meta = [] ) { - $this->mockCurrentUserId($id); - $this->mockCurrentUserData($data); - - if ($meta) { - foreach ($meta as $key => $value) { - WP_Mock::userFunction('get_user_meta', [ - 'args' => [ '*', $key, WP_Mock\Functions::type('bool') ], - 'return' => $value, - ]); - } - } - - WP_Mock::userFunction('get_avatar_url', [ - 'return' => 'https://example.com/avatar.gif', - ]); + return $method->invokeArgs($object, $args); + } - return Timber::get_user($id); - } + protected function mockCurrentUser($id, $data = [], $meta = []) { + $this->mockCurrentUserId($id); + $this->mockCurrentUserData($data); - protected function mockCurrentUserId($id ) { - WP_Mock::userFunction('get_current_user_id', [ - 'return' => $id, + if ($meta) { + foreach ($meta as $key => $value) { + WP_Mock::userFunction('get_user_meta', [ + 'args' => ['*', $key, WP_Mock\Functions::type('bool')], + 'return' => $value, ]); + } } - protected function mockCurrentUserData($data = [] ) { - WP_Mock::userFunction('get_userdata', [ - 'return' => $data, - ]); - } + WP_Mock::userFunction('get_avatar_url', [ + 'return' => 'https://example.com/avatar.gif', + ]); + + return new User($id); + } + + protected function mockCurrentUserId($id) { + WP_Mock::userFunction('get_current_user_id', [ + 'return' => $id, + ]); + } + + protected function mockCurrentUserData($data = []) { + WP_Mock::userFunction('get_userdata', [ + 'return' => $data, + ]); + } } diff --git a/test/unit/DismissableAlertTest.php b/test/unit/DismissableAlertTest.php index e1bdebb..14baf44 100644 --- a/test/unit/DismissableAlertTest.php +++ b/test/unit/DismissableAlertTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Alert\DismissableAlert class * @@ -7,66 +6,64 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use DateTime; +use \DateTime; use Conifer\Alert\DismissableAlert; class DismissableAlertTest extends Base { - public function test_cookie_text(): void { - $text = 'IMPORTANT ALERT!!!'; - $cookie = 'wp-user_dismissed_alert_' . md5($text); - $alert = new DismissableAlert($text, [ - 'cookies' => [ $cookie => 1 ], - 'cookie_expires' => 'Sat, 21 Mar 2020 12:11:34 -0700', - ]); + public function test_cookie_text() { + $text = 'IMPORTANT ALERT!!!'; + $cookie = 'wp-user_dismissed_alert_' . md5($text); + $alert = new DismissableAlert($text, [ + 'cookies' => [$cookie => 1], + 'cookie_expires' => 'Sat, 21 Mar 2020 12:11:34 -0700', + ]); - $this->assertEquals( - 'wp-user_dismissed_alert_7f408226eeff9c2f4661611635792d9c=1; expires=Sat, 21 Mar 2020 12:11:34 -0700; path=/', - $alert->cookie_text() - ); - } + $this->assertEquals( + 'wp-user_dismissed_alert_7f408226eeff9c2f4661611635792d9c=1; expires=Sat, 21 Mar 2020 12:11:34 -0700; path=/', + $alert->cookie_text() + ); + } - public function test_cookie_text_default_expires(): void { - $text = 'IMPORTANT ALERT!!!'; - $cookie = 'wp-user_dismissed_alert_' . md5($text); - $alert = new DismissableAlert($text, [ - 'cookies' => [ $cookie => 1 ], - // let the expiry default to one year from now - ]); - preg_match('~=1; expires=(.+); path=/$~', $alert->cookie_text(), $matches); - $dt = date_create_from_format('U', (string) strtotime($matches[1])); + public function test_cookie_text_default_expires() { + $text = 'IMPORTANT ALERT!!!'; + $cookie = 'wp-user_dismissed_alert_' . md5($text); + $alert = new DismissableAlert($text, [ + 'cookies' => [$cookie => 1], + // let the expiry default to one year from now + ]); + preg_match('~=1; expires=(.+); path=/$~', $alert->cookie_text(), $matches); + $dt = date_create_from_format('U', strtotime($matches[1])); - // assert that expires=(...) gets a valid formatted datetime - $this->assertNotEmpty($matches[1]); - $this->assertEquals($matches[1], $dt->format('r')); - } + // assert that expires=(...) gets a valid formatted datetime + $this->assertNotEmpty($matches[1]); + $this->assertEquals($matches[1], $dt->format('r')); + } - public function test_cooke_text_path_opt(): void { - $text = 'THIS IS AN IMPORTANT ALERT!!!'; - $cookie = 'wp-user_dismissed_alert_' . md5($text); - $alert = new DismissableAlert($text, [ - 'cookies' => [ $cookie => 1 ], - 'cookie_expires' => 'Sat, 21 Mar 2020 12:11:34 -0700', - 'cookie_path' => '/custom', - ]); + public function test_cooke_text_path_opt() { + $text = 'THIS IS AN IMPORTANT ALERT!!!'; + $cookie = 'wp-user_dismissed_alert_' . md5($text); + $alert = new DismissableAlert($text, [ + 'cookies' => [$cookie => 1], + 'cookie_expires' => 'Sat, 21 Mar 2020 12:11:34 -0700', + 'cookie_path' => '/custom', + ]); - $this->assertEquals( - 'wp-user_dismissed_alert_c3b076f60a0a7bf31661e52ad147f761=1; expires=Sat, 21 Mar 2020 12:11:34 -0700; path=/custom', - $alert->cookie_text() - ); - } + $this->assertEquals( + 'wp-user_dismissed_alert_c3b076f60a0a7bf31661e52ad147f761=1; expires=Sat, 21 Mar 2020 12:11:34 -0700; path=/custom', + $alert->cookie_text() + ); + } - public function test_dismissed(): void { - $text = 'IMPORTANT ALERT!!!'; - $cookie = 'wp-user_dismissed_alert_' . md5($text); - $alert = new DismissableAlert($text, [ - 'cookies' => [ $cookie => 1 ], - ]); + public function test_dismissed() { + $text = 'IMPORTANT ALERT!!!'; + $cookie = 'wp-user_dismissed_alert_' . md5($text); + $alert = new DismissableAlert($text, [ + 'cookies' => [$cookie => 1], + ]); - $this->assertTrue($alert->dismissed()); - } + $this->assertTrue($alert->dismissed()); + } } diff --git a/test/unit/FormTest.php b/test/unit/FormTest.php index d76eba6..6b582f0 100644 --- a/test/unit/FormTest.php +++ b/test/unit/FormTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Form\AbstractBase class * @@ -7,469 +6,450 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use Conifer\Form\AbstractBase; class FormTest extends Base { - protected MockObject $form; - - protected function setUp(): void { - parent::setUp(); - - $this->form = $this->getMockForAbstractClass(AbstractBase::class); - // Set the uploaded files for this form to our mocked $_FILES superglobal - $this->setFiles($this->getDefaultFiles()); - } - - public function test_hydrate_stripslashes(): void { - $this->setFields([ - 'first_name' => [], - 'last_name' => [], - 'favorite_things' => [], - ]); - $this->form->hydrate([ - 'first_name' => 'Julie', - 'last_name' => 'Andrews\\', - 'favorite_things' => [ 'kettles\\', 'mittens\\' ], - ], [ - 'strip_slashes' => true, - ]); - - $this->assertEquals('Julie', $this->form->get('first_name')); - $this->assertEquals('Andrews', $this->form->get('last_name')); - $this->assertEquals( - [ 'kettles', 'mittens' ], - $this->form->get('favorite_things') - ); - $this->assertNull($this->form->get('yes_or_no')); - } - - public function test_hydrate(): void { - $this->setFields([ - 'first_name' => [], - 'last_name' => [], - 'favorite_things' => [], - ]); - $this->form->hydrate([ - 'first_name' => 'Julie', - 'last_name' => 'Andrews\\', - 'favorite_things' => [ 'kettles\\', 'mittens\\' ], - ]); - - // Slashes should get stripped automatically - $this->assertEquals('Julie', $this->form->get('first_name')); - $this->assertEquals('Andrews\\', $this->form->get('last_name')); - $this->assertEquals( - [ 'kettles\\', 'mittens\\' ], - $this->form->get('favorite_things') - ); - $this->assertNull($this->form->get('yes_or_no')); - } - - public function test_checked_with_single_value(): void { - $this->setFields([ - 'highest_award' => [], - ]); - $this->form->hydrate([ - 'highest_award' => 'Academy Award', - ]); - - $this->assertTrue($this->form->checked('highest_award', 'Academy Award')); - $this->assertFalse($this->form->checked('highest_award', 'a blue ribbon')); - } - - public function test_checked_with_multiple_values(): void { - $this->setFields([ - 'favorite_things' => [], - ]); - $this->form->hydrate([ - 'favorite_things' => [ 'kettles', 'mittens' ], - ]); - - $this->assertFalse($this->form->checked('favorite_things', 'raindrops')); - $this->assertFalse($this->form->checked('favorite_things', 'whiskers')); - $this->assertTrue($this->form->checked('favorite_things', 'kettles')); - $this->assertTrue($this->form->checked('favorite_things', 'mittens')); - } - - public function test_checked_with_nonsense(): void { - $this->setFields([]); - $this->form->hydrate([]); - - $this->assertFalse($this->form->checked('nonsense')); - } - - public function test_selected_with_single_option(): void { - $this->setFields([ - 'favorite_thing' => [], - ]); - $this->form->hydrate([ - 'favorite_thing' => 'raindrops', - ]); - - $this->assertTrue($this->form->selected('favorite_thing', 'raindrops')); - $this->assertFalse($this->form->selected('favorite_thing', 'kittens')); - } - - public function test_selected_with_multiple_options(): void { - $this->setFields([ - 'favorite_thing' => [], - ]); - $this->form->hydrate([ - 'favorite_thing' => [ 'raindrops', 'mittens' ], - ]); - - $this->assertTrue($this->form->selected('favorite_thing', 'raindrops')); - $this->assertTrue($this->form->selected('favorite_thing', 'mittens')); - $this->assertFalse($this->form->selected('favorite_thing', 'kittens')); - } - - // @see https://github.com/sitecrafting/conifer/issues/129 - public function test_get_falsey_value(): void { - $this->setFields([ - 'empty_array' => [], - 'falsey_string' => [], - 'empty_string' => [], - 'null_field' => [], - ]); - $this->form->hydrate([ - 'empty_array' => [], - 'falsey_string' => '0', - 'empty_string' => '', - ]); - - $this->assertEquals([], $this->form->get('empty_array')); - $this->assertEquals('0', $this->form->get('falsey_string')); - $this->assertEquals('', $this->form->get('empty_string')); - $this->assertNull($this->form->get('null_field')); - } - - public function test_get_errors_for(): void { - $this->form->add_error('nationality', 'INVALID NATIONALITY'); - - $this->assertEquals([ - [ + protected $form; + + public function setUp(): void { + parent::setUp(); + + $this->form = $this->getMockForAbstractClass(AbstractBase::class); + // Set the uploaded files for this form to our mocked $_FILES superglobal + $this->setFiles($this->getDefaultFiles()); + } + + public function test_hydrate_stripslashes() { + $this->setFields([ + 'first_name' => [], + 'last_name' => [], + 'favorite_things' => [], + ]); + $this->form->hydrate([ + 'first_name' => 'Julie', + 'last_name' => 'Andrews\\', + 'favorite_things' => ['kettles\\', 'mittens\\'], + ], [ + 'strip_slashes' => true, + ]); + + $this->assertEquals('Julie', $this->form->get('first_name')); + $this->assertEquals('Andrews', $this->form->get('last_name')); + $this->assertEquals( + ['kettles', 'mittens'], + $this->form->get('favorite_things') + ); + $this->assertNull($this->form->get('yes_or_no')); + } + + public function test_hydrate() { + $this->setFields([ + 'first_name' => [], + 'last_name' => [], + 'favorite_things' => [], + ]); + $this->form->hydrate([ + 'first_name' => 'Julie', + 'last_name' => 'Andrews\\', + 'favorite_things' => ['kettles\\', 'mittens\\'], + ]); + + // Slashes should get stripped automatically + $this->assertEquals('Julie', $this->form->get('first_name')); + $this->assertEquals('Andrews\\', $this->form->get('last_name')); + $this->assertEquals( + ['kettles\\', 'mittens\\'], + $this->form->get('favorite_things') + ); + $this->assertNull($this->form->get('yes_or_no')); + } + + public function test_checked_with_single_value() { + $this->setFields([ + 'highest_award' => [], + ]); + $this->form->hydrate([ + 'highest_award' => 'Academy Award', + ]); + + $this->assertTrue($this->form->checked('highest_award', 'Academy Award')); + $this->assertFalse($this->form->checked('highest_award', 'a blue ribbon')); + } + + public function test_checked_with_multiple_values() { + $this->setFields([ + 'favorite_things' => [], + ]); + $this->form->hydrate([ + 'favorite_things' => ['kettles', 'mittens'], + ]); + + $this->assertFalse($this->form->checked('favorite_things', 'raindrops')); + $this->assertFalse($this->form->checked('favorite_things', 'whiskers')); + $this->assertTrue($this->form->checked('favorite_things', 'kettles')); + $this->assertTrue($this->form->checked('favorite_things', 'mittens')); + } + + public function test_checked_with_nonsense() { + $this->setFields([]); + $this->form->hydrate([]); + + $this->assertFalse($this->form->checked('nonsense')); + } + + public function test_selected_with_single_option() { + $this->setFields([ + 'favorite_thing' => [], + ]); + $this->form->hydrate([ + 'favorite_thing' => 'raindrops', + ]); + + $this->assertTrue($this->form->selected('favorite_thing', 'raindrops')); + $this->assertFalse($this->form->selected('favorite_thing', 'kittens')); + } + + public function test_selected_with_multiple_options() { + $this->setFields([ + 'favorite_thing' => [], + ]); + $this->form->hydrate([ + 'favorite_thing' => ['raindrops', 'mittens'], + ]); + + $this->assertTrue($this->form->selected('favorite_thing', 'raindrops')); + $this->assertTrue($this->form->selected('favorite_thing', 'mittens')); + $this->assertFalse($this->form->selected('favorite_thing', 'kittens')); + } + + // @see https://github.com/sitecrafting/conifer/issues/129 + public function test_get_falsey_value() { + $this->setFields([ + 'empty_array' => [], + 'falsey_string' => [], + 'empty_string' => [], + 'null_field' => [], + ]); + $this->form->hydrate([ + 'empty_array' => [], + 'falsey_string' => '0', + 'empty_string' => '', + ]); + + $this->assertEquals([], $this->form->get('empty_array')); + $this->assertEquals('0', $this->form->get('falsey_string')); + $this->assertEquals('', $this->form->get('empty_string')); + $this->assertNull($this->form->get('null_field')); + } + + public function test_get_errors_for() { + $this->form->add_error('nationality', 'INVALID NATIONALITY'); + + $this->assertEquals([ + [ 'field' => 'nationality', 'message' => 'INVALID NATIONALITY', - ], - ], array_values($this->form->get_errors_for('nationality'))); - } - - public function test_get_error_messages_for(): void { - $this->form->add_error('nationality', 'INVALID NATIONALITY'); - - $this->assertEquals( - [ 'INVALID NATIONALITY' ], - array_values($this->form->get_error_messages_for('nationality')) - ); - } - - public function test_validate_valid_submission(): void { - $isMaryPoppins = (fn(array $_, string $value ): bool => $value === 'Mary Poppins'); - - $this->setFields([ - 'nanny' => [ - 'validators' => [ [ $this->form, 'require' ], $isMaryPoppins ], - ], - 'field_without_validator' => [], - 'spanish_inquisition' => [], // don't expect this field at all... - ]); - - $this->assertTrue($this->form->validate([ - 'nanny' => 'Mary Poppins', - 'field_without_validator', - ])); - $this->assertEmpty($this->form->get_errors()); - } - - public function test_validate_shorthand(): void { - $this->setFields([ - 'best_band' => [ - 'validators' => [ 'require' ], - ], - ]); - - $this->assertFalse($this->form->validate([ - 'best_band' => '', - ])); - $this->assertEquals(1, count($this->form->get_errors())); - } - - public function test_require_with_empty_value(): void { - $bestBand = [ - 'name' => 'best_band', - 'required_message' => 'You have to put somethin here broh.', - 'validators' => [ [ $this->form, 'require' ] ], - ]; - - $this->assertFalse($this->form->require($bestBand, '')); - $this->assertEquals( - [ 'You have to put somethin here broh.' ], - $this->form->get_error_messages_for('best_band') - ); - } - - public function test_require_with_value(): void { - $bestBand = [ - 'name' => 'best_band', - 'validators' => [ [ $this->form, 'require' ] ], - ]; - - $this->assertTrue($this->form->require($bestBand, 'Creed')); - $this->assertEmpty($this->form->get_error_messages_for('best_band')); - } - - public function test_get_whitelisted_fields_with_filter(): void { - $this->setFields([ - 'activity' => [ - 'filter' => fn(string $val ): string => sprintf('FILTERED->%s<-FILTERED', $val), - ], - ]); - - $whitelist = $this->form->get_whitelisted_fields([ - 'activity' => 'anything really', - ]); - - $this->assertEquals( - 'FILTERED->anything really<-FILTERED', - $whitelist['activity'] - ); - } - - public function test_get_whitelisted_fields_with_default(): void { - $this->setFields([ - 'adjective' => [ + ], + ], array_values($this->form->get_errors_for('nationality'))); + } + + public function test_get_error_messages_for() { + $this->form->add_error('nationality', 'INVALID NATIONALITY'); + + $this->assertEquals( + ['INVALID NATIONALITY'], + array_values($this->form->get_error_messages_for('nationality')) + ); + } + + public function test_validate_valid_submission() { + $isMaryPoppins = function(array $_, string $value) { + return $value === 'Mary Poppins'; + }; + + $this->setFields([ + 'nanny' => [ + 'validators' => [[$this->form, 'require'], $isMaryPoppins], + ], + 'field_without_validator' => [], + 'spanish_inquisition' => [], // don't expect this field at all... + ]); + + $this->assertTrue($this->form->validate([ + 'nanny' => 'Mary Poppins', + 'field_without_validator', + ])); + $this->assertEmpty($this->form->get_errors()); + } + + public function test_validate_shorthand() { + $this->setFields([ + 'best_band' => [ + 'validators' => ['require'], + ], + ]); + + $this->assertFalse($this->form->validate([ + 'best_band' => '', + ])); + $this->assertEquals(1, count($this->form->get_errors())); + } + + public function test_require_with_empty_value() { + $bestBand = [ + 'name' => 'best_band', + 'required_message' => 'You have to put somethin here broh.', + 'validators' => [[$this->form, 'require']], + ]; + + $this->assertFalse($this->form->require($bestBand, '')); + $this->assertEquals( + ['You have to put somethin here broh.'], + $this->form->get_error_messages_for('best_band') + ); + } + + public function test_require_with_value() { + $bestBand = [ + 'name' => 'best_band', + 'validators' => [[$this->form, 'require']], + ]; + + $this->assertTrue($this->form->require($bestBand, 'Creed')); + $this->assertEmpty($this->form->get_error_messages_for('best_band')); + } + + public function test_get_whitelisted_fields_with_filter() { + $this->setFields([ + 'activity' => [ + 'filter' => function($val) { + return "FILTERED->$val<-FILTERED"; + }, + ], + ]); + + $whitelist = $this->form->get_whitelisted_fields([ + 'activity' => 'anything really', + ]); + + $this->assertEquals( + 'FILTERED->anything really<-FILTERED', + $whitelist['activity'] + ); + } + + public function test_get_whitelisted_fields_with_default() { + $this->setFields([ + 'adjective' => [ 'default' => 'supercalifragilisticexpialidocious', - ], - ]); - $whitelist = $this->form->get_whitelisted_fields([ 'adjective' => '' ]); - - $this->assertEquals( - 'supercalifragilisticexpialidocious', - $whitelist['adjective'] - ); - } - - public function test_get_file(): void { - $this->assertNotEmpty($this->form->get_file('favoriteThings')); - } - - public function test_required_file_missing(): void { - $this->setFields([ - 'leastFavoriteThings' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('leastFavoriteThings')); - } - - public function test_file_mime_type_valid(): void { - $this->setFields([ - 'favoriteThings' => [ - 'validators' => [ [ $this->form, 'validate_file_mime_type', [ 'text/plain' ] ] ], - ], - ]); - - $this->assertTrue($this->form->validate([])); - $this->assertEmpty($this->form->get_errors()); - } - - public function test_file_mime_type_invalid(): void { - $this->setFields([ - 'favoriteThings' => [ - 'validators' => [ [ $this->form, 'validate_file_mime_type', [ 'application/pdf' ] ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('favoriteThings')); - $this->assertEquals( - [ sprintf($this->form::MESSAGE_INVALID_MIME_TYPE, 'favoriteThings') ], - $this->form->get_error_messages_for('favoriteThings') - ); - } - - public function test_file_upload_error_ini_size(): void { - $this->setFields([ - 'uploadErrorSizeIni' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorSizeIni')); - $this->assertEquals( - [ sprintf($this->form::MESSAGE_FILE_SIZE, 'uploadErrorSizeIni') ], - $this->form->get_error_messages_for('uploadErrorSizeIni') - ); - } - - public function test_file_upload_error_form_size(): void { - $this->setFields([ - 'uploadErrorSizeForm' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorSizeForm')); - $this->assertEquals( - [ sprintf($this->form::MESSAGE_FILE_SIZE, 'uploadErrorSizeForm') ], - $this->form->get_error_messages_for('uploadErrorSizeForm') - ); - } - - public function test_file_upload_error_partial(): void { - $this->setFields([ - 'uploadErrorPartialFile' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorPartialFile')); - $this->assertEquals( - [ sprintf($this->form::MESSAGE_UPLOAD_ERROR, 'uploadErrorPartialFile') ], - $this->form->get_error_messages_for('uploadErrorPartialFile') - ); - } - - public function test_file_upload_error_no_file(): void { - $this->setFields([ - 'austrianAbbeyMembership' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->assertFalse($this->form->validate([])); - $this->assertNotEmpty($this->form->get_error_messages_for('austrianAbbeyMembership')); - $this->assertEquals( - [ sprintf($this->form::MESSAGE_FIELD_REQUIRED, 'austrianAbbeyMembership') ], - $this->form->get_error_messages_for('austrianAbbeyMembership') - ); - } - - public function test_no_files_exception_get_files(): void { - $this->setFiles(null); - $this->expectException(\LogicException::class); - - $this->form->get_files(); - } - - public function test_no_files_exception_get_file(): void { - $this->setFiles(null); - $this->expectException(\LogicException::class); - - $this->form->get_file('favoriteThings'); - } - - public function test_no_files_exception_require_file(): void { - $this->setFiles(null); - $this->setFields([ - 'austrianAbbeyMembership' => [ - 'validators' => [ [ $this->form, 'require_file' ] ], - ], - ]); - - $this->expectException(\LogicException::class); - - $this->form->validate([]); - } - - /** - * Set the specified fields. - * - * @param array<array<string, array<string, mixed>>, mixed> $fields - */ - protected function setFields(array $fields ) { - $this->setProtectedProperty($this->form, 'fields', $fields); - } - - /** - * Set the specified files. - - * @param array<string, array<string, float|int|string>> $files - */ - protected function setFiles(array $files = null ) { - $this->setProtectedProperty($this->form, 'files', $files); - } - - /** - * Get the default fields. - * - * @return array<string, array|array<string, array<int, array>|string>|array<string, string[]>> - */ - protected function getDefaultFields(): array { - return [ - 'first_name' => [ - 'validators' => [ [ $this->form, 'require' ] ], + ], + ]); + $whitelist = $this->form->get_whitelisted_fields(['adjective' => '']); + + $this->assertEquals( + 'supercalifragilisticexpialidocious', + $whitelist['adjective'] + ); + } + + public function test_get_file() { + $this->assertNotEmpty($this->form->get_file('favoriteThings')); + } + + public function test_required_file_missing() { + $this->setFields([ + 'leastFavoriteThings' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('leastFavoriteThings')); + } + + public function test_file_mime_type_valid() { + $this->setFields([ + 'favoriteThings' => [ + 'validators' => [[$this->form, 'validate_file_mime_type', ['text/plain']]], + ], + ]); + + $this->assertTrue($this->form->validate([])); + $this->assertEmpty($this->form->get_errors()); + } + + public function test_file_mime_type_invalid() { + $this->setFields([ + 'favoriteThings' => [ + 'validators' => [[$this->form, 'validate_file_mime_type', ['application/pdf']]], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('favoriteThings')); + $this->assertEquals( + [sprintf($this->form::MESSAGE_INVALID_MIME_TYPE, 'favoriteThings')], + $this->form->get_error_messages_for('favoriteThings') + ); + } + + public function test_file_upload_error_ini_size() { + $this->setFields([ + 'uploadErrorSizeIni' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorSizeIni')); + $this->assertEquals( + [sprintf($this->form::MESSAGE_FILE_SIZE, 'uploadErrorSizeIni')], + $this->form->get_error_messages_for('uploadErrorSizeIni') + ); + } + + public function test_file_upload_error_form_size() { + $this->setFields([ + 'uploadErrorSizeForm' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorSizeForm')); + $this->assertEquals( + [sprintf($this->form::MESSAGE_FILE_SIZE, 'uploadErrorSizeForm')], + $this->form->get_error_messages_for('uploadErrorSizeForm') + ); + } + + public function test_file_upload_error_partial() { + $this->setFields([ + 'uploadErrorPartialFile' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('uploadErrorPartialFile')); + $this->assertEquals( + [sprintf($this->form::MESSAGE_UPLOAD_ERROR, 'uploadErrorPartialFile')], + $this->form->get_error_messages_for('uploadErrorPartialFile') + ); + } + + public function test_file_upload_error_no_file() { + $this->setFields([ + 'austrianAbbeyMembership' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->assertFalse($this->form->validate([])); + $this->assertNotEmpty($this->form->get_error_messages_for('austrianAbbeyMembership')); + $this->assertEquals( + [sprintf($this->form::MESSAGE_FIELD_REQUIRED, 'austrianAbbeyMembership')], + $this->form->get_error_messages_for('austrianAbbeyMembership') + ); + } + + public function test_no_files_exception_get_files() { + $this->setFiles(null); + $this->expectException(\LogicException::class); + + $this->form->get_files(); + } + + public function test_no_files_exception_get_file() { + $this->setFiles(null); + $this->expectException(\LogicException::class); + + $this->form->get_file('favoriteThings'); + } + + public function test_no_files_exception_require_file() { + $this->setFiles(null); + $this->setFields([ + 'austrianAbbeyMembership' => [ + 'validators' => [[$this->form, 'require_file']], + ], + ]); + + $this->expectException(\LogicException::class); + + $this->form->validate([]); + } + + protected function setFields(array $fields) { + $this->setProtectedProperty($this->form, 'fields', $fields); + } + + protected function setFiles(array $files = null) { + $this->setProtectedProperty($this->form, 'files', $files); + } + + protected function getDefaultFields() { + return [ + 'first_name' => [ + 'validators' => [[$this->form, 'require']], 'required_message' => 'Kindly tell us your first name.', - ], - 'last_name' => [ - 'validators' => [ [ $this->form, 'require' ] ], + ], + 'last_name' => [ + 'validators' => [[$this->form, 'require']], 'required_message' => 'Kindly tell us your last name.', - ], - 'yes_or_no' => [ - 'options' => [ 'yes', 'no' ], - ], - 'highest_award' => [], - 'favorite_things' => [ - 'options' => [ 'raindrops', 'whiskers', 'kettles', 'mittens' ], + ], + 'yes_or_no' => [ + 'options' => ['yes', 'no'], + ], + 'highest_award' => [], + 'favorite_things' => [ + 'options' => ['raindrops', 'whiskers', 'kettles', 'mittens'], // TODO validate at_least - ], - ]; - } - - /** - * Get the default files. - - * @return array<string, array<string, int|string>|array<string, float|int|string>> - */ - protected function getDefaultFiles(): array { - return [ - 'favoriteThings' => [ + ], + ]; + } + + protected function getDefaultFiles() { + return [ + 'favoriteThings' => [ 'name' => 'My%20Favorite%20Things.txt', 'type' => 'text/plain', 'tmp_name' => '/tmp/php/somethingarbitrary', 'error' => UPLOAD_ERR_OK, 'size' => 16.99, - ], - 'uploadErrorSizeIni' => [ + ], + 'uploadErrorSizeIni' => [ 'name' => 'My%20Favorite%20Things.txt', 'type' => 'text/plain', 'tmp_name' => '/tmp/php/somethingarbitrary', 'error' => UPLOAD_ERR_INI_SIZE, 'size' => 17, - ], - 'uploadErrorSizeForm' => [ + ], + 'uploadErrorSizeForm' => [ 'name' => 'My%20Favorite%20Things.txt', 'type' => 'text/plain', 'tmp_name' => '/tmp/php/somethingarbitrary', 'error' => UPLOAD_ERR_FORM_SIZE, 'size' => 17, - ], - 'uploadErrorPartialFile' => [ + ], + 'uploadErrorPartialFile' => [ 'name' => 'My%20Favorite%20Things.txt', 'type' => 'text/plain', 'tmp_name' => '/tmp/php/somethingarbitrary', 'error' => UPLOAD_ERR_PARTIAL, 'size' => 16.99, - ], - 'austrianAbbeyMembership' => [ + ], + 'austrianAbbeyMembership' => [ 'name' => 'Nonnberg_Abbey_Nun_ID.jpg', 'type' => 'image/jpeg', 'tmp_name' => '/tmp/php/somethingarbitrary', 'error' => UPLOAD_ERR_NO_FILE, 'size' => 16.99, - ], - ]; - } + ], + ]; + } } diff --git a/test/unit/PostRegistrationTest.php b/test/unit/PostRegistrationTest.php index d743454..2d5ebf1 100644 --- a/test/unit/PostRegistrationTest.php +++ b/test/unit/PostRegistrationTest.php @@ -7,8 +7,6 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; use Conifer\Post\Post; @@ -18,14 +16,14 @@ use WP_Term; class PostRegistrationTest extends Base { - public function test_register_type(): void { - /* https://codex.wordpress.org/Function_Reference/register_post_type */ - WP_Mock::userFunction('register_post_type', [ - 'times' => 1, - 'args' => [ + public function test_register_type() { + /* https://codex.wordpress.org/Function_Reference/register_post_type */ + WP_Mock::userFunction('register_post_type', [ + 'times' => 1, + 'args' => [ 'person', [ - 'labels' => [ + 'labels' => [ 'name' => 'People', 'singular_name' => 'Person', 'add_new_item' => 'Onboard New Person', @@ -41,23 +39,23 @@ public function test_register_type(): void { 'attributes' => 'Person Attributes', 'insert_into_item' => 'Insert into description', 'uploaded_to_this_item' => 'Uploaded to this Person', - ], - ], + ], ], - ]); + ], + ]); - $this->assertNull(Person::register_type()); - } + $this->assertNull(Person::register_type()); + } - public function test_register_taxonomy(): void { - /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ - WP_Mock::userFunction('register_taxonomy', [ - 'times' => 1, - 'args' => [ + public function test_register_taxonomy() { + /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ + WP_Mock::userFunction('register_taxonomy', [ + 'times' => 1, + 'args' => [ 'sign', 'person', [ - 'labels' => [ + 'labels' => [ 'name' => 'Signs', 'singular_name' => 'Sign', 'menu_name' => 'Signs', @@ -76,26 +74,26 @@ public function test_register_taxonomy(): void { 'choose_from_most_used' => 'Choose from the most used Signs', 'not_found' => 'No Signs found', 'back_to_items' => '← Back to Signs', - ], + ], ], - ], - ]); + ], + ]); - $this->assertNull(Person::register_taxonomy('sign', [ - 'plural_label' => 'Signs', - 'labels' => [ 'all_items' => 'All Astrological Signs' ], - ])); - } + $this->assertNull(Person::register_taxonomy('sign', [ + 'plural_label' => 'Signs', + 'labels' => ['all_items' => 'All Astrological Signs'], + ])); + } - public function test_register_taxonomy_without_explicit_options(): void { - /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ - WP_Mock::userFunction('register_taxonomy', [ - 'times' => 1, - 'args' => [ + public function test_register_taxonomy_without_explicit_options() { + /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ + WP_Mock::userFunction('register_taxonomy', [ + 'times' => 1, + 'args' => [ 'sign', 'person', [ - 'labels' => [ + 'labels' => [ 'name' => 'Signs', 'singular_name' => 'Sign', 'menu_name' => 'Signs', @@ -114,26 +112,26 @@ public function test_register_taxonomy_without_explicit_options(): void { 'choose_from_most_used' => 'Choose from the most used Signs', 'not_found' => 'No Signs found', 'back_to_items' => '← Back to Signs', - ], - ], + ], ], - ]); + ], + ]); - $this->assertNull(Person::register_taxonomy('sign', [ - 'plural_label' => 'Signs', - 'labels' => [ 'all_items' => 'All Astrological Signs' ], - ])); - } + $this->assertNull(Person::register_taxonomy('sign', [ + 'plural_label' => 'Signs', + 'labels' => ['all_items' => 'All Astrological Signs'], + ])); + } - public function test_register_taxonomy_with_underscores(): void { - /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ - WP_Mock::userFunction('register_taxonomy', [ - 'times' => 1, - 'args' => [ + public function test_register_taxonomy_with_underscores() { + /* https://codex.wordpress.org/Function_Reference/register_taxonomy */ + WP_Mock::userFunction('register_taxonomy', [ + 'times' => 1, + 'args' => [ 'personal_attribute', 'person', [ - 'labels' => [ + 'labels' => [ 'name' => 'Personal Attributes', 'singular_name' => 'Personal Attribute', 'menu_name' => 'Personal Attributes', @@ -152,24 +150,25 @@ public function test_register_taxonomy_with_underscores(): void { 'choose_from_most_used' => 'Choose from the most used Personal Attributes', 'not_found' => 'No Personal Attributes found', 'back_to_items' => '← Back to Personal Attributes', - ], + ], ], - ], - ]); + ], + ]); - $this->assertNull(Person::register_taxonomy('personal_attribute')); - } + $this->assertNull(Person::register_taxonomy('personal_attribute')); + } - public function test_register_taxonomy_omitting_post_type(): void { - WP_Mock::userFunction('register_taxonomy', [ - 'times' => 1, - 'args' => [ + public function test_register_taxonomy_omitting_post_type() { + WP_Mock::userFunction('register_taxonomy', [ + 'times' => 1, + 'args' => [ 'sign', null, Functions::type('array'), - ], - ]); + ], + ]); + + $this->assertNull(Person::register_taxonomy('sign', [], true)); + } - $this->assertNull(Person::register_taxonomy('sign', [], true)); - } } diff --git a/test/unit/SendsEmailTest.php b/test/unit/SendsEmailTest.php index c46780f..ed54cfb 100644 --- a/test/unit/SendsEmailTest.php +++ b/test/unit/SendsEmailTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Notifier\SendsEmail trait * @@ -7,71 +6,68 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use Conifer\Notifier\SendsEmail; class SendsEmailTest extends Base { - const TO_ADDRESS = 'you@example.com'; - - const HTML_HEADERS = [ 'Content-Type: text/html; charset=UTF-8' ]; - - protected MockObject $notifier; - - protected function setUp(): void { - parent::setUp(); - - $this->notifier = $this->getMockForTrait(SendsEmail::class); - - // mock the abstract to() method - $this->notifier - ->expects($this->any()) - ->method('to') - ->will($this->returnValue(self::TO_ADDRESS)); - } - - public function test_html_message(): void { - $expectedHeaders = self::HTML_HEADERS; - $expectedHeaders[] = 'x-can-haz-cheezburger: yas'; - - // assert that it adds in the correct header - WP_Mock::userFunction('wp_mail', [ - 'times' => 1, - 'args' => [ self::TO_ADDRESS, 'hi', 'lorem ipsum', $expectedHeaders ], - 'return' => true, - ]); - - // pass custom headers - $this->assertTrue($this->notifier->send_html_message( - self::TO_ADDRESS, - 'hi', - 'lorem ipsum', - [ 'x-can-haz-cheezburger: yas' ] - )); - } - - public function test_notify_html(): void { - WP_Mock::userFunction('wp_mail', [ - 'times' => 1, - 'args' => [ self::TO_ADDRESS, 'hi', 'lorem ipsum', self::HTML_HEADERS ], - 'return' => true, - ]); - - $this->assertTrue($this->notifier->notify('hi', 'lorem ipsum')); - } - - public function test_notify_plaintext(): void { - WP_Mock::userFunction('wp_mail', [ - 'times' => 1, - 'args' => [ self::TO_ADDRESS, 'hi', 'lorem ipsum', [] ], - 'return' => true, - ]); - - $this->assertTrue($this->notifier->notify_plaintext('hi', 'lorem ipsum')); - } + const TO_ADDRESS = 'you@example.com'; + + const HTML_HEADERS = ['Content-Type: text/html; charset=UTF-8']; + + protected $notifier; + + public function setUp(): void { + parent::setUp(); + + $this->notifier = $this->getMockForTrait(SendsEmail::class); + + // mock the abstract to() method + $this->notifier + ->expects($this->any()) + ->method('to') + ->will($this->returnValue(self::TO_ADDRESS)); + } + + public function test_html_message() { + $expectedHeaders = self::HTML_HEADERS; + $expectedHeaders[] = 'x-can-haz-cheezburger: yas'; + + // assert that it adds in the correct header + WP_Mock::userFunction('wp_mail', [ + 'times' => 1, + 'args' => [self::TO_ADDRESS, 'hi', 'lorem ipsum', $expectedHeaders], + 'return' => true, + ]); + + // pass custom headers + $this->assertTrue($this->notifier->send_html_message( + self::TO_ADDRESS, + 'hi', + 'lorem ipsum', + ['x-can-haz-cheezburger: yas'] + )); + } + + public function test_notify_html() { + WP_Mock::userFunction('wp_mail', [ + 'times' => 1, + 'args' => [self::TO_ADDRESS, 'hi', 'lorem ipsum', self::HTML_HEADERS], + 'return' => true, + ]); + + $this->assertTrue($this->notifier->notify('hi', 'lorem ipsum')); + } + + public function test_notify_plaintext() { + WP_Mock::userFunction('wp_mail', [ + 'times' => 1, + 'args' => [self::TO_ADDRESS, 'hi', 'lorem ipsum', []], + 'return' => true, + ]); + + $this->assertTrue($this->notifier->notify_plaintext('hi', 'lorem ipsum')); + } } diff --git a/test/unit/Shortcode/ButtonTest.php b/test/unit/Shortcode/ButtonTest.php index d8bb4de..0d831dd 100644 --- a/test/unit/Shortcode/ButtonTest.php +++ b/test/unit/Shortcode/ButtonTest.php @@ -7,41 +7,39 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; use Conifer\Shortcode\Button; class ButtonTest extends Base { - protected $button; - - protected function setUp(): void { - parent::setUp(); - - $this->button = new Button(); - } - - public function test_render_with_link(): void { - $this->assertEquals( - '<a href="/link" class="btn">A Link</a>', - $this->button->render([], '<a href="/link">A Link</a>') - ); - } - - public function test_render_with_wrapped_link(): void { - $this->assertEquals( - '<span><a href="/link" class="btn">A Link</a></span>', - $this->button->render([], '<span><a href="/link">A Link</a></span>') - ); - } - - public function test_render_with_custom_class(): void { - $this->assertEquals( - '<a href="/link" class="my-button-class">A Link</a>', - $this->button->render( - [ 'class' => 'my-button-class' ], + protected $button; + + public function setUp(): void { + parent::setUp(); + + $this->button = new Button(); + } + + public function test_render_with_link() { + $this->assertEquals( + '<a href="/link" class="btn">A Link</a>', + $this->button->render([], '<a href="/link">A Link</a>') + ); + } + + public function test_render_with_wrapped_link() { + $this->assertEquals( + '<span><a href="/link" class="btn">A Link</a></span>', + $this->button->render([], '<span><a href="/link">A Link</a></span>') + ); + } + + public function test_render_with_custom_class() { + $this->assertEquals( + '<a href="/link" class="my-button-class">A Link</a>', + $this->button->render( + ['class' => 'my-button-class'], '<a href="/link">A Link</a>' - )); - } + )); + } } diff --git a/test/unit/ShortcodeAuthorizationPolicyTest.php b/test/unit/ShortcodeAuthorizationPolicyTest.php index eb91a36..e84515e 100644 --- a/test/unit/ShortcodeAuthorizationPolicyTest.php +++ b/test/unit/ShortcodeAuthorizationPolicyTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the ShortcodePolicy class * @@ -7,11 +6,8 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use Timber\User; @@ -19,60 +15,60 @@ use Conifer\Authorization\ShortcodePolicy; class ShortcodeAuthorizationPolicyTest extends Base { - private MockObject $policy; - - protected function setUp(): void { - parent::setUp(); - $this->policy = $this->getMockBuilder(ShortcodePolicy::class)->setMethods([ 'tag' ])->getMockForAbstractClass(); - } - - public function test_adopt(): void { - $this->policy->expects($this->once()) - ->method('tag') - ->will($this->returnValue('foobar')); - WP_Mock::userFunction('add_shortcode', [ - 'times' => 1, - 'args' => [ + private $policy; + + public function setUp(): void { + parent::setUp(); + $this->policy = $this->getMockBuilder(ShortcodePolicy::class)->setMethods(['tag'])->getMockForAbstractClass(); + } + + public function test_adopt() { + $this->policy->expects($this->once()) + ->method('tag') + ->will($this->returnValue('foobar')); + WP_Mock::userFunction('add_shortcode', [ + 'times' => 1, + 'args' => [ 'foobar', WP_Mock\Functions::type('callable'), - ], - ]); - - $policy = $this->policy->adopt(); - - // test fluent interface - $this->assertEquals($policy, $this->policy); - } - - public function test_enforce_when_unauthorized(): void { - $this->markTestSkipped(); - $user = $this->mockCurrentUser(123); - - $this->policy->expects($this->once()) - ->method('decide') - ->will($this->returnValue(false)); - - $this->assertEquals('', $this->policy->enforce( - [], - 'This is restricted content', - $user - )); - } - - public function test_enforce_when_authorized(): void { - $this->markTestSkipped(); - $user = $this->mockCurrentUser(123); - - $this->policy->expects($this->once()) - ->method('decide') - ->will($this->returnValue(true)); - - // a restricted place with golden trees - $restricted = 'CAN YOU TAKE ME HIGHER'; - $this->assertEquals($restricted, $this->policy->enforce( - [], - $restricted, - $user - )); - } + ], + ]); + + $policy = $this->policy->adopt(); + + // test fluent interface + $this->assertEquals($policy, $this->policy); + } + + public function test_enforce_when_unauthorized() { + $this->markTestSkipped(); + $user = $this->mockCurrentUser(123); + + $this->policy->expects($this->once()) + ->method('decide') + ->will($this->returnValue(false)); + + $this->assertEquals('', $this->policy->enforce( + [], + 'This is restricted content', + $user + )); + } + + public function test_enforce_when_authorized() { + $this->markTestSkipped(); + $user = $this->mockCurrentUser(123); + + $this->policy->expects($this->once()) + ->method('decide') + ->will($this->returnValue(true)); + + // a restricted place with golden trees + $restricted = 'CAN YOU TAKE ME HIGHER'; + $this->assertEquals($restricted, $this->policy->enforce( + [], + $restricted, + $user + )); + } } diff --git a/test/unit/SiteTest.php b/test/unit/SiteTest.php index bdb108e..7e859f0 100644 --- a/test/unit/SiteTest.php +++ b/test/unit/SiteTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Site class * @@ -7,11 +6,8 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; -use org\bovigo\vfs\vfsStreamDirectory; use WP_Mock; use Conifer\Site; @@ -19,238 +15,244 @@ use org\bovigo\vfs\vfsStream; class SiteTest extends Base { - public vfsStreamDirectory $file_system; - - const THEME_DIRECTORY = 'wp-content/themes/foo'; + const THEME_DIRECTORY = 'wp-content/themes/foo'; - protected function setUp(): void { - parent::setUp(); + public function setUp(): void { + parent::setUp(); - // do a terrible amount of boilerplate to workaround Timber's decision - // to put a ton of stuff in the constructor - $theme = $this->getMockBuilder('\WP_Theme') - ->setMethods([ + // do a terrible amount of boilerplate to workaround Timber's decision + // to put a ton of stuff in the constructor + $theme = $this->getMockBuilder('\WP_Theme') + ->setMethods([ 'get', 'get_stylesheet', 'get_template_directory_uri', 'parent', '__toString', - ]) - ->getMock(); - $theme->expects($this->any()) - ->method('__toString') - ->will($this->returnValue('')); - - WP_Mock::userFunction('is_multisite', [ - 'return' => false, - ]); - WP_Mock::userFunction('home_url', [ - 'return' => 'http://appserver', - ]); - WP_Mock::userFunction('site_url', [ - 'return' => 'http://appserver', - ]); - WP_Mock::userFunction('get_bloginfo', [ - 'return' => [], - ]); - WP_Mock::userFunction('wp_get_theme', [ - 'return' => $theme, - ]); - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => self::THEME_DIRECTORY, - 'times' => 4, - ]); - - WP_Mock::userFunction('get_locale', [ - 'return' => 'en_US', - ]); - - // Set up a new virtual file system to test some of the site functions - $structure = [ - 'theme-dir' => [ - 'test.php' => 'some text content', - 'assets.version' =>'1', - 'custom-assets.version' => 'CUSTOM', - ], - 'an_empty_folder' => [], - ]; - $this->file_system = vfsStream::setup('root', null, $structure); - } - - protected function tearDown(): void { - WP_Mock::tearDown(); - } - - public function test_find_file(): void { - - $site = new Site(); - - $fileURL = $site->find_file('test.php', [ - vfsStream::url('root/theme-dir/'), - vfsStream::url('root/an_empty_folder/'), - ]); - - $this->assertEquals('vfs://root/theme-dir/test.php', $fileURL); - } - - public function test_find_file_without_trailing_slash(): void { - - $site = new Site(); - - $fileURL = $site->find_file('test.php', [ - vfsStream::url('root/theme-dir'), - ]); - - $this->assertEquals('vfs://root/theme-dir/test.php', $fileURL); - } - - public function test_find_file_fail(): void { - - $site = new Site(); - - $fileURL = $site->find_file('test2.php', [ - vfsStream::url('root/theme-dir/'), - vfsStream::url('root/an_empty_folder/'), - ]); - - $this->assertEquals('', $fileURL); - } - - public function test_get_assets_version(): void { - - $site = new Site(); - - // We will set the stylesheet directory to our virtual file system directory - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => vfsStream::url('root/theme-dir'), - 'times' => 2, - ]); - - // read the file value from our file in the virtual file system directory - $this->assertEquals( - '1', - $site->get_assets_version() - ); - } - - public function test_get_assets_version_with_arg(): void { - - $site = new Site(); - - // We will set the stylesheet directory to our virtual file system directory - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => vfsStream::url('root/theme-dir'), - 'times' => 2, - ]); - - // read the file value from our file in the virtual file system directory - $this->assertEquals( - 'CUSTOM', - $site->get_assets_version('custom-assets.version') - ); - } - - public function test_subsequent_get_assets_version_with(): void { - - $site = new Site(); - - // We will set the stylesheet directory to our virtual file system directory - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => vfsStream::url('root/theme-dir'), - 'times' => 4, - ]); - - // read the file value from our file in the virtual file system directory - $this->assertEquals( - 'CUSTOM', - $site->get_assets_version('custom-assets.version') - ); - $this->assertEquals( - '1', - $site->get_assets_version('assets.version') - ); - } - - public function test_get_assets_version_with_no_file(): void { - - $site = new Site(); - - $this->file_system = vfsStream::setup('root', null, [ - 'theme-dir' => [], - ]); - - // We will set the stylesheet directory to our virtual file system directory - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => vfsStream::url('root/theme-dir'), - 'times' => 1, - ]); - - // read the file value from our file in the virtual file system directory - $this->assertEquals('', $site->get_assets_version()); - } - - public function test_get_theme_file(): void { - $site = new Site(); - - WP_Mock::userFunction('get_stylesheet_directory', [ - 'return' => 'wp-content/themes/foo', - ]); - - // method should add a leading slash to the filename if necessary - $this->assertEquals( - self::THEME_DIRECTORY . '/bar.txt', - $site->get_theme_file('bar.txt') - ); - // a leading slash should be preserved - $this->assertEquals( - self::THEME_DIRECTORY . '/bar.txt', - $site->get_theme_file('/bar.txt') - ); - } - - public function test_add_twig_helper(): void { - $site = new Site(); - - // mock HelperInterface - $helper = $this->getMockBuilder(HelperInterface::class) - ->getMock(); - - // mock Twig API - WP_Mock::expectFilterAdded('get_twig', WP_Mock\Functions::type('callable')); - - // add the helper - // NOTE: the real assertion is above, we just assert null here so PHPUnit - // doesn't yell at us for not asserting anything - $this->assertNull($site->add_twig_helper($helper)); - } - - public function test_get_twig_with_helper(): void { - $site = new Site(); - - // mock Twig API - $twig = $this->getMockBuilder(\Twig\Environment::class) - ->disableOriginalConstructor() - ->setMethods([ 'addFilter', 'addFunction' ]) - ->getMock(); - $twig->expects($this->once()) - ->method('addFilter'); - $twig->expects($this->once()) - ->method('addFunction'); - - // mock HelperInterface - $helper = $this->getMockBuilder(HelperInterface::class) - ->setMethods([ 'get_functions', 'get_filters' ]) - ->getMock(); - $helper->expects($this->once()) - ->method('get_filters') - ->will($this->returnValue([ 'foo' => function (): void {} ])); - $helper->expects($this->once()) - ->method('get_functions') - ->will($this->returnValue([ 'bar' => function (): void {} ])); - - $this->assertEquals( - $twig, - $site->get_twig_with_helper($twig, $helper) - ); - } + ]) + ->getMock(); + $theme->expects($this->any()) + ->method('__toString') + ->will($this->returnValue('')); + + WP_Mock::userFunction('is_multisite', [ + 'return' => false, + ]); + WP_Mock::userFunction('home_url', [ + 'return' => 'http://appserver', + ]); + WP_Mock::userFunction('site_url', [ + 'return' => 'http://appserver', + ]); + WP_Mock::userFunction('get_bloginfo', [ + 'return' => [], + ]); + WP_Mock::userFunction('wp_get_theme', [ + 'return' => $theme, + ]); + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => self::THEME_DIRECTORY, + 'times' => 4, + ]); + + WP_Mock::userFunction('get_locale', [ + 'return' => 'en_US', + ]); + + // Set up a new virtual file system to test some of the site functions + $structure = [ + 'theme-dir' => [ + 'test.php' => 'some text content', + 'assets.version' =>'1', + 'custom-assets.version' => 'CUSTOM', + ], + 'an_empty_folder' => [], + ]; + $this->file_system = vfsStream::setup('root', null, $structure); + + } + + public function tearDown(): void { + WP_Mock::tearDown(); + } + + public function test_find_file() { + + $site = new Site(); + + $fileURL = $site->find_file('test.php', [ + vfsStream::url('root/theme-dir/'), + vfsStream::url('root/an_empty_folder/'), + ]); + + $this->assertEquals('vfs://root/theme-dir/test.php', $fileURL); + + } + + public function test_find_file_without_trailing_slash() { + + $site = new Site(); + + $fileURL = $site->find_file('test.php', [ + vfsStream::url('root/theme-dir'), + ]); + + $this->assertEquals('vfs://root/theme-dir/test.php', $fileURL); + + } + + public function test_find_file_fail() { + + $site = new Site(); + + $fileURL = $site->find_file('test2.php', [ + vfsStream::url('root/theme-dir/'), + vfsStream::url('root/an_empty_folder/'), + ]); + + $this->assertEquals('', $fileURL); + + } + + public function test_get_assets_version() { + + $site = new Site(); + + // We will set the stylesheet directory to our virtual file system directory + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => vfsStream::url('root/theme-dir'), + 'times' => 2, + ]); + + // read the file value from our file in the virtual file system directory + $this->assertEquals( + '1', + $site->get_assets_version() + ); + + } + + public function test_get_assets_version_with_arg() { + + $site = new Site(); + + // We will set the stylesheet directory to our virtual file system directory + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => vfsStream::url('root/theme-dir'), + 'times' => 2, + ]); + + // read the file value from our file in the virtual file system directory + $this->assertEquals( + 'CUSTOM', + $site->get_assets_version('custom-assets.version') + ); + + } + + public function test_subsequent_get_assets_version_with() { + + $site = new Site(); + + // We will set the stylesheet directory to our virtual file system directory + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => vfsStream::url('root/theme-dir'), + 'times' => 4, + ]); + + // read the file value from our file in the virtual file system directory + $this->assertEquals( + 'CUSTOM', + $site->get_assets_version('custom-assets.version') + ); + $this->assertEquals( + '1', + $site->get_assets_version('assets.version') + ); + + } + + public function test_get_assets_version_with_no_file() { + + $site = new Site(); + + $this->file_system = vfsStream::setup('root', null, [ + 'theme-dir' => [], + ]); + + // We will set the stylesheet directory to our virtual file system directory + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => vfsStream::url('root/theme-dir'), + 'times' => 1, + ]); + + // read the file value from our file in the virtual file system directory + $this->assertEquals('', $site->get_assets_version()); + + } + + public function test_get_theme_file() { + $site = new Site(); + + WP_Mock::userFunction('get_stylesheet_directory', [ + 'return' => 'wp-content/themes/foo', + ]); + + // method should add a leading slash to the filename if necessary + $this->assertEquals( + self::THEME_DIRECTORY . '/bar.txt', + $site->get_theme_file('bar.txt') + ); + // a leading slash should be preserved + $this->assertEquals( + self::THEME_DIRECTORY . '/bar.txt', + $site->get_theme_file('/bar.txt') + ); + } + + public function test_add_twig_helper() { + $site = new Site(); + + // mock HelperInterface + $helper = $this->getMockBuilder(HelperInterface::class) + ->getMock(); + + // mock Twig API + WP_Mock::expectFilterAdded('get_twig', WP_Mock\Functions::type('callable')); + + // add the helper + // NOTE: the real assertion is above, we just assert null here so PHPUnit + // doesn't yell at us for not asserting anything + $this->assertNull($site->add_twig_helper($helper)); + } + + public function test_get_twig_with_helper() { + $site = new Site(); + + // mock Twig API + $twig = $this->getMockBuilder('Twig\Environment') + ->disableOriginalConstructor() + ->setMethods(['addFilter', 'addFunction']) + ->getMock(); + $twig->expects($this->once()) + ->method('addFilter'); + $twig->expects($this->once()) + ->method('addFunction'); + + // mock HelperInterface + $helper = $this->getMockBuilder(HelperInterface::class) + ->setMethods(['get_functions', 'get_filters']) + ->getMock(); + $helper->expects($this->once()) + ->method('get_filters') + ->will($this->returnValue(['foo' => function() {}])); + $helper->expects($this->once()) + ->method('get_functions') + ->will($this->returnValue(['bar' => function() {}])); + + $this->assertEquals( + $twig, + $site->get_twig_with_helper($twig, $helper) + ); + } } diff --git a/test/unit/TemplateAuthorizationPolicyTest.php b/test/unit/TemplateAuthorizationPolicyTest.php index 68b1b17..c00c70f 100644 --- a/test/unit/TemplateAuthorizationPolicyTest.php +++ b/test/unit/TemplateAuthorizationPolicyTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the TemplateAuthorizationPolicy class * @@ -7,30 +6,27 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; -use PHPUnit\Framework\MockObject\MockObject; use WP_Mock; use Conifer\Authorization\TemplatePolicy; class TemplateAuthorizationPolicyTest extends Base { - private MockObject $policy; - - protected function setUp(): void { - parent::setUp(); - $this->policy = $this->getMockForAbstractClass( - TemplatePolicy::class - ); - } - - public function test_adopt(): void { - WP_Mock::expectFilterAdded('template_include', WP_Mock\Functions::type('callable')); - $policy = $this->policy->adopt(); - - // test fluent interface - $this->assertEquals($policy, $this->policy); - } + private $policy; + + public function setUp(): void { + parent::setUp(); + $this->policy = $this->getMockForAbstractClass( + TemplatePolicy::class + ); + } + + public function test_adopt() { + WP_Mock::expectFilterAdded('template_include', WP_Mock\Functions::type('callable')); + $policy = $this->policy->adopt(); + + // test fluent interface + $this->assertEquals($policy, $this->policy); + } } diff --git a/test/unit/TestHelperTest.php b/test/unit/TestHelperTest.php index c618826..44f1415 100644 --- a/test/unit/TestHelperTest.php +++ b/test/unit/TestHelperTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the Conifer\Site class * @@ -7,64 +6,60 @@ * @author Coby Tamayo <ctamayo@sitecrafting.com> */ -declare(strict_types=1); - namespace Conifer\Unit; use Conifer\Twig\TextHelper; class TextHelperTest extends Base { - public $helper; - - const THEME_DIRECTORY = 'wp-content/themes/foo'; + const THEME_DIRECTORY = 'wp-content/themes/foo'; - protected function setUp(): void { - parent::setUp(); + public function setUp(): void { + parent::setUp(); - $this->helper = new TextHelper(); - } + $this->helper = new TextHelper(); + } - public function test_oxford_comma(): void { - $this->assertEquals( - 'one', - $this->helper->oxford_comma([ 'one' ]) - ); - $this->assertEquals( - 'one and two', - $this->helper->oxford_comma([ 'one', 'two' ]) - ); - $this->assertEquals( - 'one, two, and three', - $this->helper->oxford_comma([ 'one', 'two', 'three' ]) - ); - $this->assertEquals( - 'one, two, three, and four', - $this->helper->oxford_comma([ 'one', 'two', 'three', 'four' ]) - ); - } + public function test_oxford_comma() { + $this->assertEquals( + 'one', + $this->helper->oxford_comma(['one']) + ); + $this->assertEquals( + 'one and two', + $this->helper->oxford_comma(['one', 'two']) + ); + $this->assertEquals( + 'one, two, and three', + $this->helper->oxford_comma(['one', 'two', 'three']) + ); + $this->assertEquals( + 'one, two, three, and four', + $this->helper->oxford_comma(['one', 'two', 'three', 'four']) + ); + } - public function test_pluralize(): void { - $this->assertEquals('person', $this->helper->pluralize('person', 1)); - $this->assertEquals('people', $this->helper->pluralize('person', 2)); - $this->assertEquals('zebras', $this->helper->pluralize('zebra', 2)); - } + public function test_pluralize() { + $this->assertEquals('person', $this->helper->pluralize('person', 1)); + $this->assertEquals('people', $this->helper->pluralize('person', 2)); + $this->assertEquals('zebras', $this->helper->pluralize('zebra', 2)); + } - public function test_capitalize_each(): void { - $this->assertEquals( - 'Three Blind Mice', - $this->helper->capitalize_each('three blind mice') - ); - $this->assertEquals( - 'One, Two, or Three', - $this->helper->capitalize_each('one, two, or three') - ); - $this->assertEquals( - 'The Old Man and the Sea', - $this->helper->capitalize_each('the old man and the sea') - ); - $this->assertEquals( - 'Made by SiteCrafting', - $this->helper->capitalize_each('Made By SiteCrafting') - ); - } + public function test_capitalize_each() { + $this->assertEquals( + 'Three Blind Mice', + $this->helper->capitalize_each('three blind mice') + ); + $this->assertEquals( + 'One, Two, or Three', + $this->helper->capitalize_each('one, two, or three') + ); + $this->assertEquals( + 'The Old Man and the Sea', + $this->helper->capitalize_each('the old man and the sea') + ); + $this->assertEquals( + 'Made by SiteCrafting', + $this->helper->capitalize_each('Made By SiteCrafting') + ); + } } diff --git a/test/unit/Twig/Filters/FormHelperTest.php b/test/unit/Twig/Filters/FormHelperTest.php index 4a7ee6a..b77ed03 100644 --- a/test/unit/Twig/Filters/FormHelperTest.php +++ b/test/unit/Twig/Filters/FormHelperTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the FormHelper methods exposed to Twig * @@ -7,103 +6,92 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; use Conifer\Form\AbstractBase as Form; use Conifer\Twig\FormHelper; -use PHPUnit\Framework\MockObject\MockObject; class FormHelperTest extends Base { - public $wrapper; - - protected function setUp(): void { - parent::setUp(); - $this->wrapper = new FormHelper(); - } - - public function test_field_class(): void { - // mock up some form errors - $form = $this->getMockForAbstractClass(Form::class); - $form->add_error('foo', 'error message for foo'); - - $this->assertEquals( - 'error', - $this->wrapper->get_field_class($form, 'foo') - ); - - $this->assertEquals( - 'my-error-class', - $this->wrapper->get_field_class($form, 'foo', 'my-error-class') - ); - } - - public function test_get_error_messages_for(): void { - $form = $this->getMockForAbstractClass(Form::class); - $form->add_error('foo', 'error message for foo'); - $form->add_error('foo', 'another error for foo'); - - $this->assertEquals( - 'error message for foo<br>another error for foo', - $this->wrapper->get_error_messages_for($form, 'foo') - ); - - $this->assertEquals( - 'error message for foo; another error for foo', - $this->wrapper->get_error_messages_for($form, 'foo', '; ') - ); - } - - public function test_checked_attr(): void { - $form = $this->setup_form([ - // field config - 'my_checkbox' => [], - ], [ - // field config - 'my_checkbox' => '1', - ]); - - $this->assertEquals( - ' checked ', - $this->wrapper->checked_attr($form, 'my_checkbox', '1') - ); - $this->assertEquals( - '', - $this->wrapper->checked_attr($form, 'my_checkbox', 'something else') - ); - } - - public function test_selected_attr(): void { - $form = $this->setup_form([ - // field config - 'my_select' => [], - ], [ - // field config - 'my_select' => '1', - ]); - - $this->assertEquals( - ' selected ', - $this->wrapper->selected_attr($form, 'my_select', '1') - ); - $this->assertEquals( - '', - $this->wrapper->selected_attr($form, 'my_select', 'something else') - ); - } - - /** - * Setup the form for testing. - * - * @param array<string, array> $fields - * @param array<string, string> $values - */ - protected function setup_form(array $fields, array $values = [] ): MockObject { - $form = $this->getMockForAbstractClass(Form::class); - $this->setProtectedProperty($form, 'fields', $fields); - $form->hydrate($values); - - return $form; - } + public function setUp(): void { + parent::setUp(); + $this->wrapper = new FormHelper(); + } + + public function test_field_class() { + // mock up some form errors + $form = $this->getMockForAbstractClass(Form::class); + $form->add_error('foo', 'error message for foo'); + + $this->assertEquals( + 'error', + $this->wrapper->get_field_class($form, 'foo') + ); + + $this->assertEquals( + 'my-error-class', + $this->wrapper->get_field_class($form, 'foo', 'my-error-class') + ); + } + + public function test_get_error_messages_for() { + $form = $this->getMockForAbstractClass(Form::class); + $form->add_error('foo', 'error message for foo'); + $form->add_error('foo', 'another error for foo'); + + $this->assertEquals( + 'error message for foo<br>another error for foo', + $this->wrapper->get_error_messages_for($form, 'foo') + ); + + $this->assertEquals( + 'error message for foo; another error for foo', + $this->wrapper->get_error_messages_for($form, 'foo', '; ') + ); + } + + public function test_checked_attr() { + $form = $this->setup_form([ + // field config + 'my_checkbox' => [], + ], [ + // field config + 'my_checkbox' => '1', + ]); + + $this->assertEquals( + ' checked ', + $this->wrapper->checked_attr($form, 'my_checkbox', '1') + ); + $this->assertEquals( + '', + $this->wrapper->checked_attr($form, 'my_checkbox', 'something else') + ); + } + + public function test_selected_attr() { + $form = $this->setup_form([ + // field config + 'my_select' => [], + ], [ + // field config + 'my_select' => '1', + ]); + + $this->assertEquals( + ' selected ', + $this->wrapper->selected_attr($form, 'my_select', '1') + ); + $this->assertEquals( + '', + $this->wrapper->selected_attr($form, 'my_select', 'something else') + ); + } + + protected function setup_form(array $fields, array $values = []) { + $form = $this->getMockForAbstractClass(Form::class); + $this->setProtectedProperty($form, 'fields', $fields); + $form->hydrate($values); + + return $form; + } } diff --git a/test/unit/Twig/Filters/ImageTest.php b/test/unit/Twig/Filters/ImageTest.php index f9fb644..1e8a331 100644 --- a/test/unit/Twig/Filters/ImageTest.php +++ b/test/unit/Twig/Filters/ImageTest.php @@ -7,18 +7,16 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; use Conifer\Twig\ImageHelper; class ImageHelperTest extends Base { - public function test_src_to_retina(): void { - $helper = new ImageHelper(); - $this->assertEquals( - 'foo.bar@2x.baz', - $helper->src_to_retina('foo.bar.baz') - ); - } + public function test_src_to_retina() { + $helper = new ImageHelper(); + $this->assertEquals( + 'foo.bar@2x.baz', + $helper->src_to_retina('foo.bar.baz') + ); + } } diff --git a/test/unit/UserRoleShortcodePolicyTest.php b/test/unit/UserRoleShortcodePolicyTest.php index 84f7111..8203979 100644 --- a/test/unit/UserRoleShortcodePolicyTest.php +++ b/test/unit/UserRoleShortcodePolicyTest.php @@ -1,5 +1,4 @@ <?php - /** * Test the UserRoleShortcodePolicy class * @@ -7,8 +6,6 @@ * @author Coby Tamayo */ -declare(strict_types=1); - namespace Conifer\Unit; use WP_Mock; @@ -16,50 +13,50 @@ use Conifer\Authorization\UserRoleShortcodePolicy; class UserRoleShortcodeAuthorizationPolicyTest extends Base { - private UserRoleShortcodePolicy $policy; - - protected function setUp(): void { - parent::setUp(); - $this->policy = new UserRoleShortcodePolicy(); - } - - public function test_decide_authorized(): void { - $this->markTestSkipped(); - $this->assertTrue($this->policy->decide( - [ 'role' => 'editor' ], - 'some content', - $this->mockCurrentUser(123, [], [ 'wp_capabilities' => [ 'editor' => true ] ]) - )); - } - - public function test_decide_unauthorized(): void { - $this->markTestSkipped(); - $this->assertFalse($this->policy->decide( - [ 'role' => 'editor' ], - 'some content', - $this->mockCurrentUser(123, [], [ 'wp_capabilities' => [ 'subscriber' => true ] ]) - )); - } - - public function test_decide_with_default_atts(): void { - $this->markTestSkipped(); - $this->assertTrue($this->policy->decide( - [], // require "administrator" role by default - 'some content', - $this->mockCurrentUser(123, [], [ 'wp_capabilities' => [ 'administrator' => true ] ]) - )); - } - - public function test_decide_with_multiple_roles(): void { - $this->markTestSkipped(); - $user = $this->mockCurrentUser(123, [], [ - 'wp_capabilities' => [ 'editor' => true ], - ]); - - $this->assertTrue($this->policy->decide( - [ 'role' => ' editor, administrator' ], - 'some content', - $user - )); - } + private $policy; + + public function setUp(): void { + parent::setUp(); + $this->policy = new UserRoleShortcodePolicy(); + } + + public function test_decide_authorized() { + $this->markTestSkipped(); + $this->assertTrue($this->policy->decide( + ['role' => 'editor'], + 'some content', + $this->mockCurrentUser(123, [], ['wp_capabilities' => ['editor' => true]]) + )); + } + + public function test_decide_unauthorized() { + $this->markTestSkipped(); + $this->assertFalse($this->policy->decide( + ['role' => 'editor'], + 'some content', + $this->mockCurrentUser(123, [], ['wp_capabilities' => ['subscriber' => true]]) + )); + } + + public function test_decide_with_default_atts() { + $this->markTestSkipped(); + $this->assertTrue($this->policy->decide( + [], // require "administrator" role by default + 'some content', + $this->mockCurrentUser(123, [], ['wp_capabilities' => ['administrator' => true]]) + )); + } + + public function test_decide_with_multiple_roles() { + $this->markTestSkipped(); + $user = $this->mockCurrentUser(123, [], [ + 'wp_capabilities' => ['editor' => true], + ]); + + $this->assertTrue($this->policy->decide( + ['role' => ' editor, administrator'], + 'some content', + $user + )); + } } diff --git a/wp-cli.yml b/wp-cli.yml index 16520be..09899c2 100644 --- a/wp-cli.yml +++ b/wp-cli.yml @@ -1,4 +1,3 @@ --- path: wp - ... diff --git a/yarn.lock b/yarn.lock index 9379b48..2042ca7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -12,165 +12,170 @@ resolved "https://registry.yarnpkg.com/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz#010b6938fab7cb7df74aa2bbc06aa503b8fe5fb4" integrity sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q== -"@babel/parser@^7.28.4": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.28.5.tgz#0b0225ee90362f030efd644e8034c99468893b08" - integrity sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ== +"@babel/parser@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/parser/-/parser-7.29.0.tgz#669ef345add7d057e92b7ed15f0bac07611831b6" + integrity sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww== dependencies: - "@babel/types" "^7.28.5" + "@babel/types" "^7.29.0" -"@babel/types@^7.28.5": - version "7.28.5" - resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.28.5.tgz#10fc405f60897c35f07e85493c932c7b5ca0592b" - integrity sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA== +"@babel/types@^7.29.0": + version "7.29.0" + resolved "https://registry.yarnpkg.com/@babel/types/-/types-7.29.0.tgz#9f5b1e838c446e72cf3cd4b918152b8c605e37c7" + integrity sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A== dependencies: "@babel/helper-string-parser" "^7.27.1" "@babel/helper-validator-identifier" "^7.28.5" -"@docsearch/css@^4.0.0-beta.7": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.2.0.tgz#473bb4c51f4b2b037a71f423e569907ab19e6d72" - integrity sha512-65KU9Fw5fGsPPPlgIghonMcndyx1bszzrDQYLfierN+Ha29yotMHzVS94bPkZS6On9LS8dE4qmW4P/fGjtCf/g== - -"@docsearch/js@^4.0.0-beta.7": - version "4.2.0" - resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-4.2.0.tgz#66f08161102effaf60c0197be9bb74b81fbfb176" - integrity sha512-KBHVPO29QiGUFJYeAqxW0oXtGf/aghNmRrIRPT4/28JAefqoCkNn/ZM/jeQ7fHjl0KNM6C+KlLVYjwyz6lNZnA== - -"@esbuild/aix-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.11.tgz#2ae33300598132cc4cf580dbbb28d30fed3c5c49" - integrity sha512-Xt1dOL13m8u0WE8iplx9Ibbm+hFAO0GsU2P34UNoDGvZYkY8ifSiy6Zuc1lYxfG7svWE2fzqCUmFp5HCn51gJg== - -"@esbuild/android-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.25.11.tgz#927708b3db5d739d6cb7709136924cc81bec9b03" - integrity sha512-9slpyFBc4FPPz48+f6jyiXOx/Y4v34TUeDDXJpZqAWQn/08lKGeD8aDp9TMn9jDz2CiEuHwfhRmGBvpnd/PWIQ== - -"@esbuild/android-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.25.11.tgz#571f94e7f4068957ec4c2cfb907deae3d01b55ae" - integrity sha512-uoa7dU+Dt3HYsethkJ1k6Z9YdcHjTrSb5NUy66ZfZaSV8hEYGD5ZHbEMXnqLFlbBflLsl89Zke7CAdDJ4JI+Gg== - -"@esbuild/android-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.25.11.tgz#8a3bf5cae6c560c7ececa3150b2bde76e0fb81e6" - integrity sha512-Sgiab4xBjPU1QoPEIqS3Xx+R2lezu0LKIEcYe6pftr56PqPygbB7+szVnzoShbx64MUupqoE0KyRlN7gezbl8g== - -"@esbuild/darwin-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.25.11.tgz#0a678c4ac4bf8717e67481e1a797e6c152f93c84" - integrity sha512-VekY0PBCukppoQrycFxUqkCojnTQhdec0vevUL/EDOCnXd9LKWqD/bHwMPzigIJXPhC59Vd1WFIL57SKs2mg4w== - -"@esbuild/darwin-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.25.11.tgz#70f5e925a30c8309f1294d407a5e5e002e0315fe" - integrity sha512-+hfp3yfBalNEpTGp9loYgbknjR695HkqtY3d3/JjSRUyPg/xd6q+mQqIb5qdywnDxRZykIHs3axEqU6l1+oWEQ== - -"@esbuild/freebsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.11.tgz#4ec1db687c5b2b78b44148025da9632397553e8a" - integrity sha512-CmKjrnayyTJF2eVuO//uSjl/K3KsMIeYeyN7FyDBjsR3lnSJHaXlVoAK8DZa7lXWChbuOk7NjAc7ygAwrnPBhA== - -"@esbuild/freebsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.25.11.tgz#4c81abd1b142f1e9acfef8c5153d438ca53f44bb" - integrity sha512-Dyq+5oscTJvMaYPvW3x3FLpi2+gSZTCE/1ffdwuM6G1ARang/mb3jvjxs0mw6n3Lsw84ocfo9CrNMqc5lTfGOw== - -"@esbuild/linux-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.25.11.tgz#69517a111acfc2b93aa0fb5eaeb834c0202ccda5" - integrity sha512-Qr8AzcplUhGvdyUF08A1kHU3Vr2O88xxP0Tm8GcdVOUm25XYcMPp2YqSVHbLuXzYQMf9Bh/iKx7YPqECs6ffLA== - -"@esbuild/linux-arm@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.25.11.tgz#58dac26eae2dba0fac5405052b9002dac088d38f" - integrity sha512-TBMv6B4kCfrGJ8cUPo7vd6NECZH/8hPpBHHlYI3qzoYFvWu2AdTvZNuU/7hsbKWqu/COU7NIK12dHAAqBLLXgw== - -"@esbuild/linux-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.25.11.tgz#b89d4efe9bdad46ba944f0f3b8ddd40834268c2b" - integrity sha512-TmnJg8BMGPehs5JKrCLqyWTVAvielc615jbkOirATQvWWB1NMXY77oLMzsUjRLa0+ngecEmDGqt5jiDC6bfvOw== - -"@esbuild/linux-loong64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.25.11.tgz#11f603cb60ad14392c3f5c94d64b3cc8b630fbeb" - integrity sha512-DIGXL2+gvDaXlaq8xruNXUJdT5tF+SBbJQKbWy/0J7OhU8gOHOzKmGIlfTTl6nHaCOoipxQbuJi7O++ldrxgMw== - -"@esbuild/linux-mips64el@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.25.11.tgz#b7d447ff0676b8ab247d69dac40a5cf08e5eeaf5" - integrity sha512-Osx1nALUJu4pU43o9OyjSCXokFkFbyzjXb6VhGIJZQ5JZi8ylCQ9/LFagolPsHtgw6himDSyb5ETSfmp4rpiKQ== - -"@esbuild/linux-ppc64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.25.11.tgz#b3a28ed7cc252a61b07ff7c8fd8a984ffd3a2f74" - integrity sha512-nbLFgsQQEsBa8XSgSTSlrnBSrpoWh7ioFDUmwo158gIm5NNP+17IYmNWzaIzWmgCxq56vfr34xGkOcZ7jX6CPw== - -"@esbuild/linux-riscv64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.25.11.tgz#ce75b08f7d871a75edcf4d2125f50b21dc9dc273" - integrity sha512-HfyAmqZi9uBAbgKYP1yGuI7tSREXwIb438q0nqvlpxAOs3XnZ8RsisRfmVsgV486NdjD7Mw2UrFSw51lzUk1ww== - -"@esbuild/linux-s390x@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.25.11.tgz#cd08f6c73b6b6ff9ccdaabbd3ff6ad3dca99c263" - integrity sha512-HjLqVgSSYnVXRisyfmzsH6mXqyvj0SA7pG5g+9W7ESgwA70AXYNpfKBqh1KbTxmQVaYxpzA/SvlB9oclGPbApw== - -"@esbuild/linux-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.25.11.tgz#3c3718af31a95d8946ebd3c32bb1e699bdf74910" - integrity sha512-HSFAT4+WYjIhrHxKBwGmOOSpphjYkcswF449j6EjsjbinTZbp8PJtjsVK1XFJStdzXdy/jaddAep2FGY+wyFAQ== - -"@esbuild/netbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.11.tgz#b4c767082401e3a4e8595fe53c47cd7f097c8077" - integrity sha512-hr9Oxj1Fa4r04dNpWr3P8QKVVsjQhqrMSUzZzf+LZcYjZNqhA3IAfPQdEh1FLVUJSiu6sgAwp3OmwBfbFgG2Xg== - -"@esbuild/netbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.25.11.tgz#f2a930458ed2941d1f11ebc34b9c7d61f7a4d034" - integrity sha512-u7tKA+qbzBydyj0vgpu+5h5AeudxOAGncb8N6C9Kh1N4n7wU1Xw1JDApsRjpShRpXRQlJLb9wY28ELpwdPcZ7A== - -"@esbuild/openbsd-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.11.tgz#b4ae93c75aec48bc1e8a0154957a05f0641f2dad" - integrity sha512-Qq6YHhayieor3DxFOoYM1q0q1uMFYb7cSpLD2qzDSvK1NAvqFi8Xgivv0cFC6J+hWVw2teCYltyy9/m/14ryHg== - -"@esbuild/openbsd-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.25.11.tgz#b42863959c8dcf9b01581522e40012d2c70045e2" - integrity sha512-CN+7c++kkbrckTOz5hrehxWN7uIhFFlmS/hqziSFVWpAzpWrQoAG4chH+nN3Be+Kzv/uuo7zhX716x3Sn2Jduw== - -"@esbuild/openharmony-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.11.tgz#b2e717141c8fdf6bddd4010f0912e6b39e1640f1" - integrity sha512-rOREuNIQgaiR+9QuNkbkxubbp8MSO9rONmwP5nKncnWJ9v5jQ4JxFnLu4zDSRPf3x4u+2VN4pM4RdyIzDty/wQ== - -"@esbuild/sunos-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.25.11.tgz#9fbea1febe8778927804828883ec0f6dd80eb244" - integrity sha512-nq2xdYaWxyg9DcIyXkZhcYulC6pQ2FuCgem3LI92IwMgIZ69KHeY8T4Y88pcwoLIjbed8n36CyKoYRDygNSGhA== - -"@esbuild/win32-arm64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.25.11.tgz#501539cedb24468336073383989a7323005a8935" - integrity sha512-3XxECOWJq1qMZ3MN8srCJ/QfoLpL+VaxD/WfNRm1O3B4+AZ/BnLVgFbUV3eiRYDMXetciH16dwPbbHqwe1uU0Q== - -"@esbuild/win32-ia32@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.25.11.tgz#8ac7229aa82cef8f16ffb58f1176a973a7a15343" - integrity sha512-3ukss6gb9XZ8TlRyJlgLn17ecsK4NSQTmdIXRASVsiS2sQ6zPPZklNJT5GR5tE/MUarymmy8kCEf5xPCNCqVOA== - -"@esbuild/win32-x64@0.25.11": - version "0.25.11" - resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.25.11.tgz#5ecda6f3fe138b7e456f4e429edde33c823f392f" - integrity sha512-D7Hpz6A2L4hzsRpPaCYkQnGOotdUpDzSGRIv9I+1ITdHROSFUWW95ZPZWQmGka1Fg7W3zFJowyn9WGwMJ0+KPA== - -"@iconify-json/simple-icons@^1.2.47": - version "1.2.56" - resolved "https://registry.yarnpkg.com/@iconify-json/simple-icons/-/simple-icons-1.2.56.tgz#f225b1dc10a6f9bbd80b55342bf34f13633f3824" - integrity sha512-oAvxOzgSjfvdj/Jsi3S7HDUCxO8/n2j8e1w1e/FktHUAXiWjNX00n3Tu3AP+n1ayKrypcUDXCzxn+0ENMl6ouw== +"@docsearch/css@^4.5.3": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@docsearch/css/-/css-4.6.0.tgz#1780de042f61d11d60091f5c2f734543c3bc5d07" + integrity sha512-YlcAimkXclvqta47g47efzCM5CFxDwv2ClkDfEs/fC/Ak0OxPH2b3czwa4o8O1TRBf+ujFF2RiUwszz2fPVNJQ== + +"@docsearch/js@^4.5.3": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@docsearch/js/-/js-4.6.0.tgz#db5020cfa6fe8b1f89886473d3b228777b438e7c" + integrity sha512-9/rbgkm/BgTq46cwxIohvSAz3koOFjnPpg0mwkJItAfzKbQIj+310PvwtgUY1YITDuGCag6yOL50GW2DBkaaBw== + +"@docsearch/sidepanel-js@^4.5.3": + version "4.6.0" + resolved "https://registry.yarnpkg.com/@docsearch/sidepanel-js/-/sidepanel-js-4.6.0.tgz#1cf29058172eb42286eebdc77fd280d6189eb017" + integrity sha512-lFT5KLwlzUmpoGArCScNoK41l9a22JYsEPwBzMrz+/ILVR5Ax87UphCuiyDFQWEvEmbwzn/kJx5W/O5BUlN1Rw== + +"@esbuild/aix-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/aix-ppc64/-/aix-ppc64-0.27.3.tgz#815b39267f9bffd3407ea6c376ac32946e24f8d2" + integrity sha512-9fJMTNFTWZMh5qwrBItuziu834eOCUcEqymSH7pY+zoMVEZg3gcPuBNxH1EvfVYe9h0x/Ptw8KBzv7qxb7l8dg== + +"@esbuild/android-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.27.3.tgz#19b882408829ad8e12b10aff2840711b2da361e8" + integrity sha512-YdghPYUmj/FX2SYKJ0OZxf+iaKgMsKHVPF1MAq/P8WirnSpCStzKJFjOjzsW0QQ7oIAiccHdcqjbHmJxRb/dmg== + +"@esbuild/android-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.27.3.tgz#90be58de27915efa27b767fcbdb37a4470627d7b" + integrity sha512-i5D1hPY7GIQmXlXhs2w8AWHhenb00+GxjxRncS2ZM7YNVGNfaMxgzSGuO8o8SJzRc/oZwU2bcScvVERk03QhzA== + +"@esbuild/android-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.27.3.tgz#d7dcc976f16e01a9aaa2f9b938fbec7389f895ac" + integrity sha512-IN/0BNTkHtk8lkOM8JWAYFg4ORxBkZQf9zXiEOfERX/CzxW3Vg1ewAhU7QSWQpVIzTW+b8Xy+lGzdYXV6UZObQ== + +"@esbuild/darwin-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.27.3.tgz#9f6cac72b3a8532298a6a4493ed639a8988e8abd" + integrity sha512-Re491k7ByTVRy0t3EKWajdLIr0gz2kKKfzafkth4Q8A5n1xTHrkqZgLLjFEHVD+AXdUGgQMq+Godfq45mGpCKg== + +"@esbuild/darwin-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.27.3.tgz#ac61d645faa37fd650340f1866b0812e1fb14d6a" + integrity sha512-vHk/hA7/1AckjGzRqi6wbo+jaShzRowYip6rt6q7VYEDX4LEy1pZfDpdxCBnGtl+A5zq8iXDcyuxwtv3hNtHFg== + +"@esbuild/freebsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.3.tgz#b8625689d73cf1830fe58c39051acdc12474ea1b" + integrity sha512-ipTYM2fjt3kQAYOvo6vcxJx3nBYAzPjgTCk7QEgZG8AUO3ydUhvelmhrbOheMnGOlaSFUoHXB6un+A7q4ygY9w== + +"@esbuild/freebsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.27.3.tgz#07be7dd3c9d42fe0eccd2ab9f9ded780bc53bead" + integrity sha512-dDk0X87T7mI6U3K9VjWtHOXqwAMJBNN2r7bejDsc+j03SEjtD9HrOl8gVFByeM0aJksoUuUVU9TBaZa2rgj0oA== + +"@esbuild/linux-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.27.3.tgz#bf31918fe5c798586460d2b3d6c46ed2c01ca0b6" + integrity sha512-sZOuFz/xWnZ4KH3YfFrKCf1WyPZHakVzTiqji3WDc0BCl2kBwiJLCXpzLzUBLgmp4veFZdvN5ChW4Eq/8Fc2Fg== + +"@esbuild/linux-arm@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.27.3.tgz#28493ee46abec1dc3f500223cd9f8d2df08f9d11" + integrity sha512-s6nPv2QkSupJwLYyfS+gwdirm0ukyTFNl3KTgZEAiJDd+iHZcbTPPcWCcRYH+WlNbwChgH2QkE9NSlNrMT8Gfw== + +"@esbuild/linux-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.27.3.tgz#750752a8b30b43647402561eea764d0a41d0ee29" + integrity sha512-yGlQYjdxtLdh0a3jHjuwOrxQjOZYD/C9PfdbgJJF3TIZWnm/tMd/RcNiLngiu4iwcBAOezdnSLAwQDPqTmtTYg== + +"@esbuild/linux-loong64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.27.3.tgz#a5a92813a04e71198c50f05adfaf18fc1e95b9ed" + integrity sha512-WO60Sn8ly3gtzhyjATDgieJNet/KqsDlX5nRC5Y3oTFcS1l0KWba+SEa9Ja1GfDqSF1z6hif/SkpQJbL63cgOA== + +"@esbuild/linux-mips64el@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.27.3.tgz#deb45d7fd2d2161eadf1fbc593637ed766d50bb1" + integrity sha512-APsymYA6sGcZ4pD6k+UxbDjOFSvPWyZhjaiPyl/f79xKxwTnrn5QUnXR5prvetuaSMsb4jgeHewIDCIWljrSxw== + +"@esbuild/linux-ppc64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.27.3.tgz#6f39ae0b8c4d3d2d61a65b26df79f6e12a1c3d78" + integrity sha512-eizBnTeBefojtDb9nSh4vvVQ3V9Qf9Df01PfawPcRzJH4gFSgrObw+LveUyDoKU3kxi5+9RJTCWlj4FjYXVPEA== + +"@esbuild/linux-riscv64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.27.3.tgz#4c5c19c3916612ec8e3915187030b9df0b955c1d" + integrity sha512-3Emwh0r5wmfm3ssTWRQSyVhbOHvqegUDRd0WhmXKX2mkHJe1SFCMJhagUleMq+Uci34wLSipf8Lagt4LlpRFWQ== + +"@esbuild/linux-s390x@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.27.3.tgz#9ed17b3198fa08ad5ccaa9e74f6c0aff7ad0156d" + integrity sha512-pBHUx9LzXWBc7MFIEEL0yD/ZVtNgLytvx60gES28GcWMqil8ElCYR4kvbV2BDqsHOvVDRrOxGySBM9Fcv744hw== + +"@esbuild/linux-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.27.3.tgz#12383dcbf71b7cf6513e58b4b08d95a710bf52a5" + integrity sha512-Czi8yzXUWIQYAtL/2y6vogER8pvcsOsk5cpwL4Gk5nJqH5UZiVByIY8Eorm5R13gq+DQKYg0+JyQoytLQas4dA== + +"@esbuild/netbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.3.tgz#dd0cb2fa543205fcd931df44f4786bfcce6df7d7" + integrity sha512-sDpk0RgmTCR/5HguIZa9n9u+HVKf40fbEUt+iTzSnCaGvY9kFP0YKBWZtJaraonFnqef5SlJ8/TiPAxzyS+UoA== + +"@esbuild/netbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.27.3.tgz#028ad1807a8e03e155153b2d025b506c3787354b" + integrity sha512-P14lFKJl/DdaE00LItAukUdZO5iqNH7+PjoBm+fLQjtxfcfFE20Xf5CrLsmZdq5LFFZzb5JMZ9grUwvtVYzjiA== + +"@esbuild/openbsd-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.3.tgz#e3c16ff3490c9b59b969fffca87f350ffc0e2af5" + integrity sha512-AIcMP77AvirGbRl/UZFTq5hjXK+2wC7qFRGoHSDrZ5v5b8DK/GYpXW3CPRL53NkvDqb9D+alBiC/dV0Fb7eJcw== + +"@esbuild/openbsd-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.27.3.tgz#c5a4693fcb03d1cbecbf8b422422468dfc0d2a8b" + integrity sha512-DnW2sRrBzA+YnE70LKqnM3P+z8vehfJWHXECbwBmH/CU51z6FiqTQTHFenPlHmo3a8UgpLyH3PT+87OViOh1AQ== + +"@esbuild/openharmony-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.3.tgz#082082444f12db564a0775a41e1991c0e125055e" + integrity sha512-NinAEgr/etERPTsZJ7aEZQvvg/A6IsZG/LgZy+81wON2huV7SrK3e63dU0XhyZP4RKGyTm7aOgmQk0bGp0fy2g== + +"@esbuild/sunos-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.27.3.tgz#5ab036c53f929e8405c4e96e865a424160a1b537" + integrity sha512-PanZ+nEz+eWoBJ8/f8HKxTTD172SKwdXebZ0ndd953gt1HRBbhMsaNqjTyYLGLPdoWHy4zLU7bDVJztF5f3BHA== + +"@esbuild/win32-arm64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.27.3.tgz#38de700ef4b960a0045370c171794526e589862e" + integrity sha512-B2t59lWWYrbRDw/tjiWOuzSsFh1Y/E95ofKz7rIVYSQkUYBjfSgf6oeYPNWHToFRr2zx52JKApIcAS/D5TUBnA== + +"@esbuild/win32-ia32@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.27.3.tgz#451b93dc03ec5d4f38619e6cd64d9f9eff06f55c" + integrity sha512-QLKSFeXNS8+tHW7tZpMtjlNb7HKau0QDpwm49u0vUp9y1WOF+PEzkU84y9GqYaAVW8aH8f3GcBck26jh54cX4Q== + +"@esbuild/win32-x64@0.27.3": + version "0.27.3" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.27.3.tgz#0eaf705c941a218a43dba8e09f1df1d6cd2f1f17" + integrity sha512-4uJGhsxuptu3OcpVAzli+/gWusVGwZZHTlS63hh++ehExkVT8SgiEf7/uC/PclrPPkLhZqGgCTjd0VWLo6xMqA== + +"@iconify-json/simple-icons@^1.2.68": + version "1.2.72" + resolved "https://registry.yarnpkg.com/@iconify-json/simple-icons/-/simple-icons-1.2.72.tgz#9c2b310eaefe285ab0a1ca03b9a2936c48dc81fd" + integrity sha512-wkcixntHvaCoqPqerGrNFcHQ3Yx1ux4ZkhscCDK0DEHpP62XCH+cxq1HTsRjbUiQl/M9K8bj03HF6Wgn5iE2rQ== dependencies: "@iconify/types" "*" @@ -184,174 +189,189 @@ resolved "https://registry.yarnpkg.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz#6912b00d2c631c0d15ce1a7ab57cd657f2a8f8ba" integrity sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og== -"@rolldown/pluginutils@1.0.0-beta.29": - version "1.0.0-beta.29" - resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.29.tgz#f8fc9a8788757dccba0d3b7fee93183621773d4c" - integrity sha512-NIJgOsMjbxAXvoGq/X0gD7VPMQ8j9g0BiDaNjVNVjvl+iKXxL3Jre0v31RmBYeLEmkbj2s02v8vFTbUXi5XS2Q== - -"@rollup/rollup-android-arm-eabi@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.52.5.tgz#0f44a2f8668ed87b040b6fe659358ac9239da4db" - integrity sha512-8c1vW4ocv3UOMp9K+gToY5zL2XiiVw3k7f1ksf4yO1FlDFQ1C2u72iACFnSOceJFsWskc2WZNqeRhFRPzv+wtQ== - -"@rollup/rollup-android-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.52.5.tgz#25b9a01deef6518a948431564c987bcb205274f5" - integrity sha512-mQGfsIEFcu21mvqkEKKu2dYmtuSZOBMmAl5CFlPGLY94Vlcm+zWApK7F/eocsNzp8tKmbeBP8yXyAbx0XHsFNA== - -"@rollup/rollup-darwin-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.52.5.tgz#8a102869c88f3780c7d5e6776afd3f19084ecd7f" - integrity sha512-takF3CR71mCAGA+v794QUZ0b6ZSrgJkArC+gUiG6LB6TQty9T0Mqh3m2ImRBOxS2IeYBo4lKWIieSvnEk2OQWA== - -"@rollup/rollup-darwin-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.52.5.tgz#8e526417cd6f54daf1d0c04cf361160216581956" - integrity sha512-W901Pla8Ya95WpxDn//VF9K9u2JbocwV/v75TE0YIHNTbhqUTv9w4VuQ9MaWlNOkkEfFwkdNhXgcLqPSmHy0fA== - -"@rollup/rollup-freebsd-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.52.5.tgz#0e7027054493f3409b1f219a3eac5efd128ef899" - integrity sha512-QofO7i7JycsYOWxe0GFqhLmF6l1TqBswJMvICnRUjqCx8b47MTo46W8AoeQwiokAx3zVryVnxtBMcGcnX12LvA== - -"@rollup/rollup-freebsd-x64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.52.5.tgz#72b204a920139e9ec3d331bd9cfd9a0c248ccb10" - integrity sha512-jr21b/99ew8ujZubPo9skbrItHEIE50WdV86cdSoRkKtmWa+DDr6fu2c/xyRT0F/WazZpam6kk7IHBerSL7LDQ== - -"@rollup/rollup-linux-arm-gnueabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.52.5.tgz#ab1b522ebe5b7e06c99504cc38f6cd8b808ba41c" - integrity sha512-PsNAbcyv9CcecAUagQefwX8fQn9LQ4nZkpDboBOttmyffnInRy8R8dSg6hxxl2Re5QhHBf6FYIDhIj5v982ATQ== - -"@rollup/rollup-linux-arm-musleabihf@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.52.5.tgz#f8cc30b638f1ee7e3d18eac24af47ea29d9beb00" - integrity sha512-Fw4tysRutyQc/wwkmcyoqFtJhh0u31K+Q6jYjeicsGJJ7bbEq8LwPWV/w0cnzOqR2m694/Af6hpFayLJZkG2VQ== - -"@rollup/rollup-linux-arm64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.52.5.tgz#7af37a9e85f25db59dc8214172907b7e146c12cc" - integrity sha512-a+3wVnAYdQClOTlyapKmyI6BLPAFYs0JM8HRpgYZQO02rMR09ZcV9LbQB+NL6sljzG38869YqThrRnfPMCDtZg== - -"@rollup/rollup-linux-arm64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.52.5.tgz#a623eb0d3617c03b7a73716eb85c6e37b776f7e0" - integrity sha512-AvttBOMwO9Pcuuf7m9PkC1PUIKsfaAJ4AYhy944qeTJgQOqJYJ9oVl2nYgY7Rk0mkbsuOpCAYSs6wLYB2Xiw0Q== - -"@rollup/rollup-linux-loong64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.52.5.tgz#76ea038b549c5c6c5f0d062942627c4066642ee2" - integrity sha512-DkDk8pmXQV2wVrF6oq5tONK6UHLz/XcEVow4JTTerdeV1uqPeHxwcg7aFsfnSm9L+OO8WJsWotKM2JJPMWrQtA== - -"@rollup/rollup-linux-ppc64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.52.5.tgz#d9a4c3f0a3492bc78f6fdfe8131ac61c7359ccd5" - integrity sha512-W/b9ZN/U9+hPQVvlGwjzi+Wy4xdoH2I8EjaCkMvzpI7wJUs8sWJ03Rq96jRnHkSrcHTpQe8h5Tg3ZzUPGauvAw== - -"@rollup/rollup-linux-riscv64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.52.5.tgz#87ab033eebd1a9a1dd7b60509f6333ec1f82d994" - integrity sha512-sjQLr9BW7R/ZiXnQiWPkErNfLMkkWIoCz7YMn27HldKsADEKa5WYdobaa1hmN6slu9oWQbB6/jFpJ+P2IkVrmw== - -"@rollup/rollup-linux-riscv64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.52.5.tgz#bda3eb67e1c993c1ba12bc9c2f694e7703958d9f" - integrity sha512-hq3jU/kGyjXWTvAh2awn8oHroCbrPm8JqM7RUpKjalIRWWXE01CQOf/tUNWNHjmbMHg/hmNCwc/Pz3k1T/j/Lg== - -"@rollup/rollup-linux-s390x-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.52.5.tgz#f7bc10fbe096ab44694233dc42a2291ed5453d4b" - integrity sha512-gn8kHOrku8D4NGHMK1Y7NA7INQTRdVOntt1OCYypZPRt6skGbddska44K8iocdpxHTMMNui5oH4elPH4QOLrFQ== - -"@rollup/rollup-linux-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.52.5.tgz#a151cb1234cc9b2cf5e8cfc02aa91436b8f9e278" - integrity sha512-hXGLYpdhiNElzN770+H2nlx+jRog8TyynpTVzdlc6bndktjKWyZyiCsuDAlpd+j+W+WNqfcyAWz9HxxIGfZm1Q== - -"@rollup/rollup-linux-x64-musl@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.52.5.tgz#7859e196501cc3b3062d45d2776cfb4d2f3a9350" - integrity sha512-arCGIcuNKjBoKAXD+y7XomR9gY6Mw7HnFBv5Rw7wQRvwYLR7gBAgV7Mb2QTyjXfTveBNFAtPt46/36vV9STLNg== - -"@rollup/rollup-openharmony-arm64@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.52.5.tgz#85d0df7233734df31e547c1e647d2a5300b3bf30" - integrity sha512-QoFqB6+/9Rly/RiPjaomPLmR/13cgkIGfA40LHly9zcH1S0bN2HVFYk3a1eAyHQyjs3ZJYlXvIGtcCs5tko9Cw== - -"@rollup/rollup-win32-arm64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.52.5.tgz#e62357d00458db17277b88adbf690bb855cac937" - integrity sha512-w0cDWVR6MlTstla1cIfOGyl8+qb93FlAVutcor14Gf5Md5ap5ySfQ7R9S/NjNaMLSFdUnKGEasmVnu3lCMqB7w== - -"@rollup/rollup-win32-ia32-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.52.5.tgz#fc7cd40f44834a703c1f1c3fe8bcc27ce476cd50" - integrity sha512-Aufdpzp7DpOTULJCuvzqcItSGDH73pF3ko/f+ckJhxQyHtp67rHw3HMNxoIdDMUITJESNE6a8uh4Lo4SLouOUg== - -"@rollup/rollup-win32-x64-gnu@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.52.5.tgz#1a22acfc93c64a64a48c42672e857ee51774d0d3" - integrity sha512-UGBUGPFp1vkj6p8wCRraqNhqwX/4kNQPS57BCFc8wYh0g94iVIW33wJtQAx3G7vrjjNtRaxiMUylM0ktp/TRSQ== - -"@rollup/rollup-win32-x64-msvc@4.52.5": - version "4.52.5" - resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.52.5.tgz#1657f56326bbe0ac80eedc9f9c18fc1ddd24e107" - integrity sha512-TAcgQh2sSkykPRWLrdyy2AiceMckNf5loITqXxFI5VuQjS5tSuw3WlwdN8qv8vzjLAUTvYaH/mVjSFpbkFbpTg== - -"@shikijs/core@3.14.0", "@shikijs/core@^3.9.2": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.14.0.tgz#a1c547794518842fb60bb71932a7bc47de0b4703" - integrity sha512-qRSeuP5vlYHCNUIrpEBQFO7vSkR7jn7Kv+5X3FO/zBKVDGQbcnlScD3XhkrHi/R8Ltz0kEjvFR9Szp/XMRbFMw== +"@rolldown/pluginutils@1.0.0-rc.2": + version "1.0.0-rc.2" + resolved "https://registry.yarnpkg.com/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.2.tgz#10324e74cb3396cb7b616042ea7e9e6aa7d8d458" + integrity sha512-izyXV/v+cHiRfozX62W9htOAvwMo4/bXKDrQ+vom1L1qRuexPock/7VZDAhnpHCLNejd3NJ6hiab+tO0D44Rgw== + +"@rollup/rollup-android-arm-eabi@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz#a6742c74c7d9d6d604ef8a48f99326b4ecda3d82" + integrity sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg== + +"@rollup/rollup-android-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz#97247be098de4df0c11971089fd2edf80a5da8cf" + integrity sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q== + +"@rollup/rollup-darwin-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz#674852cf14cf11b8056e0b1a2f4e872b523576cf" + integrity sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg== + +"@rollup/rollup-darwin-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz#36dfd7ed0aaf4d9d89d9ef983af72632455b0246" + integrity sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w== + +"@rollup/rollup-freebsd-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz#2f87c2074b4220260fdb52a9996246edfc633c22" + integrity sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA== + +"@rollup/rollup-freebsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz#9b5a26522a38a95dc06616d1939d4d9a76937803" + integrity sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg== + +"@rollup/rollup-linux-arm-gnueabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz#86aa4859385a8734235b5e40a48e52d770758c3a" + integrity sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw== + +"@rollup/rollup-linux-arm-musleabihf@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz#cbe70e56e6ece8dac83eb773b624fc9e5a460976" + integrity sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA== + +"@rollup/rollup-linux-arm64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz#d14992a2e653bc3263d284bc6579b7a2890e1c45" + integrity sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA== + +"@rollup/rollup-linux-arm64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz#2fdd1ddc434ea90aeaa0851d2044789b4d07f6da" + integrity sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA== + +"@rollup/rollup-linux-loong64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz#8a181e6f89f969f21666a743cd411416c80099e7" + integrity sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg== + +"@rollup/rollup-linux-loong64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz#904125af2babc395f8061daa27b5af1f4e3f2f78" + integrity sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q== + +"@rollup/rollup-linux-ppc64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz#a57970ac6864c9a3447411a658224bdcf948be22" + integrity sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA== + +"@rollup/rollup-linux-ppc64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz#bb84de5b26870567a4267666e08891e80bb56a63" + integrity sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA== + +"@rollup/rollup-linux-riscv64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz#72d00d2c7fb375ce3564e759db33f17a35bffab9" + integrity sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg== + +"@rollup/rollup-linux-riscv64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz#4c166ef58e718f9245bd31873384ba15a5c1a883" + integrity sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg== + +"@rollup/rollup-linux-s390x-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz#bb5025cde9a61db478c2ca7215808ad3bce73a09" + integrity sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w== + +"@rollup/rollup-linux-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz#9b66b1f9cd95c6624c788f021c756269ffed1552" + integrity sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg== + +"@rollup/rollup-linux-x64-musl@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz#b007ca255dc7166017d57d7d2451963f0bd23fd9" + integrity sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg== + +"@rollup/rollup-openbsd-x64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz#e8b357b2d1aa2c8d76a98f5f0d889eabe93f4ef9" + integrity sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ== + +"@rollup/rollup-openharmony-arm64@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz#96c2e3f4aacd3d921981329831ff8dde492204dc" + integrity sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA== + +"@rollup/rollup-win32-arm64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz#2d865149d706d938df8b4b8f117e69a77646d581" + integrity sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A== + +"@rollup/rollup-win32-ia32-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz#abe1593be0fa92325e9971c8da429c5e05b92c36" + integrity sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA== + +"@rollup/rollup-win32-x64-gnu@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz#c4af3e9518c9a5cd4b1c163dc81d0ad4d82e7eab" + integrity sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA== + +"@rollup/rollup-win32-x64-msvc@4.59.0": + version "4.59.0" + resolved "https://registry.yarnpkg.com/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz#4584a8a87b29188a4c1fe987a9fcf701e256d86c" + integrity sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA== + +"@shikijs/core@3.23.0", "@shikijs/core@^3.21.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/core/-/core-3.23.0.tgz#79248ec4ad3de4fd5c12993f5c30cb071ec04812" + integrity sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA== dependencies: - "@shikijs/types" "3.14.0" + "@shikijs/types" "3.23.0" "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" hast-util-to-html "^9.0.5" -"@shikijs/engine-javascript@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.14.0.tgz#546c14fcf2654ca3c27f76686e969bf3b1496b53" - integrity sha512-3v1kAXI2TsWQuwv86cREH/+FK9Pjw3dorVEykzQDhwrZj0lwsHYlfyARaKmn6vr5Gasf8aeVpb8JkzeWspxOLQ== +"@shikijs/engine-javascript@3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz#eae89a47913f486e5a05130d13b965c424c33b21" + integrity sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA== dependencies: - "@shikijs/types" "3.14.0" + "@shikijs/types" "3.23.0" "@shikijs/vscode-textmate" "^10.0.2" - oniguruma-to-es "^4.3.3" + oniguruma-to-es "^4.3.4" -"@shikijs/engine-oniguruma@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.14.0.tgz#562bcce2f69cc65c92bcf2ccb637b2a7021f3d7b" - integrity sha512-TNcYTYMbJyy+ZjzWtt0bG5y4YyMIWC2nyePz+CFMWqm+HnZZyy9SWMgo8Z6KBJVIZnx8XUXS8U2afO6Y0g1Oug== +"@shikijs/engine-oniguruma@3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz#789421048d66ac1b33613169d6d18b9cc6e340ed" + integrity sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g== dependencies: - "@shikijs/types" "3.14.0" + "@shikijs/types" "3.23.0" "@shikijs/vscode-textmate" "^10.0.2" -"@shikijs/langs@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.14.0.tgz#71e6ca44e661b405209eb63d4449b57b9de529d0" - integrity sha512-DIB2EQY7yPX1/ZH7lMcwrK5pl+ZkP/xoSpUzg9YC8R+evRCCiSQ7yyrvEyBsMnfZq4eBzLzBlugMyTAf13+pzg== +"@shikijs/langs@3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/langs/-/langs-3.23.0.tgz#00959d8b16c7f671221ae79b3ad8cde7e6a5c112" + integrity sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg== dependencies: - "@shikijs/types" "3.14.0" + "@shikijs/types" "3.23.0" -"@shikijs/themes@3.14.0": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.14.0.tgz#2b516c19caf63f78f81f5df9c087800c3b2c7404" - integrity sha512-fAo/OnfWckNmv4uBoUu6dSlkcBc+SA1xzj5oUSaz5z3KqHtEbUypg/9xxgJARtM6+7RVm0Q6Xnty41xA1ma1IA== +"@shikijs/themes@3.23.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/themes/-/themes-3.23.0.tgz#fd96ca5ad52639057995bc2093682884e1846f27" + integrity sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA== dependencies: - "@shikijs/types" "3.14.0" + "@shikijs/types" "3.23.0" -"@shikijs/transformers@^3.9.2": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-3.14.0.tgz#441b87394463faa89a86b1cfa5de2f12e9f6dbd3" - integrity sha512-i67zQnY9wLMMnKasonVW1L9fKneSLZDj1ePsA4o0AZWU4uUobmJY9baRDa36z+a9/g0aG76/2tybQvm4hrwxIQ== +"@shikijs/transformers@^3.21.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/transformers/-/transformers-3.23.0.tgz#a4f1a593e4f91fa3034f7ec02094deefdadab98b" + integrity sha512-F9msZVxdF+krQNSdQ4V+Ja5QemeAoTQ2jxt7nJCwhDsdF1JWS3KxIQXA3lQbyKwS3J61oHRUSv4jYWv3CkaKTQ== dependencies: - "@shikijs/core" "3.14.0" - "@shikijs/types" "3.14.0" + "@shikijs/core" "3.23.0" + "@shikijs/types" "3.23.0" -"@shikijs/types@3.14.0", "@shikijs/types@^3.9.2": - version "3.14.0" - resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.14.0.tgz#4e666f8d31e319494daf23efcc19a32a5fdaa341" - integrity sha512-bQGgC6vrY8U/9ObG1Z/vTro+uclbjjD/uG58RvfxKZVD5p9Yc1ka3tVyEFy7BNJLzxuWyHH5NWynP9zZZS59eQ== +"@shikijs/types@3.23.0", "@shikijs/types@^3.21.0": + version "3.23.0" + resolved "https://registry.yarnpkg.com/@shikijs/types/-/types-3.23.0.tgz#d441571a058641926018ae3de99866f39e5bbdf2" + integrity sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ== dependencies: "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" @@ -413,151 +433,146 @@ resolved "https://registry.yarnpkg.com/@ungap/structured-clone/-/structured-clone-1.3.0.tgz#d06bbb384ebcf6c505fde1c3d0ed4ddffe0aaff8" integrity sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g== -"@vitejs/plugin-vue@^6.0.1": - version "6.0.1" - resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.1.tgz#4c7f559621af104a22255c6ace5626e6d8349689" - integrity sha512-+MaE752hU0wfPFJEUAIxqw18+20euHHdxVtMvbFcOEpjEyfqXH/5DCoTHiVJ0J29EhTJdoTkjEv5YBKU9dnoTw== +"@vitejs/plugin-vue@^6.0.3": + version "6.0.4" + resolved "https://registry.yarnpkg.com/@vitejs/plugin-vue/-/plugin-vue-6.0.4.tgz#f571fe5aeb0be511e3bfdd43844d8eaa0738b28e" + integrity sha512-uM5iXipgYIn13UUQCZNdWkYk+sysBeA97d5mHsAoAt1u/wpN3+zxOmsVJWosuzX+IMGRzeYUNytztrYznboIkQ== dependencies: - "@rolldown/pluginutils" "1.0.0-beta.29" + "@rolldown/pluginutils" "1.0.0-rc.2" -"@vue/compiler-core@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.22.tgz#bb8294a0dd31df540563cc6ffa0456f1f7687b97" - integrity sha512-jQ0pFPmZwTEiRNSb+i9Ow/I/cHv2tXYqsnHKKyCQ08irI2kdF5qmYedmF8si8mA7zepUFmJ2hqzS8CQmNOWOkQ== +"@vue/compiler-core@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.5.29.tgz#3fb70630c62a2e715eeddc3c2a48f46aa4507adc" + integrity sha512-cuzPhD8fwRHk8IGfmYaR4eEe4cAyJEL66Ove/WZL7yWNL134nqLddSLwNRIsFlnnW1kK+p8Ck3viFnC0chXCXw== dependencies: - "@babel/parser" "^7.28.4" - "@vue/shared" "3.5.22" - entities "^4.5.0" + "@babel/parser" "^7.29.0" + "@vue/shared" "3.5.29" + entities "^7.0.1" estree-walker "^2.0.2" source-map-js "^1.2.1" -"@vue/compiler-dom@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.22.tgz#6c9c2c9843520f6d3dbc685e5d0e1e12a2c04c56" - integrity sha512-W8RknzUM1BLkypvdz10OVsGxnMAuSIZs9Wdx1vzA3mL5fNMN15rhrSCLiTm6blWeACwUwizzPVqGJgOGBEN/hA== +"@vue/compiler-dom@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.5.29.tgz#055cf5492005249591c7fb45868a874468e4ffb3" + integrity sha512-n0G5o7R3uBVmVxjTIYcz7ovr8sy7QObFG8OQJ3xGCDNhbG60biP/P5KnyY8NLd81OuT1WJflG7N4KWYHaeeaIg== dependencies: - "@vue/compiler-core" "3.5.22" - "@vue/shared" "3.5.22" + "@vue/compiler-core" "3.5.29" + "@vue/shared" "3.5.29" -"@vue/compiler-sfc@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.22.tgz#663a8483b1dda8de83b6fa1aab38a52bf73dd965" - integrity sha512-tbTR1zKGce4Lj+JLzFXDq36K4vcSZbJ1RBu8FxcDv1IGRz//Dh2EBqksyGVypz3kXpshIfWKGOCcqpSbyGWRJQ== +"@vue/compiler-sfc@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.5.29.tgz#8b1b0707fb53c122fedd566244a564eaf40ee71f" + integrity sha512-oJZhN5XJs35Gzr50E82jg2cYdZQ78wEwvRO6Y63TvLVTc+6xICzJHP1UIecdSPPYIbkautNBanDiWYa64QSFIA== dependencies: - "@babel/parser" "^7.28.4" - "@vue/compiler-core" "3.5.22" - "@vue/compiler-dom" "3.5.22" - "@vue/compiler-ssr" "3.5.22" - "@vue/shared" "3.5.22" + "@babel/parser" "^7.29.0" + "@vue/compiler-core" "3.5.29" + "@vue/compiler-dom" "3.5.29" + "@vue/compiler-ssr" "3.5.29" + "@vue/shared" "3.5.29" estree-walker "^2.0.2" - magic-string "^0.30.19" + magic-string "^0.30.21" postcss "^8.5.6" source-map-js "^1.2.1" -"@vue/compiler-ssr@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.22.tgz#a0ef16e364731b25e79a13470569066af101320f" - integrity sha512-GdgyLvg4R+7T8Nk2Mlighx7XGxq/fJf9jaVofc3IL0EPesTE86cP/8DD1lT3h1JeZr2ySBvyqKQJgbS54IX1Ww== +"@vue/compiler-ssr@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.5.29.tgz#83ac28c860271bd1c9822d135ac78c388403f949" + integrity sha512-Y/ARJZE6fpjzL5GH/phJmsFwx3g6t2KmHKHx5q+MLl2kencADKIrhH5MLF6HHpRMmlRAYBRSvv347Mepf1zVNw== dependencies: - "@vue/compiler-dom" "3.5.22" - "@vue/shared" "3.5.22" + "@vue/compiler-dom" "3.5.29" + "@vue/shared" "3.5.29" -"@vue/devtools-api@^8.0.0": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-8.0.3.tgz#18e5e8fe57dcb4b83ebfed714bb40da7d7ba55cb" - integrity sha512-YxZE7xNvvfq5XmjJh1ml+CzVNrRjuZYCuT5Xjj0u9RlXU7za/MRuZDUXcKfp0j7IvYkDut49vlKqbiQ1xhXP2w== +"@vue/devtools-api@^8.0.5": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@vue/devtools-api/-/devtools-api-8.0.7.tgz#1b3ea34572eaeadc9a6d3302604efcb15d77302d" + integrity sha512-tc1TXAxclsn55JblLkFVcIRG7MeSJC4fWsPjfM7qu/IcmPUYnQ5Q8vzWwBpyDY24ZjmZTUCCwjRSNbx58IhlAA== dependencies: - "@vue/devtools-kit" "^8.0.3" + "@vue/devtools-kit" "^8.0.7" -"@vue/devtools-kit@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-8.0.3.tgz#c07b36ff27734c366e33eb0f4ca82304ce2d566a" - integrity sha512-UF4YUOVGdfzXLCv5pMg2DxocB8dvXz278fpgEE+nJ/DRALQGAva7sj9ton0VWZ9hmXw+SV8yKMrxP2MpMhq9Wg== +"@vue/devtools-kit@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@vue/devtools-kit/-/devtools-kit-8.0.7.tgz#bb8d4b838f10a25dec8eb728b4a3bd560bf05e55" + integrity sha512-H6esJGHGl5q0E9iV3m2EoBQHJ+V83WMW83A0/+Fn95eZ2iIvdsq4+UCS6yT/Fdd4cGZSchx/MdWDreM3WqMsDw== dependencies: - "@vue/devtools-shared" "^8.0.3" + "@vue/devtools-shared" "^8.0.7" birpc "^2.6.1" hookable "^5.5.3" - mitt "^3.0.1" perfect-debounce "^2.0.0" - speakingurl "^14.0.1" - superjson "^2.2.2" -"@vue/devtools-shared@^8.0.3": - version "8.0.3" - resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-8.0.3.tgz#0ce5d353de5d7f29d22b0683e82d0b09535adbfe" - integrity sha512-s/QNll7TlpbADFZrPVsaUNPCOF8NvQgtgmmB7Tip6pLf/HcOvBTly0lfLQ0Eylu9FQ4OqBhFpLyBgwykiSf8zw== - dependencies: - rfdc "^1.4.1" +"@vue/devtools-shared@^8.0.7": + version "8.0.7" + resolved "https://registry.yarnpkg.com/@vue/devtools-shared/-/devtools-shared-8.0.7.tgz#a33086022ea28f04e22e0fd3083b256c205f9f2d" + integrity sha512-CgAb9oJH5NUmbQRdYDj/1zMiaICYSLtm+B1kxcP72LBrifGAjUmt8bx52dDH1gWRPlQgxGPqpAMKavzVirAEhA== -"@vue/reactivity@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.22.tgz#9b26f8557c96df46c9a859914a2229f3ca5b8f4f" - integrity sha512-f2Wux4v/Z2pqc9+4SmgZC1p73Z53fyD90NFWXiX9AKVnVBEvLFOWCEgJD3GdGnlxPZt01PSlfmLqbLYzY/Fw4A== +"@vue/reactivity@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/reactivity/-/reactivity-3.5.29.tgz#68fbaedcc2584328060edceedc4d4370b49c9fab" + integrity sha512-zcrANcrRdcLtmGZETBxWqIkoQei8HaFpZWx/GHKxx79JZsiZ8j1du0VUJtu4eJjgFvU/iKL5lRXFXksVmI+5DA== dependencies: - "@vue/shared" "3.5.22" + "@vue/shared" "3.5.29" -"@vue/runtime-core@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.22.tgz#e004c1e35f423555a0e4c10646ef3e9d380643d1" - integrity sha512-EHo4W/eiYeAzRTN5PCextDUZ0dMs9I8mQ2Fy+OkzvRPUYQEyK9yAjbasrMCXbLNhF7P0OUyivLjIy0yc6VrLJQ== +"@vue/runtime-core@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/runtime-core/-/runtime-core-3.5.29.tgz#fbd34110aa47e74a22fe430734018bec56913b40" + integrity sha512-8DpW2QfdwIWOLqtsNcds4s+QgwSaHSJY/SUe04LptianUQ/0xi6KVsu/pYVh+HO3NTVvVJjIPL2t6GdeKbS4Lg== dependencies: - "@vue/reactivity" "3.5.22" - "@vue/shared" "3.5.22" + "@vue/reactivity" "3.5.29" + "@vue/shared" "3.5.29" -"@vue/runtime-dom@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.22.tgz#01276cea7cb9ac2b9aba046adfb5903b494e2e7e" - integrity sha512-Av60jsryAkI023PlN7LsqrfPvwfxOd2yAwtReCjeuugTJTkgrksYJJstg1e12qle0NarkfhfFu1ox2D+cQotww== +"@vue/runtime-dom@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/runtime-dom/-/runtime-dom-3.5.29.tgz#4eb686b22178f13c262ea1d0d7171a3134ced21f" + integrity sha512-AHvvJEtcY9tw/uk+s/YRLSlxxQnqnAkjqvK25ZiM4CllCZWzElRAoQnCM42m9AHRLNJ6oe2kC5DCgD4AUdlvXg== dependencies: - "@vue/reactivity" "3.5.22" - "@vue/runtime-core" "3.5.22" - "@vue/shared" "3.5.22" - csstype "^3.1.3" - -"@vue/server-renderer@3.5.22": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.22.tgz#d134e3409094044bd066d9803714677457756157" - integrity sha512-gXjo+ao0oHYTSswF+a3KRHZ1WszxIqO7u6XwNHqcqb9JfyIL/pbWrrh/xLv7jeDqla9u+LK7yfZKHih1e1RKAQ== + "@vue/reactivity" "3.5.29" + "@vue/runtime-core" "3.5.29" + "@vue/shared" "3.5.29" + csstype "^3.2.3" + +"@vue/server-renderer@3.5.29": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/server-renderer/-/server-renderer-3.5.29.tgz#9cbcbf676b2976fbb8171d5de79501894ba84cad" + integrity sha512-G/1k6WK5MusLlbxSE2YTcqAAezS+VuwHhOvLx2KnQU7G2zCH6KIb+5Wyt6UjMq7a3qPzNEjJXs1hvAxDclQH+g== dependencies: - "@vue/compiler-ssr" "3.5.22" - "@vue/shared" "3.5.22" - -"@vue/shared@3.5.22", "@vue/shared@^3.5.18": - version "3.5.22" - resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.22.tgz#9d56a1644a3becb8af1e34655928b0e288d827f8" - integrity sha512-F4yc6palwq3TT0u+FYf0Ns4Tfl9GRFURDN2gWG7L1ecIaS/4fCIuFOjMTnCyjsu/OK6vaDKLCrGAa+KvvH+h4w== - -"@vueuse/core@13.9.0", "@vueuse/core@^13.6.0": - version "13.9.0" - resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-13.9.0.tgz#051aeff47a259e9e4d7d0cc3e54879817b0cbcad" - integrity sha512-ts3regBQyURfCE2BcytLqzm8+MmLlo5Ln/KLoxDVcsZ2gzIwVNnQpQOL/UKV8alUqjSZOlpFZcRNsLRqj+OzyA== + "@vue/compiler-ssr" "3.5.29" + "@vue/shared" "3.5.29" + +"@vue/shared@3.5.29", "@vue/shared@^3.5.27": + version "3.5.29" + resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.5.29.tgz#0fe0d7637b05599d56ca58d83a77c637a1774110" + integrity sha512-w7SR0A5zyRByL9XUkCfdLs7t9XOHUyJ67qPGQjOou3p6GvBeBW+AVjUUmlxtZ4PIYaRvE+1LmK44O4uajlZwcg== + +"@vueuse/core@14.2.1", "@vueuse/core@^14.1.0": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@vueuse/core/-/core-14.2.1.tgz#b5cf36a07b4ea973381e18523ad0ed6ddc98a5be" + integrity sha512-3vwDzV+GDUNpdegRY6kzpLm4Igptq+GA0QkJ3W61Iv27YWwW/ufSlOfgQIpN6FZRMG0mkaz4gglJRtq5SeJyIQ== dependencies: "@types/web-bluetooth" "^0.0.21" - "@vueuse/metadata" "13.9.0" - "@vueuse/shared" "13.9.0" + "@vueuse/metadata" "14.2.1" + "@vueuse/shared" "14.2.1" -"@vueuse/integrations@^13.6.0": - version "13.9.0" - resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-13.9.0.tgz#1bd1d77093a327321cca00e2bbf5da7b18aa6b43" - integrity sha512-SDobKBbPIOe0cVL7QxMzGkuUGHvWTdihi9zOrrWaWUgFKe15cwEcwfWmgrcNzjT6kHnNmWuTajPHoIzUjYNYYQ== +"@vueuse/integrations@^14.1.0": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@vueuse/integrations/-/integrations-14.2.1.tgz#9a4476377bdcb9872102ddea35ddef081066f054" + integrity sha512-2LIUpBi/67PoXJGqSDQUF0pgQWpNHh7beiA+KG2AbybcNm+pTGWT6oPGlBgUoDWmYwfeQqM/uzOHqcILpKL7nA== dependencies: - "@vueuse/core" "13.9.0" - "@vueuse/shared" "13.9.0" + "@vueuse/core" "14.2.1" + "@vueuse/shared" "14.2.1" -"@vueuse/metadata@13.9.0": - version "13.9.0" - resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-13.9.0.tgz#57c738d99661c33347080c0bc4cd11160e0d0881" - integrity sha512-1AFRvuiGphfF7yWixZa0KwjYH8ulyjDCC0aFgrGRz8+P4kvDFSdXLVfTk5xAN9wEuD1J6z4/myMoYbnHoX07zg== +"@vueuse/metadata@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@vueuse/metadata/-/metadata-14.2.1.tgz#bd3338a565c2f651b9d18ac0f8825aa6077ee461" + integrity sha512-1ButlVtj5Sb/HDtIy1HFr1VqCP4G6Ypqt5MAo0lCgjokrk2mvQKsK2uuy0vqu/Ks+sHfuHo0B9Y9jn9xKdjZsw== -"@vueuse/shared@13.9.0": - version "13.9.0" - resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-13.9.0.tgz#7168b4ed647e625b05eb4e7e80fe8aabd00e3923" - integrity sha512-e89uuTLMh0U5cZ9iDpEI2senqPGfbPRTHM/0AaQkcxnpqjkZqDYP8rpfm7edOz8s+pOCOROEy1PIveSW8+fL5g== +"@vueuse/shared@14.2.1": + version "14.2.1" + resolved "https://registry.yarnpkg.com/@vueuse/shared/-/shared-14.2.1.tgz#829a271147937f6b105bb1422d3171e6142f47ba" + integrity sha512-shTJncjV9JTI4oVNyF1FQonetYAiTBd+Qj7cY89SWbXSkx7gyhrgtEdF2ZAVWS1S3SHlaROO6F2IesJxQEkZBw== birpc@^2.6.1: - version "2.6.1" - resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.6.1.tgz#c73463590928897e80f3263d9fbb7da63515014b" - integrity sha512-LPnFhlDpdSH6FJhJyn4M0kFO7vtQ5iPw24FnG0y21q09xC7e8+1LeR31S1MAIrDAHp4m7aas4bEkTDTvMAtebQ== + version "2.9.0" + resolved "https://registry.yarnpkg.com/birpc/-/birpc-2.9.0.tgz#b59550897e4cd96a223e2a6c1475b572236ed145" + integrity sha512-KrayHS5pBi69Xi9JmvoqrIgYGDkD6mcSe/i6YKi3w5kekCLzrX4+nawcXqrj2tIp50Kw/mT/s3p+GVK0A0sKxw== ccount@^2.0.0: version "2.0.1" @@ -579,17 +594,10 @@ comma-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz#4e89c9458acb61bc8fef19f4529973b2392839ee" integrity sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg== -copy-anything@^4: - version "4.0.5" - resolved "https://registry.yarnpkg.com/copy-anything/-/copy-anything-4.0.5.tgz#16cabafd1ea4bb327a540b750f2b4df522825aea" - integrity sha512-7Vv6asjS4gMOuILabD3l739tsaxFQmC+a7pLZm02zyvs8p977bL3zEgq3yDk5rn9B0PbYgIv++jmHcuUab4RhA== - dependencies: - is-what "^5.2.0" - -csstype@^3.1.3: - version "3.1.3" - resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81" - integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== +csstype@^3.2.3: + version "3.2.3" + resolved "https://registry.yarnpkg.com/csstype/-/csstype-3.2.3.tgz#ec48c0f3e993e50648c86da559e2610995cf989a" + integrity sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ== dequal@^2.0.0: version "2.0.3" @@ -603,42 +611,42 @@ devlop@^1.0.0: dependencies: dequal "^2.0.0" -entities@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" - integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== +entities@^7.0.1: + version "7.0.1" + resolved "https://registry.yarnpkg.com/entities/-/entities-7.0.1.tgz#26e8a88889db63417dcb9a1e79a3f1bc92b5976b" + integrity sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA== -esbuild@^0.25.0: - version "0.25.11" - resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.25.11.tgz#0f31b82f335652580f75ef6897bba81962d9ae3d" - integrity sha512-KohQwyzrKTQmhXDW1PjCv3Tyspn9n5GcY2RTDqeORIdIJY8yKIF7sTSopFmn/wpMPW4rdPXI0UE5LJLuq3bx0Q== +esbuild@^0.27.0: + version "0.27.3" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.27.3.tgz#5859ca8e70a3af956b26895ce4954d7e73bd27a8" + integrity sha512-8VwMnyGCONIs6cWue2IdpHxHnAjzxnw2Zr7MkVxB2vjmQ2ivqGFb4LEG3SMnv0Gb2F/G/2yA8zUaiL1gywDCCg== optionalDependencies: - "@esbuild/aix-ppc64" "0.25.11" - "@esbuild/android-arm" "0.25.11" - "@esbuild/android-arm64" "0.25.11" - "@esbuild/android-x64" "0.25.11" - "@esbuild/darwin-arm64" "0.25.11" - "@esbuild/darwin-x64" "0.25.11" - "@esbuild/freebsd-arm64" "0.25.11" - "@esbuild/freebsd-x64" "0.25.11" - "@esbuild/linux-arm" "0.25.11" - "@esbuild/linux-arm64" "0.25.11" - "@esbuild/linux-ia32" "0.25.11" - "@esbuild/linux-loong64" "0.25.11" - "@esbuild/linux-mips64el" "0.25.11" - "@esbuild/linux-ppc64" "0.25.11" - "@esbuild/linux-riscv64" "0.25.11" - "@esbuild/linux-s390x" "0.25.11" - "@esbuild/linux-x64" "0.25.11" - "@esbuild/netbsd-arm64" "0.25.11" - "@esbuild/netbsd-x64" "0.25.11" - "@esbuild/openbsd-arm64" "0.25.11" - "@esbuild/openbsd-x64" "0.25.11" - "@esbuild/openharmony-arm64" "0.25.11" - "@esbuild/sunos-x64" "0.25.11" - "@esbuild/win32-arm64" "0.25.11" - "@esbuild/win32-ia32" "0.25.11" - "@esbuild/win32-x64" "0.25.11" + "@esbuild/aix-ppc64" "0.27.3" + "@esbuild/android-arm" "0.27.3" + "@esbuild/android-arm64" "0.27.3" + "@esbuild/android-x64" "0.27.3" + "@esbuild/darwin-arm64" "0.27.3" + "@esbuild/darwin-x64" "0.27.3" + "@esbuild/freebsd-arm64" "0.27.3" + "@esbuild/freebsd-x64" "0.27.3" + "@esbuild/linux-arm" "0.27.3" + "@esbuild/linux-arm64" "0.27.3" + "@esbuild/linux-ia32" "0.27.3" + "@esbuild/linux-loong64" "0.27.3" + "@esbuild/linux-mips64el" "0.27.3" + "@esbuild/linux-ppc64" "0.27.3" + "@esbuild/linux-riscv64" "0.27.3" + "@esbuild/linux-s390x" "0.27.3" + "@esbuild/linux-x64" "0.27.3" + "@esbuild/netbsd-arm64" "0.27.3" + "@esbuild/netbsd-x64" "0.27.3" + "@esbuild/openbsd-arm64" "0.27.3" + "@esbuild/openbsd-x64" "0.27.3" + "@esbuild/openharmony-arm64" "0.27.3" + "@esbuild/sunos-x64" "0.27.3" + "@esbuild/win32-arm64" "0.27.3" + "@esbuild/win32-ia32" "0.27.3" + "@esbuild/win32-x64" "0.27.3" estree-walker@^2.0.2: version "2.0.2" @@ -650,12 +658,12 @@ fdir@^6.5.0: resolved "https://registry.yarnpkg.com/fdir/-/fdir-6.5.0.tgz#ed2ab967a331ade62f18d077dae192684d50d350" integrity sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg== -focus-trap@^7.6.5: - version "7.6.6" - resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.6.6.tgz#a255c1088ddc8cd4363b3023bf28b224fd38aff2" - integrity sha512-v/Z8bvMCajtx4mEXmOo7QEsIzlIOqRXTIwgUfsFOF9gEsespdbD0AkPIka1bSXZ8Y8oZ+2IVDQZePkTfEHZl7Q== +focus-trap@^7.8.0: + version "7.8.0" + resolved "https://registry.yarnpkg.com/focus-trap/-/focus-trap-7.8.0.tgz#b1d9463fa42b93ad7a5223d750493a6c09b672a8" + integrity sha512-/yNdlIkpWbM0ptxno3ONTuf+2g318kh2ez3KSeZN5dZ8YC6AAmgeWz+GasYYiBJPFaYcSAPeu4GfhUaChzIJXA== dependencies: - tabbable "^6.3.0" + tabbable "^6.4.0" fsevents@~2.3.2, fsevents@~2.3.3: version "2.3.3" @@ -696,12 +704,7 @@ html-void-elements@^3.0.0: resolved "https://registry.yarnpkg.com/html-void-elements/-/html-void-elements-3.0.0.tgz#fc9dbd84af9e747249034d4d62602def6517f1d7" integrity sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg== -is-what@^5.2.0: - version "5.5.0" - resolved "https://registry.yarnpkg.com/is-what/-/is-what-5.5.0.tgz#a3031815757cfe1f03fed990bf6355a2d3f628c4" - integrity sha512-oG7cgbmg5kLYae2N5IVd3jm2s+vldjxJzK1pcu9LfpGuQ93MQSzo0okvRna+7y5ifrD+20FE8FvjusyGaz14fw== - -magic-string@^0.30.19: +magic-string@^0.30.21: version "0.30.21" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.21.tgz#56763ec09a0fa8091df27879fd94d19078c00d91" integrity sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ== @@ -760,16 +763,11 @@ micromark-util-types@^2.0.0: resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-2.0.2.tgz#f00225f5f5a0ebc3254f96c36b6605c4b393908e" integrity sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA== -minisearch@^7.1.2: +minisearch@^7.2.0: version "7.2.0" resolved "https://registry.yarnpkg.com/minisearch/-/minisearch-7.2.0.tgz#3dc30e41e9464b3836553b6d969b656614f8f359" integrity sha512-dqT2XBYUOZOiC5t2HRnwADjhNS2cecp9u+TJRiJ1Qp/f5qjkeT5APcGPjHw+bz89Ms8Jp+cG4AlE+QZ/QnDglg== -mitt@^3.0.1: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mitt/-/mitt-3.0.1.tgz#ea36cf0cc30403601ae074c8f77b7092cdab36d1" - integrity sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw== - nanoid@^3.3.11: version "3.3.11" resolved "https://registry.yarnpkg.com/nanoid/-/nanoid-3.3.11.tgz#4f4f112cefbe303202f2199838128936266d185b" @@ -780,19 +778,19 @@ oniguruma-parser@^0.12.1: resolved "https://registry.yarnpkg.com/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz#82ba2208d7a2b69ee344b7efe0ae930c627dcc4a" integrity sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w== -oniguruma-to-es@^4.3.3: - version "4.3.3" - resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-4.3.3.tgz#50db2c1e28ec365e102c1863dfd3d1d1ad18613e" - integrity sha512-rPiZhzC3wXwE59YQMRDodUwwT9FZ9nNBwQQfsd1wfdtlKEyCdRV0avrTcSZ5xlIvGRVPd/cx6ZN45ECmS39xvg== +oniguruma-to-es@^4.3.4: + version "4.3.4" + resolved "https://registry.yarnpkg.com/oniguruma-to-es/-/oniguruma-to-es-4.3.4.tgz#0b909d960faeb84511c979b1f2af64e9bc37ce34" + integrity sha512-3VhUGN3w2eYxnTzHn+ikMI+fp/96KoRSVK9/kMTcFqj1NRDh2IhQCKvYxDnWePKRXY/AqH+Fuiyb7VHSzBjHfA== dependencies: oniguruma-parser "^0.12.1" regex "^6.0.1" regex-recursion "^6.0.2" perfect-debounce@^2.0.0: - version "2.0.0" - resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-2.0.0.tgz#0ff94f1ecbe0a6bca4b1703a2ed08bbe43739aa7" - integrity sha512-fkEH/OBiKrqqI/yIgjR92lMfs2K8105zt/VT6+7eTjNwisrsh47CeIED9z58zI7DfKdH3uHAn25ziRZn3kgAow== + version "2.1.0" + resolved "https://registry.yarnpkg.com/perfect-debounce/-/perfect-debounce-2.1.0.tgz#e7078e38f231cb191855c3136a4423aef725d261" + integrity sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g== picocolors@^1.1.1: version "1.1.1" @@ -831,59 +829,57 @@ regex-utilities@^2.3.0: integrity sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng== regex@^6.0.1: - version "6.0.1" - resolved "https://registry.yarnpkg.com/regex/-/regex-6.0.1.tgz#282fa4435d0c700b09c0eb0982b602e05ab6a34f" - integrity sha512-uorlqlzAKjKQZ5P+kTJr3eeJGSVroLKoHmquUj4zHWuR+hEyNqlXsSKlYYF5F4NI6nl7tWCs0apKJ0lmfsXAPA== + version "6.1.0" + resolved "https://registry.yarnpkg.com/regex/-/regex-6.1.0.tgz#d7ce98f8ee32da7497c13f6601fca2bc4a6a7803" + integrity sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg== dependencies: regex-utilities "^2.3.0" -rfdc@^1.4.1: - version "1.4.1" - resolved "https://registry.yarnpkg.com/rfdc/-/rfdc-1.4.1.tgz#778f76c4fb731d93414e8f925fbecf64cce7f6ca" - integrity sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA== - rollup@^4.43.0: - version "4.52.5" - resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.52.5.tgz#96982cdcaedcdd51b12359981f240f94304ec235" - integrity sha512-3GuObel8h7Kqdjt0gxkEzaifHTqLVW56Y/bjN7PSQtkKr0w3V/QYSdt6QWYtd7A1xUtYQigtdUfgj1RvWVtorw== + version "4.59.0" + resolved "https://registry.yarnpkg.com/rollup/-/rollup-4.59.0.tgz#cf74edac17c1486f562d728a4d923a694abdf06f" + integrity sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg== dependencies: "@types/estree" "1.0.8" optionalDependencies: - "@rollup/rollup-android-arm-eabi" "4.52.5" - "@rollup/rollup-android-arm64" "4.52.5" - "@rollup/rollup-darwin-arm64" "4.52.5" - "@rollup/rollup-darwin-x64" "4.52.5" - "@rollup/rollup-freebsd-arm64" "4.52.5" - "@rollup/rollup-freebsd-x64" "4.52.5" - "@rollup/rollup-linux-arm-gnueabihf" "4.52.5" - "@rollup/rollup-linux-arm-musleabihf" "4.52.5" - "@rollup/rollup-linux-arm64-gnu" "4.52.5" - "@rollup/rollup-linux-arm64-musl" "4.52.5" - "@rollup/rollup-linux-loong64-gnu" "4.52.5" - "@rollup/rollup-linux-ppc64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-gnu" "4.52.5" - "@rollup/rollup-linux-riscv64-musl" "4.52.5" - "@rollup/rollup-linux-s390x-gnu" "4.52.5" - "@rollup/rollup-linux-x64-gnu" "4.52.5" - "@rollup/rollup-linux-x64-musl" "4.52.5" - "@rollup/rollup-openharmony-arm64" "4.52.5" - "@rollup/rollup-win32-arm64-msvc" "4.52.5" - "@rollup/rollup-win32-ia32-msvc" "4.52.5" - "@rollup/rollup-win32-x64-gnu" "4.52.5" - "@rollup/rollup-win32-x64-msvc" "4.52.5" + "@rollup/rollup-android-arm-eabi" "4.59.0" + "@rollup/rollup-android-arm64" "4.59.0" + "@rollup/rollup-darwin-arm64" "4.59.0" + "@rollup/rollup-darwin-x64" "4.59.0" + "@rollup/rollup-freebsd-arm64" "4.59.0" + "@rollup/rollup-freebsd-x64" "4.59.0" + "@rollup/rollup-linux-arm-gnueabihf" "4.59.0" + "@rollup/rollup-linux-arm-musleabihf" "4.59.0" + "@rollup/rollup-linux-arm64-gnu" "4.59.0" + "@rollup/rollup-linux-arm64-musl" "4.59.0" + "@rollup/rollup-linux-loong64-gnu" "4.59.0" + "@rollup/rollup-linux-loong64-musl" "4.59.0" + "@rollup/rollup-linux-ppc64-gnu" "4.59.0" + "@rollup/rollup-linux-ppc64-musl" "4.59.0" + "@rollup/rollup-linux-riscv64-gnu" "4.59.0" + "@rollup/rollup-linux-riscv64-musl" "4.59.0" + "@rollup/rollup-linux-s390x-gnu" "4.59.0" + "@rollup/rollup-linux-x64-gnu" "4.59.0" + "@rollup/rollup-linux-x64-musl" "4.59.0" + "@rollup/rollup-openbsd-x64" "4.59.0" + "@rollup/rollup-openharmony-arm64" "4.59.0" + "@rollup/rollup-win32-arm64-msvc" "4.59.0" + "@rollup/rollup-win32-ia32-msvc" "4.59.0" + "@rollup/rollup-win32-x64-gnu" "4.59.0" + "@rollup/rollup-win32-x64-msvc" "4.59.0" fsevents "~2.3.2" -shiki@^3.9.2: - version "3.14.0" - resolved "https://registry.yarnpkg.com/shiki/-/shiki-3.14.0.tgz#fd38c387a41c918d709252d2d7d85ce63351d88d" - integrity sha512-J0yvpLI7LSig3Z3acIuDLouV5UCKQqu8qOArwMx+/yPVC3WRMgrP67beaG8F+j4xfEWE0eVC4GeBCIXeOPra1g== +shiki@^3.21.0: + version "3.23.0" + resolved "https://registry.yarnpkg.com/shiki/-/shiki-3.23.0.tgz#fca5332195e3afd6c94b384103ae9671a29c7fb9" + integrity sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA== dependencies: - "@shikijs/core" "3.14.0" - "@shikijs/engine-javascript" "3.14.0" - "@shikijs/engine-oniguruma" "3.14.0" - "@shikijs/langs" "3.14.0" - "@shikijs/themes" "3.14.0" - "@shikijs/types" "3.14.0" + "@shikijs/core" "3.23.0" + "@shikijs/engine-javascript" "3.23.0" + "@shikijs/engine-oniguruma" "3.23.0" + "@shikijs/langs" "3.23.0" + "@shikijs/themes" "3.23.0" + "@shikijs/types" "3.23.0" "@shikijs/vscode-textmate" "^10.0.2" "@types/hast" "^3.0.4" @@ -897,11 +893,6 @@ space-separated-tokens@^2.0.0: resolved "https://registry.yarnpkg.com/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz#1ecd9d2350a3844572c3f4a312bceb018348859f" integrity sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q== -speakingurl@^14.0.1: - version "14.0.1" - resolved "https://registry.yarnpkg.com/speakingurl/-/speakingurl-14.0.1.tgz#f37ec8ddc4ab98e9600c1c9ec324a8c48d772a53" - integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ== - stringify-entities@^4.0.0: version "4.0.4" resolved "https://registry.yarnpkg.com/stringify-entities/-/stringify-entities-4.0.4.tgz#b3b79ef5f277cc4ac73caeb0236c5ba939b3a4f3" @@ -910,17 +901,10 @@ stringify-entities@^4.0.0: character-entities-html4 "^2.0.0" character-entities-legacy "^3.0.0" -superjson@^2.2.2: - version "2.2.3" - resolved "https://registry.yarnpkg.com/superjson/-/superjson-2.2.3.tgz#c42236fff6ecc449b7ffa7f023a9a028a5ec9c87" - integrity sha512-ay3d+LW/S6yppKoTz3Bq4mG0xrS5bFwfWEBmQfbC7lt5wmtk+Obq0TxVuA9eYRirBTQb1K3eEpBRHMQEo0WyVw== - dependencies: - copy-anything "^4" - -tabbable@^6.3.0: - version "6.3.0" - resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.3.0.tgz#2e0e6163935387cdeacd44e9334616ca0115a8d3" - integrity sha512-EIHvdY5bPLuWForiR/AN2Bxngzpuwn1is4asboytXtpTgsArc+WmSJKVLlhdh71u7jFcryDqB2A8lQvj78MkyQ== +tabbable@^6.4.0: + version "6.4.0" + resolved "https://registry.yarnpkg.com/tabbable/-/tabbable-6.4.0.tgz#36eb7a06d80b3924a22095daf45740dea3bf5581" + integrity sha512-05PUHKSNE8ou2dwIxTngl4EzcnsCDZGJ/iCLtDflR/SHB/ny14rXc+qU5P4mG9JkusiV7EivzY9Mhm55AzAvCg== tinyglobby@^0.2.15: version "0.2.15" @@ -965,9 +949,9 @@ unist-util-visit-parents@^6.0.0: unist-util-is "^6.0.0" unist-util-visit@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.0.0.tgz#a7de1f31f72ffd3519ea71814cccf5fd6a9217d6" - integrity sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg== + version "5.1.0" + resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-5.1.0.tgz#9a2a28b0aa76a15e0da70a08a5863a2f060e2468" + integrity sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg== dependencies: "@types/unist" "^3.0.0" unist-util-is "^6.0.0" @@ -989,12 +973,12 @@ vfile@^6.0.0: "@types/unist" "^3.0.0" vfile-message "^4.0.0" -vite@^7.1.2: - version "7.1.12" - resolved "https://registry.yarnpkg.com/vite/-/vite-7.1.12.tgz#8b29a3f61eba23bcb93fc9ec9af4a3a1e83eecdb" - integrity sha512-ZWyE8YXEXqJrrSLvYgrRP7p62OziLW7xI5HYGWFzOvupfAlrLvURSzv/FyGyy0eidogEM3ujU+kUG1zuHgb6Ug== +vite@^7.3.1: + version "7.3.1" + resolved "https://registry.yarnpkg.com/vite/-/vite-7.3.1.tgz#7f6cfe8fb9074138605e822a75d9d30b814d6507" + integrity sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA== dependencies: - esbuild "^0.25.0" + esbuild "^0.27.0" fdir "^6.5.0" picomatch "^4.0.3" postcss "^8.5.6" @@ -1003,40 +987,41 @@ vite@^7.1.2: optionalDependencies: fsevents "~2.3.3" -vitepress@^2.0.0-alpha.12: - version "2.0.0-alpha.12" - resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-2.0.0-alpha.12.tgz#e75648eec6c43bff1d669f9a7f81f777acc6e4fd" - integrity sha512-yZwCwRRepcpN5QeAhwSnEJxS3I6zJcVixqL1dnm6km4cnriLpQyy2sXQDsE5Ti3pxGPbhU51nTMwI+XC1KNnJg== +vitepress@^2.0.0-alpha.16: + version "2.0.0-alpha.16" + resolved "https://registry.yarnpkg.com/vitepress/-/vitepress-2.0.0-alpha.16.tgz#1e65446af54837185a9e2169706376409bed1127" + integrity sha512-w1nwsefDVIsje7BZr2tsKxkZutDGjG0YoQ2yxO7+a9tvYVqfljYbwj5LMYkPy8Tb7YbPwa22HtIhk62jbrvuEQ== dependencies: - "@docsearch/css" "^4.0.0-beta.7" - "@docsearch/js" "^4.0.0-beta.7" - "@iconify-json/simple-icons" "^1.2.47" - "@shikijs/core" "^3.9.2" - "@shikijs/transformers" "^3.9.2" - "@shikijs/types" "^3.9.2" + "@docsearch/css" "^4.5.3" + "@docsearch/js" "^4.5.3" + "@docsearch/sidepanel-js" "^4.5.3" + "@iconify-json/simple-icons" "^1.2.68" + "@shikijs/core" "^3.21.0" + "@shikijs/transformers" "^3.21.0" + "@shikijs/types" "^3.21.0" "@types/markdown-it" "^14.1.2" - "@vitejs/plugin-vue" "^6.0.1" - "@vue/devtools-api" "^8.0.0" - "@vue/shared" "^3.5.18" - "@vueuse/core" "^13.6.0" - "@vueuse/integrations" "^13.6.0" - focus-trap "^7.6.5" + "@vitejs/plugin-vue" "^6.0.3" + "@vue/devtools-api" "^8.0.5" + "@vue/shared" "^3.5.27" + "@vueuse/core" "^14.1.0" + "@vueuse/integrations" "^14.1.0" + focus-trap "^7.8.0" mark.js "8.11.1" - minisearch "^7.1.2" - shiki "^3.9.2" - vite "^7.1.2" - vue "^3.5.18" - -vue@^3.5.18, vue@^3.5.22: - version "3.5.22" - resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.22.tgz#2b8ddb94ee4b640ef12fe7f6efe1cf16f3b582e7" - integrity sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ== + minisearch "^7.2.0" + shiki "^3.21.0" + vite "^7.3.1" + vue "^3.5.27" + +vue@^3.5.27, vue@^3.5.29: + version "3.5.29" + resolved "https://registry.yarnpkg.com/vue/-/vue-3.5.29.tgz#859a1cc5219eb1228c7ce6f355b27de080517111" + integrity sha512-BZqN4Ze6mDQVNAni0IHeMJ5mwr8VAJ3MQC9FmprRhcBYENw+wOAAjRj8jfmN6FLl0j96OXbR+CjWhmAmM+QGnA== dependencies: - "@vue/compiler-dom" "3.5.22" - "@vue/compiler-sfc" "3.5.22" - "@vue/runtime-dom" "3.5.22" - "@vue/server-renderer" "3.5.22" - "@vue/shared" "3.5.22" + "@vue/compiler-dom" "3.5.29" + "@vue/compiler-sfc" "3.5.29" + "@vue/runtime-dom" "3.5.29" + "@vue/server-renderer" "3.5.29" + "@vue/shared" "3.5.29" zwitch@^2.0.4: version "2.0.4"