feat: FontSuggest – ähnliche Schriftnamen für unbekannte Schriften (W5)
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5.5
parent
8740e9b8dc
commit
05ff2d90f3
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Integrations\Divi\Design;
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
/**
|
||||
* Names that resemble an unknown font name, for the hint of a validation error (ruling W5). Case-insensitive;
|
||||
* ranked: the same name in another case, a name that starts with it, one that contains it, then a small
|
||||
* Levenshtein distance (at most max(3, length / 3)). Within a rank the closer, then the shorter, then the
|
||||
* alphabetically first name wins, so the order is deterministic. Pure PHP.
|
||||
*/
|
||||
final class FontSuggest
|
||||
{
|
||||
/**
|
||||
* @param list<string> $names
|
||||
* @return list<string> At most $limit names.
|
||||
*/
|
||||
public static function similar(string $name, array $names, int $limit = 5): array
|
||||
{
|
||||
$needle = mb_strtolower(trim($name));
|
||||
if ($needle === '' || $limit < 1) {
|
||||
return [];
|
||||
}
|
||||
$maxDistance = max(3, intdiv(mb_strlen($needle), 3));
|
||||
$ranked = [];
|
||||
foreach (array_unique($names) as $candidate) {
|
||||
$lower = mb_strtolower($candidate);
|
||||
$distance = levenshtein($needle, $lower);
|
||||
$rank = match (true) {
|
||||
$lower === $needle => 0,
|
||||
str_starts_with($lower, $needle) => 1,
|
||||
str_contains($lower, $needle) => 2,
|
||||
$distance <= $maxDistance => 3,
|
||||
default => null,
|
||||
};
|
||||
if ($rank !== null) {
|
||||
$ranked[] = [$rank, $distance, mb_strlen($candidate), $candidate];
|
||||
}
|
||||
}
|
||||
usort($ranked, static fn (array $a, array $b): int => $a[0] <=> $b[0] ?: $a[1] <=> $b[1] ?: $a[2] <=> $b[2]
|
||||
?: strcmp($a[3], $b[3]));
|
||||
|
||||
return array_slice(array_column($ranked, 3), 0, $limit);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace LIT\MCP\Tests\Unit\Integrations\Divi\Design;
|
||||
|
||||
use LIT\MCP\Integrations\Divi\Design\FontSuggest;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
final class FontSuggestTest extends TestCase
|
||||
{
|
||||
private const NAMES = [
|
||||
'Roboto', 'Roboto Condensed', 'Roboto Slab', 'Open Sans', 'Open Sans Condensed', 'Lato', 'Lora', 'Arial',
|
||||
'Source Sans 3', 'Playfair Display', 'Helvetica', 'Georgia',
|
||||
];
|
||||
|
||||
public function testTheSameNameInAnotherCaseComesFirst(): void
|
||||
{
|
||||
self::assertSame(
|
||||
['Open Sans', 'Open Sans Condensed'],
|
||||
FontSuggest::similar('open sans', self::NAMES, 2)
|
||||
);
|
||||
self::assertSame('Lato', FontSuggest::similar('LATO', self::NAMES)[0]);
|
||||
}
|
||||
|
||||
public function testRanksPrefixBeforeSubstringBeforeASmallDistance(): void
|
||||
{
|
||||
$names = ['Big Roboto', 'Rubik', 'Roboto Slab', 'Roboot', 'Robot'];
|
||||
|
||||
self::assertSame(
|
||||
['Roboto Slab', 'Big Roboto', 'Robot', 'Roboot'],
|
||||
FontSuggest::similar('roboto', $names)
|
||||
);
|
||||
}
|
||||
|
||||
public function testATypoIsFoundByDistanceButUnrelatedNamesAreNot(): void
|
||||
{
|
||||
self::assertSame(['Open Sans'], FontSuggest::similar('Opne Sans', self::NAMES));
|
||||
self::assertSame(['Playfair Display'], FontSuggest::similar('Playfair Displey', self::NAMES));
|
||||
self::assertSame([], FontSuggest::similar('Comic Sans', ['Arial', 'Georgia', 'Helvetica']));
|
||||
}
|
||||
|
||||
public function testTheLimitCutsTheListAndTheOrderIsStable(): void
|
||||
{
|
||||
$first = FontSuggest::similar('Roboto', self::NAMES, 2);
|
||||
|
||||
self::assertSame(['Roboto', 'Roboto Slab'], $first);
|
||||
self::assertSame($first, FontSuggest::similar('Roboto', array_reverse(self::NAMES), 2));
|
||||
self::assertSame(['Roboto', 'Roboto Slab', 'Roboto Condensed'], FontSuggest::similar('Roboto', self::NAMES, 5));
|
||||
self::assertCount(5, FontSuggest::similar('o', self::NAMES));
|
||||
}
|
||||
|
||||
public function testTiesAreBrokenByLengthThenAlphabet(): void
|
||||
{
|
||||
self::assertSame(['Aab', 'Aac', 'Aaaa'], FontSuggest::similar('aa', ['Aaaa', 'Aac', 'Aab']));
|
||||
}
|
||||
|
||||
public function testEdgeCases(): void
|
||||
{
|
||||
self::assertSame([], FontSuggest::similar('', self::NAMES));
|
||||
self::assertSame([], FontSuggest::similar(' ', self::NAMES));
|
||||
self::assertSame([], FontSuggest::similar('Lato', self::NAMES, 0));
|
||||
self::assertSame([], FontSuggest::similar('Lato', []));
|
||||
self::assertSame(['Lato'], FontSuggest::similar(' lato ', ['Lato', 'Lato']), 'trimmed, duplicates once');
|
||||
self::assertSame(['Fjalla Önö'], FontSuggest::similar('FJALLA ÖNÖ', ['Fjalla Önö']));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user