mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-14 20:01:09 +02:00
refactor(shared-ui): extract search-core for highlight + debounce
Both QuickInputBar (InputBar.svelte), CommandBar.svelte, and GlobalSpotlight were duplicating syntax highlighting and the 150ms search debounce. Pull these into a new `packages/shared-ui/src/search-core/` module so the two input surfaces stay in sync on feel and matching rules. - search-core/highlight.ts — HighlightPattern type, locale-aware getHighlightPatterns(), and the shared highlightText() (HTML-escape + span wrap). Patterns were previously in quick-input/highlightPatterns.ts + inline in CommandBar.svelte. - search-core/config.ts — SEARCH_DEBOUNCE_MS = 150. Used from InputBar, CommandBar, GlobalSpotlight, and apps/mana web SearchEngine. - quick-input/highlightPatterns.ts + types.ts become thin back-compat re-exports. - Public surface: @mana/shared-ui now exports getHighlightPatterns, highlightText, SEARCH_DEBOUNCE_MS, and the HighlightPattern type. No UX change. UIs still live in their own files (per earlier split recommendation: shared backend, separate surfaces). Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
ad1659f036
commit
24eb8b3b7f
10 changed files with 161 additions and 204 deletions
|
|
@ -6,8 +6,7 @@
|
|||
|
||||
import { SearchRegistry } from './registry';
|
||||
import type { GroupedSearchResults } from './types';
|
||||
|
||||
const DEBOUNCE_MS = 150;
|
||||
import { SEARCH_DEBOUNCE_MS as DEBOUNCE_MS } from '@mana/shared-ui';
|
||||
|
||||
export function createSearchEngine() {
|
||||
const registry = new SearchRegistry();
|
||||
|
|
|
|||
|
|
@ -2,47 +2,9 @@
|
|||
import { goto } from '$app/navigation';
|
||||
import type { CommandBarItem, QuickAction, CreatePreview } from './CommandBar.types';
|
||||
import { Heart, MagnifyingGlass, Plus } from '@mana/shared-icons';
|
||||
import { getHighlightPatterns, highlightText, SEARCH_DEBOUNCE_MS } from '../search-core';
|
||||
|
||||
// Syntax highlighting patterns for command keywords
|
||||
interface HighlightPattern {
|
||||
pattern: RegExp;
|
||||
className: string;
|
||||
}
|
||||
|
||||
const HIGHLIGHT_PATTERNS: HighlightPattern[] = [
|
||||
// Priority keywords (Todo) - with specific colors per level
|
||||
{ pattern: /(!{3,}|!?dringend)\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /(!{2}|!?wichtig)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /!?normal\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
|
||||
// Tags
|
||||
{ pattern: /#\w+/g, className: 'hl-tag' },
|
||||
// Projects/Calendars/Companies (@reference)
|
||||
{ pattern: /@\w+/g, className: 'hl-reference' },
|
||||
// Date keywords
|
||||
{
|
||||
pattern:
|
||||
/\b(heute|morgen|übermorgen|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|nächsten?\s+\w+|in\s+\d+\s+tagen?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
// Time patterns
|
||||
{ pattern: /\b(\d{1,2}:\d{2}|um\s+\d{1,2}(\s*uhr)?|\d{1,2}\s*uhr)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
function highlightText(text: string): string {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text;
|
||||
// Escape HTML first
|
||||
result = result.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Apply highlights (process in order, avoiding double-highlighting)
|
||||
for (const { pattern, className } of HIGHLIGHT_PATTERNS) {
|
||||
result = result.replace(pattern, (match) => `<span class="${className}">${match}</span>`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
const HIGHLIGHT_PATTERNS = getHighlightPatterns('de');
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -89,7 +51,7 @@
|
|||
);
|
||||
|
||||
// Highlighted text for overlay
|
||||
let highlightedQuery = $derived(highlightText(searchQuery));
|
||||
let highlightedQuery = $derived(highlightText(searchQuery, HIGHLIGHT_PATTERNS));
|
||||
|
||||
// Check if create option is selected (it's always first when available)
|
||||
let isCreateSelected = $derived(selectedIndex === 0 && createPreview !== null);
|
||||
|
|
@ -126,7 +88,7 @@
|
|||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}, 150);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async function handleCreate() {
|
||||
|
|
|
|||
|
|
@ -185,13 +185,11 @@ export {
|
|||
createInputBarSettingsStore,
|
||||
getInputBarSettingsStore,
|
||||
} from './quick-input';
|
||||
export type {
|
||||
QuickInputItem,
|
||||
QuickAction,
|
||||
CreatePreview,
|
||||
HighlightPattern,
|
||||
InputBarSettings,
|
||||
} from './quick-input';
|
||||
export type { QuickInputItem, QuickAction, CreatePreview, InputBarSettings } from './quick-input';
|
||||
|
||||
// Shared search/command core — highlight patterns, debounce, common helpers.
|
||||
export { getHighlightPatterns, highlightText, SEARCH_DEBOUNCE_MS } from './search-core';
|
||||
export type { HighlightPattern } from './search-core';
|
||||
|
||||
// Pages
|
||||
export { default as AppsPage } from './pages/AppsPage.svelte';
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@
|
|||
ContentSearcher,
|
||||
} from './types';
|
||||
import { createAppNavigationStore } from './appNavigationStore.svelte';
|
||||
import { SEARCH_DEBOUNCE_MS } from '../search-core';
|
||||
|
||||
interface Props {
|
||||
open: boolean;
|
||||
|
|
@ -160,7 +161,7 @@
|
|||
contentLoading = false;
|
||||
}
|
||||
}
|
||||
}, 150);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
});
|
||||
|
||||
// Group results by category for display
|
||||
|
|
|
|||
|
|
@ -1,29 +1,15 @@
|
|||
<script lang="ts">
|
||||
import { onMount } from 'svelte';
|
||||
import { slide } from 'svelte/transition';
|
||||
import type { QuickInputItem, CreatePreview, HighlightPattern } from './types';
|
||||
import type { QuickInputItem, CreatePreview } from './types';
|
||||
import InputBarContextMenu from './InputBarContextMenu.svelte';
|
||||
import { getInputBarSettingsStore } from './inputBarSettings.svelte';
|
||||
import { getHighlightPatterns } from './highlightPatterns';
|
||||
import { getHighlightPatterns, highlightText, SEARCH_DEBOUNCE_MS } from '../search-core';
|
||||
import type { HighlightPattern } from '../search-core';
|
||||
|
||||
// Settings store
|
||||
const settingsStore = getInputBarSettingsStore();
|
||||
|
||||
function highlightText(text: string, patterns: HighlightPattern[]): string {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text;
|
||||
// Escape HTML first
|
||||
result = result.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
// Apply highlights (process in order, avoiding double-highlighting)
|
||||
for (const { pattern, className } of patterns) {
|
||||
result = result.replace(pattern, (match) => `<span class="${className}">${match}</span>`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
interface DefaultOption {
|
||||
id: string;
|
||||
label: string;
|
||||
|
|
@ -242,7 +228,7 @@
|
|||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
}, 150);
|
||||
}, SEARCH_DEBOUNCE_MS);
|
||||
}
|
||||
|
||||
async function triggerDeferredSearch() {
|
||||
|
|
@ -669,25 +655,6 @@
|
|||
}
|
||||
}
|
||||
|
||||
.app-icon.success-icon {
|
||||
color: hsl(var(--color-success, 142 71% 45%));
|
||||
animation: success-check 0.4s ease-out;
|
||||
}
|
||||
|
||||
@keyframes success-check {
|
||||
0% {
|
||||
transform: scale(0.5);
|
||||
opacity: 0;
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.2);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.left-action,
|
||||
.right-action {
|
||||
display: flex;
|
||||
|
|
@ -696,13 +663,6 @@
|
|||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
color: hsl(var(--color-muted-foreground));
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
position: relative;
|
||||
flex: 1;
|
||||
|
|
|
|||
|
|
@ -1,104 +1,3 @@
|
|||
import type { HighlightPattern } from './types';
|
||||
|
||||
/** Shared patterns that work across all locales (symbols, not words) */
|
||||
const UNIVERSAL_PATTERNS: HighlightPattern[] = [
|
||||
// Priority shortcuts: !!! = urgent, !! = high
|
||||
{ pattern: /!{3,}/g, className: 'hl-priority-urgent' },
|
||||
{ pattern: /!{2}/g, className: 'hl-priority-high' },
|
||||
// Tags
|
||||
{ pattern: /#\w+/g, className: 'hl-tag' },
|
||||
// Projects/Calendars/Companies (@reference)
|
||||
{ pattern: /@\w+/g, className: 'hl-reference' },
|
||||
// Time patterns (universal formats)
|
||||
{ pattern: /\b\d{1,2}:\d{2}\b/g, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** German date/priority keywords */
|
||||
const DE_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?dringend\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?wichtig\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?normal\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(heute|morgen|übermorgen|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|nächsten?\s+\w+|in\s+\d+\s+tagen?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(um\s+\d{1,2}(\s*uhr)?|\d{1,2}\s*uhr)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** English date/priority keywords */
|
||||
const EN_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important|high)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medium)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(low|later)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(today|tomorrow|yesterday|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+|in\s+\d+\s+days?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(at\s+\d{1,2}(:\d{2})?\s*(am|pm)?)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** French date/priority keywords */
|
||||
const FR_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent[e]?\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important[e]?|haut[e]?)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal[e]?|moyen(?:ne)?)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bas(?:se)?|plus\s+tard)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(aujourd'hui|demain|après-demain|hier|lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche|prochain[e]?\s+\w+|dans\s+\d+\s+jours?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(à\s+\d{1,2}[h:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Italian date/priority keywords */
|
||||
const IT_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normale|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(basso|dopo)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(oggi|domani|dopodomani|ieri|luned[ìi]|marted[ìi]|mercoled[ìi]|gioved[ìi]|venerd[ìi]|sabato|domenica|prossim[oa]\s+\w+|tra\s+\d+\s+giorn[oi])\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(alle\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Spanish date/priority keywords */
|
||||
const ES_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bajo|luego)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(hoy|mañana|pasado\s+mañana|ayer|lunes|martes|miércoles|jueves|viernes|sábado|domingo|próxim[oa]\s+\w+|en\s+\d+\s+d[ií]as?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(a\s+las?\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
const LOCALE_PATTERNS: Record<string, HighlightPattern[]> = {
|
||||
de: DE_PATTERNS,
|
||||
en: EN_PATTERNS,
|
||||
fr: FR_PATTERNS,
|
||||
it: IT_PATTERNS,
|
||||
es: ES_PATTERNS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Get highlight patterns for a given locale.
|
||||
* Returns universal patterns + locale-specific patterns.
|
||||
* Falls back to German if locale is unknown.
|
||||
*/
|
||||
export function getHighlightPatterns(locale = 'de'): HighlightPattern[] {
|
||||
// Normalize locale (e.g., "en-US" -> "en")
|
||||
const lang = locale.split('-')[0].toLowerCase();
|
||||
const localeSpecific = LOCALE_PATTERNS[lang] || LOCALE_PATTERNS['de'];
|
||||
return [...UNIVERSAL_PATTERNS, ...localeSpecific];
|
||||
}
|
||||
// Re-export from the shared search-core for back-compat.
|
||||
// Prefer importing from `../search-core` (or `@mana/shared-ui`) in new code.
|
||||
export { getHighlightPatterns } from '../search-core/highlight';
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ export interface CreatePreview {
|
|||
subtitle: string;
|
||||
}
|
||||
|
||||
export interface HighlightPattern {
|
||||
pattern: RegExp;
|
||||
className: string;
|
||||
}
|
||||
// HighlightPattern lives in ../search-core/highlight; re-exported here for
|
||||
// back-compat with older imports.
|
||||
export type { HighlightPattern } from '../search-core/highlight';
|
||||
|
|
|
|||
9
packages/shared-ui/src/search-core/config.ts
Normal file
9
packages/shared-ui/src/search-core/config.ts
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
/**
|
||||
* Shared configuration for search/command input surfaces.
|
||||
*
|
||||
* Both QuickInputBar and GlobalSpotlight debounce their input with the same
|
||||
* 150ms window — keep them in sync so the feel of the two surfaces doesn't
|
||||
* diverge.
|
||||
*/
|
||||
|
||||
export const SEARCH_DEBOUNCE_MS = 150;
|
||||
127
packages/shared-ui/src/search-core/highlight.ts
Normal file
127
packages/shared-ui/src/search-core/highlight.ts
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
/**
|
||||
* Syntax-highlight patterns for search/command input surfaces
|
||||
* (QuickInputBar, CommandBar). Patterns are locale-aware for date/priority
|
||||
* keywords; universal patterns (tags, @references, times) apply to every locale.
|
||||
*/
|
||||
|
||||
export interface HighlightPattern {
|
||||
pattern: RegExp;
|
||||
className: string;
|
||||
}
|
||||
|
||||
/** Shared patterns that work across all locales (symbols, not words) */
|
||||
const UNIVERSAL_PATTERNS: HighlightPattern[] = [
|
||||
// Priority shortcuts: !!! = urgent, !! = high
|
||||
{ pattern: /!{3,}/g, className: 'hl-priority-urgent' },
|
||||
{ pattern: /!{2}/g, className: 'hl-priority-high' },
|
||||
// Tags
|
||||
{ pattern: /#\w+/g, className: 'hl-tag' },
|
||||
// Projects/Calendars/Companies (@reference)
|
||||
{ pattern: /@\w+/g, className: 'hl-reference' },
|
||||
// Time patterns (universal formats)
|
||||
{ pattern: /\b\d{1,2}:\d{2}\b/g, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** German date/priority keywords */
|
||||
const DE_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?dringend\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?wichtig\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?normal\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?sp[aä]ter\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(heute|morgen|übermorgen|montag|dienstag|mittwoch|donnerstag|freitag|samstag|sonntag|nächsten?\s+\w+|in\s+\d+\s+tagen?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(um\s+\d{1,2}(\s*uhr)?|\d{1,2}\s*uhr)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** English date/priority keywords */
|
||||
const EN_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important|high)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medium)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(low|later)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(today|tomorrow|yesterday|monday|tuesday|wednesday|thursday|friday|saturday|sunday|next\s+\w+|in\s+\d+\s+days?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(at\s+\d{1,2}(:\d{2})?\s*(am|pm)?)\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** French date/priority keywords */
|
||||
const FR_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgent[e]?\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(important[e]?|haut[e]?)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal[e]?|moyen(?:ne)?)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bas(?:se)?|plus\s+tard)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(aujourd'hui|demain|après-demain|hier|lundi|mardi|mercredi|jeudi|vendredi|samedi|dimanche|prochain[e]?\s+\w+|dans\s+\d+\s+jours?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(à\s+\d{1,2}[h:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Italian date/priority keywords */
|
||||
const IT_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normale|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(basso|dopo)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(oggi|domani|dopodomani|ieri|luned[ìi]|marted[ìi]|mercoled[ìi]|gioved[ìi]|venerd[ìi]|sabato|domenica|prossim[oa]\s+\w+|tra\s+\d+\s+giorn[oi])\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(alle\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
/** Spanish date/priority keywords */
|
||||
const ES_PATTERNS: HighlightPattern[] = [
|
||||
{ pattern: /\b!?urgente\b/gi, className: 'hl-priority-urgent' },
|
||||
{ pattern: /\b!?(importante|alto)\b/gi, className: 'hl-priority-high' },
|
||||
{ pattern: /\b!?(normal|medio)\b/gi, className: 'hl-priority-medium' },
|
||||
{ pattern: /\b!?(bajo|luego)\b/gi, className: 'hl-priority-low' },
|
||||
{
|
||||
pattern:
|
||||
/\b(hoy|mañana|pasado\s+mañana|ayer|lunes|martes|miércoles|jueves|viernes|sábado|domingo|próxim[oa]\s+\w+|en\s+\d+\s+d[ií]as?)\b/gi,
|
||||
className: 'hl-date',
|
||||
},
|
||||
{ pattern: /\b(a\s+las?\s+\d{1,2}[.:]\d{0,2})\b/gi, className: 'hl-time' },
|
||||
];
|
||||
|
||||
const LOCALE_PATTERNS: Record<string, HighlightPattern[]> = {
|
||||
de: DE_PATTERNS,
|
||||
en: EN_PATTERNS,
|
||||
fr: FR_PATTERNS,
|
||||
it: IT_PATTERNS,
|
||||
es: ES_PATTERNS,
|
||||
};
|
||||
|
||||
/**
|
||||
* Returns universal + locale-specific highlight patterns. Falls back to German
|
||||
* if the locale is unknown.
|
||||
*/
|
||||
export function getHighlightPatterns(locale = 'de'): HighlightPattern[] {
|
||||
const lang = locale.split('-')[0].toLowerCase();
|
||||
const localeSpecific = LOCALE_PATTERNS[lang] || LOCALE_PATTERNS['de'];
|
||||
return [...UNIVERSAL_PATTERNS, ...localeSpecific];
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps each match of the given patterns in `<span class="{className}">…</span>`.
|
||||
* HTML is escaped first to make the output safe for `{@html}` rendering.
|
||||
*/
|
||||
export function highlightText(text: string, patterns: HighlightPattern[]): string {
|
||||
if (!text) return '';
|
||||
|
||||
let result = text.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||
|
||||
for (const { pattern, className } of patterns) {
|
||||
result = result.replace(pattern, (match) => `<span class="${className}">${match}</span>`);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
3
packages/shared-ui/src/search-core/index.ts
Normal file
3
packages/shared-ui/src/search-core/index.ts
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
export { getHighlightPatterns, highlightText } from './highlight';
|
||||
export type { HighlightPattern } from './highlight';
|
||||
export { SEARCH_DEBOUNCE_MS } from './config';
|
||||
Loading…
Add table
Add a link
Reference in a new issue