1
0
Files
breadcrumb-the-shire/core/Support/Search/SearchSpecialResourceProvider.php
fs 0e86925464 refactor: rename lib/ to core/ for clearer core-module separation
Rename the top-level lib/ directory to core/ so the project root
immediately communicates which code is core platform and which lives
in modules/. PHP namespaces (MintyPHP\*) are unchanged — only the
Composer PSR-4 path mapping moves from lib/ to core/.

Module-internal lib/ directories (modules/*/lib/) are untouched.

Updated across the full stack:
- composer.json PSR-4 mapping
- bootstrap entry points (web/index.php, bin/*, tests/bootstrap.php)
- tooling configs (phpstan.neon, phpunit.xml, php-cs-fixer)
- 26 architecture contract tests
- enforcement-policy, guard-catalog, quality-gates
- all documentation (CLAUDE.md, README, 25 docs/, .agents/skills/)
- bin/qa-extended.sh search paths
- .claude/settings.local.json permission paths

Workflow: .agents/runs/CORE-LIB-RENAME-001/ (Analyst → Planner →
Executor → Code Review (4 findings fixed) → Security Review →
Acceptance Test → Finalizer)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-13 23:20:42 +02:00

254 lines
7.9 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<?php
namespace MintyPHP\Support\Search;
use MintyPHP\Service\Docs\DocsCatalogService;
use MintyPHP\Service\Ui\HotkeyService;
class SearchSpecialResourceProvider
{
private static function hotkeyEntries(): array
{
$rows = [];
foreach (HotkeyService::list() as $row) {
$actionKey = trim($row['action_key']);
$mac = trim($row['mac']);
$win = trim($row['win']);
if ($actionKey === '' || $mac === '' || $win === '') {
continue;
}
$rows[] = [
'label' => t($actionKey),
'mac' => $mac,
'win' => $win,
];
}
return $rows;
}
// Two-tier match: first checks against the hotkey service's keyword list (e.g. "shortcut", "keyboard"),
// then falls back to substring matching against individual entry labels/key combos.
private static function matchesHotkeyQueryWith(string $query, array $entries): bool
{
$query = trim(mb_strtolower($query));
if ($query === '') {
return false;
}
$tokens = preg_split('/[^a-z0-9]+/i', $query);
$tokens = is_array($tokens) ? array_values(array_filter($tokens, static fn ($part): bool => $part !== '')) : [];
foreach (HotkeyService::searchKeywords() as $keywordRaw) {
$keyword = trim(mb_strtolower((string) $keywordRaw));
if ($keyword === '') {
continue;
}
if (in_array($keyword, $tokens, true)) {
return true;
}
foreach ($tokens as $token) {
if (strlen($token) >= 3 && str_starts_with($keyword, $token)) {
return true;
}
}
if (strlen($query) >= 3 && str_starts_with($keyword, $query)) {
return true;
}
}
foreach ($entries as $entry) {
$label = mb_strtolower((string) ($entry['label'] ?? ''));
$mac = mb_strtolower((string) ($entry['mac'] ?? ''));
$win = mb_strtolower((string) ($entry['win'] ?? ''));
if (($label !== '' && str_contains($label, $query))
|| ($mac !== '' && str_contains($mac, $query))
|| ($win !== '' && str_contains($win, $query))) {
return true;
}
}
return false;
}
private static function filterHotkeyEntries(string $query): array
{
$needle = trim(mb_strtolower($query));
$entries = self::hotkeyEntries();
if (!self::matchesHotkeyQueryWith($query, $entries)) {
return [];
}
if ($needle === '') {
return $entries;
}
$filtered = [];
foreach ($entries as $entry) {
$label = mb_strtolower((string) ($entry['label'] ?? ''));
$mac = mb_strtolower((string) ($entry['mac'] ?? ''));
$win = mb_strtolower((string) ($entry['win'] ?? ''));
if (str_contains($label, $needle) || str_contains($mac, $needle) || str_contains($win, $needle)) {
$filtered[] = $entry;
}
}
return $filtered;
}
public static function hotkeyPreviewResource(string $query): ?array
{
$entries = self::filterHotkeyEntries($query);
if ($entries === []) {
return null;
}
$items = [];
foreach ($entries as $entry) {
$items[] = [
'label' => trim((string) ($entry['label'] ?? '') . ' - ' . (string) ($entry['mac'] ?? '') . ' / ' . (string) ($entry['win'] ?? '')),
'url' => lurl('help/hotkeys'),
];
}
return [
'key' => 'hotkeys',
'label' => t('Keyboard shortcuts'),
'count' => count($items),
'url' => lurl('help/hotkeys'),
'items' => array_slice($items, 0, 5),
];
}
public static function hotkeyResultItems(string $query): array
{
$entries = self::filterHotkeyEntries($query);
if ($entries === []) {
return [];
}
$result = [];
foreach ($entries as $entry) {
$result[] = [
'type' => t('Keyboard shortcuts'),
'title' => (string) ($entry['label'] ?? ''),
'description' => trim((string) ($entry['mac'] ?? '') . ' / ' . (string) ($entry['win'] ?? '')),
'url' => lurl('help/hotkeys'),
'image' => '',
'icon' => 'bi-keyboard',
];
}
return $result;
}
private static function docsResultUrl(string $slug, string $anchor = ''): string
{
$url = lurl('admin/docs/' . $slug);
if ($anchor !== '') {
$url .= '#' . $anchor;
}
return $url;
}
public static function docsPreviewResource(string $query): ?array
{
$matches = DocsCatalogService::search($query);
if ($matches === []) {
return null;
}
$seenDocs = [];
$previewItems = [];
foreach ($matches as $match) {
$slug = trim((string) $match['slug']);
if ($slug === '') {
continue;
}
$seenDocs[$slug] = true;
if (isset($previewItems[$slug])) {
continue;
}
$title = trim((string) $match['title']);
if ($title === '') {
$title = $slug;
}
$section = trim((string) $match['section']);
$label = $title;
if ($section !== '' && mb_strtolower($section) !== mb_strtolower($title)) {
$label .= ' - ' . $section;
}
$previewItems[$slug] = [
'label' => $label,
'url' => self::docsResultUrl($slug, (string) $match['anchor']),
];
}
return [
'key' => 'docs',
'label' => t('Documentation'),
'count' => count($seenDocs),
'url' => lurl('admin/docs/' . DocsCatalogService::defaultSlug()) . '?search=' . urlencode($query),
'items' => array_values(array_slice($previewItems, 0, 5)),
];
}
public static function docsResultItems(string $query): array
{
$matches = DocsCatalogService::search($query);
if ($matches === []) {
return [];
}
$items = [];
foreach ($matches as $match) {
$slug = trim((string) $match['slug']);
$title = trim((string) $match['title']);
if ($slug === '' || $title === '') {
continue;
}
$section = trim((string) $match['section']);
$snippet = trim((string) $match['snippet']);
$descriptionParts = [];
if ($section !== '' && mb_strtolower($section) !== mb_strtolower($title)) {
$descriptionParts[] = $section;
}
if ($snippet !== '') {
$descriptionParts[] = $snippet;
}
// Map raw doc scores (01000+) to coarser search_score buckets for result ranking.
$rawScore = (int) $match['score'];
$searchScore = match (true) {
$rawScore >= 1000 => 400,
$rawScore >= 900 => 360,
$rawScore >= 800 => 330,
$rawScore >= 700 => 300,
$rawScore >= 650 => 260,
$rawScore >= 600 => 230,
$rawScore >= 500 => 200,
default => 150,
};
$items[] = [
'type' => t('Documentation'),
'title' => $title,
'description' => implode(' - ', $descriptionParts),
'url' => self::docsResultUrl($slug, (string) $match['anchor']),
'image' => '',
'icon' => 'bi-journal-text',
'search_score' => $searchScore,
];
}
return $items;
}
}