Skip to content

Commit

Permalink
Search bar: Highlighting the search query
Browse files Browse the repository at this point in the history
  • Loading branch information
JanMikes committed Jan 28, 2025
1 parent 13f8e89 commit 24f37ba
Show file tree
Hide file tree
Showing 5 changed files with 111 additions and 4 deletions.
5 changes: 5 additions & 0 deletions assets/styles/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -673,3 +673,8 @@ canvas {
overflow-y: auto; /* Enable vertical scrolling */
overflow-x: hidden; /* Disable horizontal scrolling (optional) */
}

.search-highlight {
background: rgba(254, 64, 66, .1);
border-radius: 2px;
}
38 changes: 38 additions & 0 deletions src/Services/SearchHighlighter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
<?php

declare(strict_types=1);

namespace SpeedPuzzling\Web\Services;

readonly final class SearchHighlighter
{
public function highlight(string|int|null $text, string $query): string
{
$text = trim((string) $text);

if ($text === '') {
return '';
}

$words = array_filter(explode(' ', trim($query)));

if ($words === []) {
return $text;
}

$escapedWords = array_map(
callback: fn (string $word): string => preg_quote($word, '/'),
array: $words
);

$pattern = '/' . implode('|', $escapedWords) . '/i';

$highlightedText = preg_replace_callback(
pattern: $pattern,
callback: fn (array $matches): string => '<span class="search-highlight">' . htmlspecialchars($matches[0], ENT_QUOTES, 'UTF-8') . '</span>',
subject: $text,
);

return $highlightedText ?? $text;
}
}
27 changes: 27 additions & 0 deletions src/Twig/SearchHighlightTwigExtension.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
<?php

declare(strict_types=1);

namespace SpeedPuzzling\Web\Twig;

use SpeedPuzzling\Web\Services\SearchHighlighter;
use Twig\Extension\AbstractExtension;
use Twig\TwigFunction;

final class SearchHighlightTwigExtension extends AbstractExtension
{
public function __construct(
readonly private SearchHighlighter $highlighter,
) {
}

/**
* @return array<TwigFunction>
*/
public function getFunctions(): array
{
return [
new TwigFunction('highlight', $this->highlighter->highlight(...), ['is_safe' => ['html']]),
];
}
}
8 changes: 4 additions & 4 deletions templates/components/GlobalSearch.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,8 @@
<i class="ci-star-filled text-warning"></i>
{% endif %}

{{ player.playerName }}
<code class="text-muted">#{{ player.playerCode|upper }}</code>
{{ highlight(player.playerName, this.query) }}
<code class="text-muted">#{{ highlight(player.playerCode|upper, this.query) }}</code>
{% if player.playerCountry is not null %}
<small class="shadow-custom fi fi-{{ player.playerCountry.name }}"></small>
{% endif %}
Expand Down Expand Up @@ -62,7 +62,7 @@
<div class="ps-2 flex-grow-1">
<div class="puzzle-name">
<a class="text-decoration-underline fs-sm low-line-height d-inline-block mb-1" data-turbo-frame="_top" href="{{ path('puzzle_detail', {puzzleId: puzzle.puzzleId}) }}">
{{ puzzle.puzzleName }}
{{ highlight(puzzle.puzzleName, this.query) }}
{#
{% if puzzle.puzzleId in puzzles_solved_by_user %}<i class="ms-1 text-decoration-none ci-check-circle text-success"></i>{% endif %}
#}
Expand All @@ -74,7 +74,7 @@
<div class="fs-sm manufacturer-name">
{{ puzzle.manufacturerName }}
{% if puzzle.puzzleIdentificationNumber is not null %}
<small class="text-muted">{{ puzzle.puzzleIdentificationNumber }}</small>
<small class="text-muted">{{ highlight(puzzle.puzzleIdentificationNumber, this.query) }}</small>
{% endif %}
</div>

Expand Down
37 changes: 37 additions & 0 deletions tests/Services/SearchHighlighterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
<?php

declare(strict_types=1);

namespace SpeedPuzzling\Web\Tests\Services;

use PHPUnit\Framework\Attributes\DataProvider;
use SpeedPuzzling\Web\Services\SearchHighlighter;
use PHPUnit\Framework\TestCase;

final class SearchHighlighterTest extends TestCase
{
#[DataProvider('provideTestHighlightData')]
public function testHighlight(null|int|string $text, string $search, string $expected): void
{
$highlighter = new SearchHighlighter();

$this->assertSame($expected, $highlighter->highlight($text, $search));
}

/**
* @return \Generator<array{null|int|string, string, string}>
*/
public static function provideTestHighlightData(): \Generator
{
yield ['', '', ''];
yield [null, 'are', ''];
yield [1234, 'are', '1234'];
yield [1234, '23', '1<span class="search-highlight">23</span>4'];
yield ['Karen', 'are', 'K<span class="search-highlight">are</span>n'];
yield ['KAREN', 'are', 'K<span class="search-highlight">ARE</span>N'];
yield ['karen', 'ARE', 'k<span class="search-highlight">are</span>n'];
yield ['Karen Puzzle', 'kar puz', '<span class="search-highlight">Kar</span>en <span class="search-highlight">Puz</span>zle'];
yield ['KAREN PUZZLE', 'kar puz', '<span class="search-highlight">KAR</span>EN <span class="search-highlight">PUZ</span>ZLE'];
yield ['karen puzzle', 'kAR pUZ', '<span class="search-highlight">kar</span>en <span class="search-highlight">puz</span>zle'];
}
}

0 comments on commit 24f37ba

Please sign in to comment.