diff --git a/.github/workflows/fix-code-styling.yml b/.github/workflows/fix-code-styling.yml index bb07cf3..dfbe75e 100644 --- a/.github/workflows/fix-code-styling.yml +++ b/.github/workflows/fix-code-styling.yml @@ -3,7 +3,7 @@ name: "Fix Code Styling" on: push: branches: - - master + - '*.x' jobs: lint: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..e76727e --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,61 @@ +name: "Publish Release" + +on: + push: + tags: + - 'v*' + +permissions: + contents: write + +jobs: + release: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Determine release branch + id: branch + run: | + branch=$(git branch -r --contains "$GITHUB_SHA" --format='%(refname:short)' | sed 's#^origin/##' | grep -E '^[0-9]+\.x$' | head -n1) + if [ -z "$branch" ]; then + echo "Could not find a release branch containing $GITHUB_SHA" >&2 + exit 1 + fi + echo "branch=$branch" >> "$GITHUB_OUTPUT" + + - name: Publish release + uses: softprops/action-gh-release@v2 + with: + generate_release_notes: true + + - name: Fetch release notes + id: notes + env: + GH_TOKEN: ${{ github.token }} + run: | + { + echo 'body<> "$GITHUB_OUTPUT" + + - name: Switch to release branch + run: git checkout ${{ steps.branch.outputs.branch }} + + - name: Update Changelog + uses: stefanzweifel/changelog-updater-action@v1 + with: + latest-version: ${{ github.ref_name }} + release-notes: ${{ steps.notes.outputs.body }} + + - name: Commit updated CHANGELOG + uses: stefanzweifel/git-auto-commit-action@v5 + with: + branch: ${{ steps.branch.outputs.branch }} + commit_message: Update CHANGELOG + file_pattern: CHANGELOG.md diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml index 8c108e6..19234b8 100644 --- a/.github/workflows/run-tests.yml +++ b/.github/workflows/run-tests.yml @@ -1,6 +1,10 @@ name: run-tests -on: [push, pull_request] +on: + push: + branches: + - '*.x' + pull_request: jobs: test: @@ -9,20 +13,27 @@ jobs: fail-fast: true matrix: os: [ubuntu-latest] - php: [8.2, 8.1] - laravel: [10.*] + php: [8.2, 8.3, 8.4] + laravel: [12.*, 13.*] stability: [prefer-lowest, prefer-stable] include: - - laravel: 10.* - testbench: 8.* - pest: ^2.13 - pest_laravel: ^2.0 + - laravel: 12.* + testbench: 10.* + pest: ^3.0 + pest_laravel: ^3.0 + - laravel: 13.* + testbench: 11.* + pest: ^4.0 + pest_laravel: ^4.0 + exclude: + - laravel: 13.* + php: 8.2 name: P${{ matrix.php }} - L${{ matrix.laravel }} - ${{ matrix.stability }} - ${{ matrix.os }} steps: - name: Checkout code - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Setup PHP uses: shivammathur/setup-php@v2 diff --git a/.github/workflows/update-changelog.yml b/.github/workflows/update-changelog.yml deleted file mode 100644 index 74f8889..0000000 --- a/.github/workflows/update-changelog.yml +++ /dev/null @@ -1,28 +0,0 @@ -name: "Update Changelog" - -on: - release: - types: [released] - -jobs: - update: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v3 - with: - ref: main - - - name: Update Changelog - uses: stefanzweifel/changelog-updater-action@v1 - with: - latest-version: ${{ github.event.release.name }} - release-notes: ${{ github.event.release.body }} - - - name: Commit updated CHANGELOG - uses: stefanzweifel/git-auto-commit-action@v4.16.0 - with: - branch: main - commit_message: Update CHANGELOG - file_pattern: CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..e70d671 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +All notable changes to `cachethq/badger` will be documented in this file. diff --git a/README.md b/README.md index 39e5f5f..61ac32a 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ An SVG badge generator for Laravel. ## Installation -This version requires [PHP](https://php.net) 8.2 and Laravel 11.0 and above. For older versions of PHP or Laravel, check out the [3.x](https://github.com/cachethq/badger/tree/3.x) branch. +This version requires [PHP](https://php.net) 8.2 and Laravel 12.0 and above. For older versions of PHP or Laravel, check out the [4.x](https://github.com/cachethq/badger/tree/4.x) branch. To get the latest version, simply require the project using [Composer](https://getcomposer.org): @@ -46,12 +46,13 @@ The first parameter is the subject, or what it is the badge is showing. In the e - `grey` - `lightgray` -And finally the fourth parameter is the badge type. This determines the style of the badge and can be one of the four values below. +And finally the fourth parameter is the badge type. This determines the style of the badge and can be one of the five values below. - `flat-square` (default) - `plastic-flat`, `flat` - `plastic` - `social` (without links) +- `for-the-badge` ## Security diff --git a/composer.json b/composer.json index 2c06f46..c184969 100644 --- a/composer.json +++ b/composer.json @@ -14,14 +14,14 @@ } ], "require": { - "php": "^8.0", - "illuminate/contracts": "^8.0|^9.0|^10.0", - "illuminate/support": "^8.0|^9.0|^10.0" + "php": "^8.2", + "illuminate/contracts": "^12.0|^13.0", + "illuminate/support": "^12.0|^13.0" }, "require-dev": { - "orchestra/testbench": "^8.8", - "pestphp/pest": "^2.13", - "pestphp/pest-plugin-laravel": "^2.2" + "orchestra/testbench": "^10.0|^11.0", + "pestphp/pest": "^3.0|^4.0", + "pestphp/pest-plugin-laravel": "^3.0|^4.0" }, "autoload": { "psr-4": { @@ -44,7 +44,7 @@ }, "extra": { "branch-alias": { - "dev-master": "4.0-dev" + "dev-5.x": "5.0-dev" }, "laravel": { "providers": [ diff --git a/resources/templates/for-the-badge.svg b/resources/templates/for-the-badge.svg new file mode 100644 index 0000000..6ca9755 --- /dev/null +++ b/resources/templates/for-the-badge.svg @@ -0,0 +1,12 @@ + + + + + + + {{ vendor }} + {{ vendor }} + {{ value }} + {{ value }} + + diff --git a/src/Badge.php b/src/Badge.php index 99ed9ff..d444893 100755 --- a/src/Badge.php +++ b/src/Badge.php @@ -54,9 +54,9 @@ class Badge /** * Create a new badge instance. * - * @throws \Cachet\Badger\Exceptions\InvalidHexColorException + * @throws InvalidHexColorException */ - public function __construct(string $subject, string $status, string $color, string $format = null) + public function __construct(string $subject, string $status, string $color, ?string $format = null) { $this->subject = htmlspecialchars($subject, ENT_XML1, 'UTF-8'); $this->status = htmlspecialchars($status, ENT_XML1, 'UTF-8'); @@ -75,7 +75,7 @@ public function __construct(string $subject, string $status, string $color, stri */ public static function fromString(string $format): Badge { - if (preg_match('/^(([^-]|--)+)-(([^-]|--)+)-(([^-]|--)+)\.(svg|png|gif|jpg)$/', $format, $match) === false && (7 != count($match))) { + if (preg_match('/^(([^-]|--)+)-(([^-]|--)+)-(([^-]|--)+)\.(svg|png|gif|jpg)$/', $format, $match) === false && (count($match) != 7)) { throw new InvalidArgumentException('The given format string is invalid: '.$format); } diff --git a/src/Badger.php b/src/Badger.php index ca26d4a..8e268f1 100755 --- a/src/Badger.php +++ b/src/Badger.php @@ -10,16 +10,16 @@ class Badger /** * The available renderers. * - * @var \Cachet\Badger\Render\RenderInterface[] + * @var RenderInterface[] */ protected array $renderers; /** * Create a new badger instance. * - * @param \Cachet\Badger\Render\RenderInterface[] $renderers + * @param RenderInterface[] $renderers */ - public function __construct(array $renderers = null) + public function __construct(?array $renderers = null) { foreach ($renderers as $renderer) { $this->addRenderFormat($renderer); @@ -61,7 +61,7 @@ protected function addRenderFormat(RenderInterface $renderer): void /** * Returns the renderer for the given format. * - * @throws \Cachet\Badger\Exceptions\InvalidRendererException + * @throws InvalidRendererException */ protected function getRendererForFormat(string $format): RenderInterface { diff --git a/src/BadgerServiceProvider.php b/src/BadgerServiceProvider.php index 6f19b55..bee8b21 100755 --- a/src/BadgerServiceProvider.php +++ b/src/BadgerServiceProvider.php @@ -5,6 +5,7 @@ use Cachet\Badger\Calculator\GDTextSizeCalculator; use Cachet\Badger\Calculator\TextSizeCalculatorInterface; use Cachet\Badger\Render\FlatSquareRender; +use Cachet\Badger\Render\ForTheBadgeRender; use Cachet\Badger\Render\PlasticFlatRender; use Cachet\Badger\Render\PlasticRender; use Cachet\Badger\Render\SocialRender; @@ -49,6 +50,7 @@ protected function registerBadger(): void new PlasticRender($calculator, $path), new PlasticFlatRender($calculator, $path), new FlatSquareRender($calculator, $path), + new ForTheBadgeRender($calculator, $path), new SocialRender($calculator, $path), ]; diff --git a/src/Calculator/GDTextSizeCalculator.php b/src/Calculator/GDTextSizeCalculator.php index 3782af8..e2003c9 100755 --- a/src/Calculator/GDTextSizeCalculator.php +++ b/src/Calculator/GDTextSizeCalculator.php @@ -20,7 +20,7 @@ public function __construct(string $path) /** * Calculate the width of the text box. */ - public function calculateWidth(string $text, int $size = null): float + public function calculateWidth(string $text, ?int $size = null): float { $size = round(($size ?: static::TEXT_SIZE) * 0.75, 1); $box = imagettfbbox($size, 0, $this->path, $text); diff --git a/src/Calculator/TextSizeCalculatorInterface.php b/src/Calculator/TextSizeCalculatorInterface.php index e5d1706..296ebb7 100755 --- a/src/Calculator/TextSizeCalculatorInterface.php +++ b/src/Calculator/TextSizeCalculatorInterface.php @@ -28,5 +28,5 @@ interface TextSizeCalculatorInterface /** * Calculate the width of the text box. */ - public function calculateWidth(string $text, int $size = null): float; + public function calculateWidth(string $text, ?int $size = null): float; } diff --git a/src/Render/AbstractRender.php b/src/Render/AbstractRender.php index df36ffc..ea4b115 100755 --- a/src/Render/AbstractRender.php +++ b/src/Render/AbstractRender.php @@ -26,7 +26,7 @@ abstract class AbstractRender implements RenderInterface /** * Create a new svg render instance. */ - public function __construct(TextSizeCalculatorInterface $calculator, string $path, string $color = null) + public function __construct(TextSizeCalculatorInterface $calculator, string $path, ?string $color = null) { $this->calculator = $calculator; $this->path = $path; diff --git a/src/Render/ForTheBadgeRender.php b/src/Render/ForTheBadgeRender.php new file mode 100644 index 0000000..4338a28 --- /dev/null +++ b/src/Render/ForTheBadgeRender.php @@ -0,0 +1,81 @@ +uppercase($badge->subject()); + $status = $this->uppercase($badge->status()); + + $subjectWidth = $this->sectionWidth($subject); + $statusWidth = $this->sectionWidth($status); + + $params = [ + 'vendorWidth' => $subjectWidth, + 'valueWidth' => $statusWidth, + 'totalWidth' => $subjectWidth + $statusWidth, + 'vendorColor' => $this->color, + 'valueColor' => $badge->hexColor(), + 'vendor' => $subject, + 'value' => $status, + 'vendorStartPosition' => round($subjectWidth / 2, 1) + 1, + 'valueStartPosition' => $subjectWidth + round($statusWidth / 2, 1) - 1, + ]; + + return $this->renderSvg($params, $badge->format()); + } + + /** + * Uppercase already XML-escaped text without corrupting its entities. + */ + protected function uppercase(string $text): string + { + return htmlspecialchars( + mb_strtoupper(htmlspecialchars_decode($text, ENT_XML1)), + ENT_XML1, + 'UTF-8' + ); + } + + /** + * Calculate the width of a badge section, accounting for letter spacing and padding. + */ + protected function sectionWidth(string $text): float + { + return $this->stringWidth($text) + (mb_strlen($text) * self::LETTER_SPACING) + self::EXTRA_PADDING; + } + + /** + * Returns the template contents. + */ + protected function getTemplate(): string + { + return 'for-the-badge.svg'; + } +} diff --git a/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_awesome_bright_green.snap b/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_awesome_bright_green.snap new file mode 100644 index 0000000..fc3a02c --- /dev/null +++ b/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_awesome_bright_green.snap @@ -0,0 +1,12 @@ + + + + + + + ALT THREE + ALT THREE + AWESOME + AWESOME + + diff --git a/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_dead_red.snap b/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_dead_red.snap new file mode 100644 index 0000000..e941c25 --- /dev/null +++ b/tests/.pest/snapshots/Render/ForTheBadgeRenderTest/it_can_render_alt_three_dead_red.snap @@ -0,0 +1,12 @@ + + + + + + + ALT THREE + ALT THREE + DEAD + DEAD + + diff --git a/tests/Render/ForTheBadgeRenderTest.php b/tests/Render/ForTheBadgeRenderTest.php new file mode 100644 index 0000000..833b7b9 --- /dev/null +++ b/tests/Render/ForTheBadgeRenderTest.php @@ -0,0 +1,37 @@ +getSupportedFormats())->toMatchArray(['for-the-badge']); +}); + +it('can render alt three awesome bright green', function () { + $svgRender = getSvgRenderer(ForTheBadgeRender::class); + $badge = new Badge('Alt Three', 'Awesome', 'brightgreen', 'svg'); + $badgeImage = $svgRender->render($badge); + + expect($badgeImage)->toMatchSnapshot(); + expect($badgeImage->getFormat())->toBe('svg'); +}); + +it('uppercases text without corrupting xml entities', function () { + $svgRender = getSvgRenderer(ForTheBadgeRender::class); + $badge = new Badge('R&D', 'Arender($badge); + + expect((string) $badgeImage)->toContain('R&D'); + expect((string) $badgeImage)->toContain('A<B'); +}); + +it('can render alt three dead red', function () { + $svgRender = getSvgRenderer(ForTheBadgeRender::class); + $badge = new Badge('Alt Three', 'Dead', 'red', 'svg'); + $badgeImage = $svgRender->render($badge); + + expect($badgeImage)->toMatchSnapshot(); + expect($badgeImage->getFormat())->toBe('svg'); +}); diff --git a/tests/TestCase.php b/tests/TestCase.php index 1a033a7..d7f927b 100755 --- a/tests/TestCase.php +++ b/tests/TestCase.php @@ -2,6 +2,9 @@ namespace Cachet\Tests\Badger; +use Illuminate\Foundation\Application; +use Illuminate\Support\Facades\Facade; +use Illuminate\Support\ServiceProvider; use Orchestra\Testbench\TestCase as Orchestra; abstract class TestCase extends Orchestra @@ -9,8 +12,8 @@ abstract class TestCase extends Orchestra /** * Get package providers. * - * @param \Illuminate\Foundation\Application $app - * @return array> + * @param Application $app + * @return array> */ protected function getPackageProviders($app) { @@ -22,8 +25,8 @@ protected function getPackageProviders($app) /** * Override application aliases. * - * @param \Illuminate\Foundation\Application $app - * @return array> + * @param Application $app + * @return array> */ protected function getPackageAliases($app) {