12 changed files with 1050 additions and 77 deletions
@ -0,0 +1,118 @@ |
|||||||
|
import { Info } from 'lucide-react' |
||||||
|
import { Button } from '@/components/ui/button' |
||||||
|
import { HoverCard, HoverCardContent, HoverCardTrigger } from '@/components/ui/hover-card' |
||||||
|
import { |
||||||
|
Drawer, |
||||||
|
DrawerClose, |
||||||
|
DrawerContent, |
||||||
|
DrawerDescription, |
||||||
|
DrawerHeader, |
||||||
|
DrawerTitle, |
||||||
|
DrawerTrigger |
||||||
|
} from '@/components/ui/drawer' |
||||||
|
import { useScreenSize } from '@/providers/ScreenSizeProvider' |
||||||
|
import { cn } from '@/lib/utils' |
||||||
|
|
||||||
|
export default function SearchInfo() { |
||||||
|
const { isSmallScreen } = useScreenSize() |
||||||
|
|
||||||
|
const searchInfoContent = ( |
||||||
|
<div className="space-y-3"> |
||||||
|
<div> |
||||||
|
<h4 className="font-semibold mb-2">Advanced Search Parameters</h4> |
||||||
|
<div className="space-y-2 text-sm"> |
||||||
|
<div> |
||||||
|
<strong>Plain text:</strong> Searches by d-tag for replaceable events (normalized, hyphenated) |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<strong>Date ranges:</strong> |
||||||
|
<ul className="ml-4 mt-1 space-y-1 list-disc"> |
||||||
|
<li><code className="text-xs">YYYY-MM-DD to YYYY-MM-DD</code> - Date range (e.g., <code className="text-xs">2025-10-23 to 2025-10-30</code>)</li> |
||||||
|
<li><code className="text-xs">from:YYYY-MM-DD</code> - Events from this date</li> |
||||||
|
<li><code className="text-xs">to:YYYY-MM-DD</code> - Events until this date</li> |
||||||
|
<li><code className="text-xs">before:YYYY-MM-DD</code> - Events before this date</li> |
||||||
|
<li><code className="text-xs">after:YYYY-MM-DD</code> - Events after this date</li> |
||||||
|
<li>Supports 2-digit years (e.g., <code className="text-xs">25-10-23</code> = <code className="text-xs">2025-10-23</code>)</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div> |
||||||
|
<strong>Metadata fields:</strong> |
||||||
|
<ul className="ml-4 mt-1 space-y-1 list-disc"> |
||||||
|
<li><code className="text-xs">title:"text"</code> or <code className="text-xs">title:text</code> - Search in title tag</li> |
||||||
|
<li><code className="text-xs">subject:"text"</code> or <code className="text-xs">subject:text</code> - Search in subject tag</li> |
||||||
|
<li><code className="text-xs">description:"text"</code> - Search in description tag</li> |
||||||
|
<li><code className="text-xs">author:"name"</code> - Search by author tag (not pubkey)</li> |
||||||
|
<li><code className="text-xs">pubkey:npub...</code>, <code className="text-xs">pubkey:hex</code>, <code className="text-xs">pubkey:nprofile...</code>, or <code className="text-xs">pubkey:user@domain.com</code> - Filter by pubkey (accepts npub, nprofile, hex, or NIP-05)</li> |
||||||
|
<li><code className="text-xs">events:hex</code>, <code className="text-xs">events:note1...</code>, <code className="text-xs">events:nevent1...</code>, or <code className="text-xs">events:naddr1...</code> - Filter by specific events (accepts hex, note, nevent, or naddr)</li> |
||||||
|
<li><code className="text-xs">type:value</code> - Filter by type tag</li> |
||||||
|
<li><code className="text-xs">kind:30023</code> - Filter by event kind (e.g., 1=notes, 30023=articles, 30817/30818=wiki)</li> |
||||||
|
<li>Multiple values supported: <code className="text-xs">author:Aristotle,Plato</code> or <code className="text-xs">kind:30023,2018</code></li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
<div className="pt-2 border-t"> |
||||||
|
<p className="text-xs text-muted-foreground"> |
||||||
|
<strong>Examples:</strong> |
||||||
|
</p> |
||||||
|
<ul className="ml-4 mt-1 space-y-1 list-disc text-xs text-muted-foreground"> |
||||||
|
<li><code>jumble search</code> → searches d-tag</li> |
||||||
|
<li><code>title:"My Article" from:2024-01-01</code></li> |
||||||
|
<li><code>author:"John Doe" type:wiki</code></li> |
||||||
|
<li><code>2025-10-23 to 2025-10-30</code> → date range</li> |
||||||
|
</ul> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
) |
||||||
|
|
||||||
|
if (isSmallScreen) { |
||||||
|
return ( |
||||||
|
<Drawer> |
||||||
|
<DrawerTrigger asChild> |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
size="icon" |
||||||
|
className={cn("h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md relative z-10")} |
||||||
|
title="Search help" |
||||||
|
> |
||||||
|
<Info className="h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
</DrawerTrigger> |
||||||
|
<DrawerContent> |
||||||
|
<DrawerHeader> |
||||||
|
<DrawerTitle>Advanced Search Help</DrawerTitle> |
||||||
|
<DrawerDescription> |
||||||
|
Learn about available search parameters |
||||||
|
</DrawerDescription> |
||||||
|
</DrawerHeader> |
||||||
|
<div className="px-4 pb-4 max-h-[60vh] overflow-y-auto"> |
||||||
|
{searchInfoContent} |
||||||
|
</div> |
||||||
|
<DrawerClose asChild> |
||||||
|
<Button variant="outline" className="m-4">Close</Button> |
||||||
|
</DrawerClose> |
||||||
|
</DrawerContent> |
||||||
|
</Drawer> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
|
return ( |
||||||
|
<HoverCard> |
||||||
|
<HoverCardTrigger asChild> |
||||||
|
<Button |
||||||
|
variant="ghost" |
||||||
|
size="icon" |
||||||
|
className={cn("h-9 w-9 shrink-0 text-muted-foreground hover:text-foreground border border-border/50 hover:border-border rounded-md relative z-10")} |
||||||
|
title="Search help" |
||||||
|
> |
||||||
|
<Info className="h-4 w-4" /> |
||||||
|
</Button> |
||||||
|
</HoverCardTrigger> |
||||||
|
<HoverCardContent className="w-96 max-h-[80vh] overflow-y-auto" side="left" align="start"> |
||||||
|
<h3 className="font-semibold mb-3">Advanced Search Help</h3> |
||||||
|
{searchInfoContent} |
||||||
|
</HoverCardContent> |
||||||
|
</HoverCard> |
||||||
|
) |
||||||
|
} |
||||||
|
|
||||||
@ -0,0 +1,410 @@ |
|||||||
|
/** |
||||||
|
* Advanced search parser for Nostr events |
||||||
|
* Supports multiple search parameters: |
||||||
|
* - Date ranges: YYYY-MM-DD to YYYY-MM-DD, from:YYYY-MM-DD, to:YYYY-MM-DD, before:YYYY-MM-DD, after:YYYY-MM-DD |
||||||
|
* - Title: title:"text" or title:text |
||||||
|
* - Subject: subject:"text" or subject:text |
||||||
|
* - Description: description:"text" or description:text |
||||||
|
* - Author: author:"name" (author tag, not pubkey) |
||||||
|
* - Pubkey: pubkey:npub... or pubkey:hex... |
||||||
|
* - Type: type:value |
||||||
|
* - Kind: kind:30023 (filter by event kind) |
||||||
|
* - Plain text: becomes d-tag search for replaceable events |
||||||
|
*/ |
||||||
|
|
||||||
|
export interface AdvancedSearchParams { |
||||||
|
dtag?: string |
||||||
|
title?: string | string[] |
||||||
|
subject?: string | string[] |
||||||
|
description?: string | string[] |
||||||
|
author?: string | string[] |
||||||
|
pubkey?: string | string[] // Accepts: hex, npub, nprofile, or NIP-05
|
||||||
|
events?: string | string[] // Accepts: hex event ID, note, nevent, naddr
|
||||||
|
type?: string | string[] |
||||||
|
from?: string // YYYY-MM-DD
|
||||||
|
to?: string // YYYY-MM-DD
|
||||||
|
before?: string // YYYY-MM-DD
|
||||||
|
after?: string // YYYY-MM-DD
|
||||||
|
kinds?: number[] |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalize search term to d-tag format (lowercase, hyphenated) |
||||||
|
*/ |
||||||
|
export function normalizeToDTag(term: string): string { |
||||||
|
return term |
||||||
|
.toLowerCase() |
||||||
|
.trim() |
||||||
|
.replace(/[^\w\s-]/g, '') // Remove special characters
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||||
|
.replace(/-+/g, '-') // Replace multiple hyphens with single hyphen
|
||||||
|
.replace(/^-|-$/g, '') // Remove leading/trailing hyphens
|
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Normalize date to YYYY-MM-DD format |
||||||
|
* Supports both 2-digit (YY) and 4-digit (YYYY) years |
||||||
|
*/ |
||||||
|
function normalizeDate(dateStr: string): string { |
||||||
|
const parts = dateStr.split('-') |
||||||
|
if (parts.length !== 3) return dateStr |
||||||
|
|
||||||
|
let year = parts[0] |
||||||
|
const month = parts[1] |
||||||
|
const day = parts[2] |
||||||
|
|
||||||
|
// Convert 2-digit year to 4-digit
|
||||||
|
if (year.length === 2) { |
||||||
|
const yearNum = parseInt(year) |
||||||
|
// Assume years 00-30 are 2000-2030, years 31-99 are 1931-1999
|
||||||
|
year = yearNum <= 30 ? `20${year.padStart(2, '0')}` : `19${year}` |
||||||
|
} |
||||||
|
|
||||||
|
return `${year}-${month}-${day}` |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Parse advanced search query |
||||||
|
*/ |
||||||
|
export function parseAdvancedSearch(query: string): AdvancedSearchParams { |
||||||
|
// Normalize the query: trim, normalize whitespace, handle multiple spaces
|
||||||
|
const normalizedQuery = query |
||||||
|
.trim() |
||||||
|
.replace(/\s+/g, ' ') // Replace multiple whitespace with single space
|
||||||
|
.replace(/\s*,\s*/g, ',') // Normalize spaces around commas
|
||||||
|
.replace(/\s*:\s*/g, ':') // Normalize spaces around colons
|
||||||
|
.replace(/\s+to\s+/gi, ' to ') // Normalize "to" in date ranges
|
||||||
|
|
||||||
|
const params: AdvancedSearchParams = {} |
||||||
|
|
||||||
|
// Regular expressions for different parameter types
|
||||||
|
// Support both 4-digit (YYYY) and 2-digit (YY) years, date ranges (DATE to DATE)
|
||||||
|
const dateRangePattern = /(\d{2,4}-\d{2}-\d{2})\s+to\s+(\d{2,4}-\d{2}-\d{2})/gi |
||||||
|
const datePattern = /(?:from|to|before|after):(\d{2,4}-\d{2}-\d{2})/gi |
||||||
|
const quotedPattern = /(title|subject|description|author|type|pubkey|events):"([^"]+)"/gi |
||||||
|
const unquotedPattern = /(title|subject|description|author|pubkey|type|kind|events):([^\s]+)/gi |
||||||
|
|
||||||
|
// Pattern to detect bare nip19 IDs (nevent, note, naddr) or hex event IDs
|
||||||
|
// These start with the prefix and are base32 encoded (use word boundary to avoid partial matches)
|
||||||
|
const bareEventIdPattern = /\b(nevent1|note1|naddr1)[a-z0-9]{0,58}\b/gi |
||||||
|
const hexEventIdPattern = /\b[a-f0-9]{64}\b/i |
||||||
|
|
||||||
|
// Pattern to detect bare pubkey IDs (npub, nprofile) or hex pubkeys
|
||||||
|
const barePubkeyIdPattern = /\b(nprofile1|npub1)[a-z0-9]{0,58}\b/gi |
||||||
|
const nip05Pattern = /\b[a-zA-Z0-9._-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}\b/gi |
||||||
|
|
||||||
|
// Extract quoted parameters
|
||||||
|
let match |
||||||
|
let lastIndex = 0 |
||||||
|
const usedIndices: number[] = [] |
||||||
|
const detectedEventIds: { id: string; start: number; end: number }[] = [] |
||||||
|
const detectedPubkeyIds: { id: string; start: number; end: number }[] = [] |
||||||
|
|
||||||
|
// First, detect bare event IDs (nevent, note, naddr) in the normalized query
|
||||||
|
bareEventIdPattern.lastIndex = 0 |
||||||
|
while ((match = bareEventIdPattern.exec(normalizedQuery)) !== null) { |
||||||
|
const id = match[0] |
||||||
|
const start = match.index |
||||||
|
const end = start + id.length |
||||||
|
detectedEventIds.push({ id, start, end }) |
||||||
|
usedIndices.push(start, end) |
||||||
|
} |
||||||
|
|
||||||
|
// Detect bare pubkey IDs (npub, nprofile)
|
||||||
|
barePubkeyIdPattern.lastIndex = 0 |
||||||
|
while ((match = barePubkeyIdPattern.exec(normalizedQuery)) !== null) { |
||||||
|
const id = match[0] |
||||||
|
const start = match.index |
||||||
|
const end = start + id.length |
||||||
|
detectedPubkeyIds.push({ id, start, end }) |
||||||
|
usedIndices.push(start, end) |
||||||
|
} |
||||||
|
|
||||||
|
// Detect NIP-05 identifiers
|
||||||
|
nip05Pattern.lastIndex = 0 |
||||||
|
while ((match = nip05Pattern.exec(normalizedQuery)) !== null) { |
||||||
|
const id = match[0] |
||||||
|
const start = match.index |
||||||
|
const end = start + id.length |
||||||
|
|
||||||
|
// Skip if already used by a parameter pattern or other detected IDs
|
||||||
|
if (!usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
detectedPubkeyIds.push({ id, start, end }) |
||||||
|
usedIndices.push(start, end) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Check for hex IDs (64 character hex string) - could be either event or pubkey
|
||||||
|
// We'll treat them as events by default, but they might be interpreted differently in context
|
||||||
|
hexEventIdPattern.lastIndex = 0 |
||||||
|
while ((match = hexEventIdPattern.exec(normalizedQuery)) !== null) { |
||||||
|
const id = match[0] |
||||||
|
const start = match.index |
||||||
|
const end = start + id.length |
||||||
|
|
||||||
|
// Only add if not already in a detected ID range
|
||||||
|
if (!usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
// Default to treating as event ID (most common case for hex IDs in Nostr)
|
||||||
|
detectedEventIds.push({ id, start, end }) |
||||||
|
usedIndices.push(start, end) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Helper function to parse comma-separated values
|
||||||
|
const parseValues = (value: string): string[] => { |
||||||
|
return value.split(',').map(v => v.trim()).filter(v => v.length > 0) |
||||||
|
} |
||||||
|
|
||||||
|
// Process quoted strings first (they can contain spaces)
|
||||||
|
while ((match = quotedPattern.exec(normalizedQuery)) !== null) { |
||||||
|
const param = match[1].toLowerCase() |
||||||
|
const value = match[2] |
||||||
|
const start = match.index |
||||||
|
const end = start + match[0].length |
||||||
|
|
||||||
|
usedIndices.push(start, end) |
||||||
|
lastIndex = end |
||||||
|
|
||||||
|
const values = parseValues(value) |
||||||
|
switch (param) { |
||||||
|
case 'title': |
||||||
|
params.title = values.length === 1 ? values[0] : values |
||||||
|
break |
||||||
|
case 'subject': |
||||||
|
params.subject = values.length === 1 ? values[0] : values |
||||||
|
break |
||||||
|
case 'description': |
||||||
|
params.description = values.length === 1 ? values[0] : values |
||||||
|
break |
||||||
|
case 'author': |
||||||
|
params.author = values.length === 1 ? values[0] : values |
||||||
|
break |
||||||
|
case 'type': |
||||||
|
params.type = values.length === 1 ? values[0] : values |
||||||
|
break |
||||||
|
case 'pubkey': |
||||||
|
const pubkeyValues = parseValues(value) |
||||||
|
params.pubkey = pubkeyValues.length === 1 ? pubkeyValues[0] : pubkeyValues |
||||||
|
break |
||||||
|
case 'events': |
||||||
|
const eventValues = parseValues(value) |
||||||
|
params.events = eventValues.length === 1 ? eventValues[0] : eventValues |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Process unquoted parameters
|
||||||
|
while ((match = unquotedPattern.exec(normalizedQuery)) !== null) { |
||||||
|
const start = match.index |
||||||
|
// Skip if already used by quoted pattern
|
||||||
|
if (usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
const param = match[1].toLowerCase() |
||||||
|
const value = match[2] |
||||||
|
const end = start + match[0].length |
||||||
|
|
||||||
|
usedIndices.push(start, end) |
||||||
|
lastIndex = Math.max(lastIndex, end) |
||||||
|
|
||||||
|
switch (param) { |
||||||
|
case 'title': |
||||||
|
if (!params.title) { |
||||||
|
const values = parseValues(value) |
||||||
|
params.title = values.length === 1 ? values[0] : values |
||||||
|
} |
||||||
|
break |
||||||
|
case 'subject': |
||||||
|
if (!params.subject) { |
||||||
|
const values = parseValues(value) |
||||||
|
params.subject = values.length === 1 ? values[0] : values |
||||||
|
} |
||||||
|
break |
||||||
|
case 'description': |
||||||
|
if (!params.description) { |
||||||
|
const values = parseValues(value) |
||||||
|
params.description = values.length === 1 ? values[0] : values |
||||||
|
} |
||||||
|
break |
||||||
|
case 'author': |
||||||
|
if (!params.author) { |
||||||
|
const values = parseValues(value) |
||||||
|
params.author = values.length === 1 ? values[0] : values |
||||||
|
} |
||||||
|
break |
||||||
|
case 'pubkey': |
||||||
|
if (!params.pubkey) { |
||||||
|
const pubkeyValues = parseValues(value) |
||||||
|
params.pubkey = pubkeyValues.length === 1 ? pubkeyValues[0] : pubkeyValues |
||||||
|
} |
||||||
|
break |
||||||
|
case 'events': |
||||||
|
if (!params.events) { |
||||||
|
const eventValues = parseValues(value) |
||||||
|
params.events = eventValues.length === 1 ? eventValues[0] : eventValues |
||||||
|
} |
||||||
|
break |
||||||
|
case 'type': |
||||||
|
if (!params.type) { |
||||||
|
const values = parseValues(value) |
||||||
|
params.type = values.length === 1 ? values[0] : values |
||||||
|
} |
||||||
|
break |
||||||
|
case 'kind': |
||||||
|
const kindValues = parseValues(value) |
||||||
|
params.kinds = params.kinds || [] |
||||||
|
for (const kindVal of kindValues) { |
||||||
|
const kindNum = parseInt(kindVal) |
||||||
|
if (!isNaN(kindNum)) { |
||||||
|
params.kinds.push(kindNum) |
||||||
|
} |
||||||
|
} |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Process detected bare event IDs (those not used as parameters)
|
||||||
|
for (const detectedId of detectedEventIds) { |
||||||
|
const start = detectedId.start |
||||||
|
// Skip if already used by a parameter pattern
|
||||||
|
if (usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Mark as used
|
||||||
|
usedIndices.push(start, detectedId.end) |
||||||
|
|
||||||
|
// Store the event ID in params.events
|
||||||
|
if (!params.events) { |
||||||
|
params.events = detectedId.id |
||||||
|
} else if (Array.isArray(params.events)) { |
||||||
|
params.events.push(detectedId.id) |
||||||
|
} else { |
||||||
|
params.events = [params.events, detectedId.id] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Process detected bare pubkey IDs (those not used as parameters)
|
||||||
|
for (const detectedId of detectedPubkeyIds) { |
||||||
|
const start = detectedId.start |
||||||
|
// Skip if already used by a parameter pattern
|
||||||
|
if (usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
// Mark as used
|
||||||
|
usedIndices.push(start, detectedId.end) |
||||||
|
|
||||||
|
// Store the pubkey ID in params.pubkey
|
||||||
|
if (!params.pubkey) { |
||||||
|
params.pubkey = detectedId.id |
||||||
|
} else if (Array.isArray(params.pubkey)) { |
||||||
|
params.pubkey.push(detectedId.id) |
||||||
|
} else { |
||||||
|
params.pubkey = [params.pubkey, detectedId.id] |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Process date range patterns first (DATE to DATE)
|
||||||
|
dateRangePattern.lastIndex = 0 |
||||||
|
while ((match = dateRangePattern.exec(normalizedQuery)) !== null) { |
||||||
|
const startDate = normalizeDate(match[1]) |
||||||
|
const endDate = normalizeDate(match[2]) |
||||||
|
const start = match.index |
||||||
|
const end = start + match[0].length |
||||||
|
|
||||||
|
usedIndices.push(start, end) |
||||||
|
lastIndex = Math.max(lastIndex, end) |
||||||
|
|
||||||
|
// Use from/to for date ranges
|
||||||
|
params.from = startDate |
||||||
|
params.to = endDate |
||||||
|
} |
||||||
|
|
||||||
|
// Process date parameters (from:, to:, before:, after:)
|
||||||
|
datePattern.lastIndex = 0 |
||||||
|
while ((match = datePattern.exec(normalizedQuery)) !== null) { |
||||||
|
const start = match.index |
||||||
|
// Skip if already used by date range pattern
|
||||||
|
if (usedIndices.some((idx, i) => i % 2 === 0 && start >= idx && start <= usedIndices[i + 1])) { |
||||||
|
continue |
||||||
|
} |
||||||
|
|
||||||
|
const param = match[0].split(':')[0].toLowerCase() |
||||||
|
const value = normalizeDate(match[1]) |
||||||
|
const end = start + match[0].length |
||||||
|
|
||||||
|
usedIndices.push(start, end) |
||||||
|
lastIndex = Math.max(lastIndex, end) |
||||||
|
|
||||||
|
switch (param) { |
||||||
|
case 'from': |
||||||
|
params.from = value |
||||||
|
break |
||||||
|
case 'to': |
||||||
|
params.to = value |
||||||
|
break |
||||||
|
case 'before': |
||||||
|
params.before = value |
||||||
|
break |
||||||
|
case 'after': |
||||||
|
params.after = value |
||||||
|
break |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Extract plain text (everything not matched by patterns)
|
||||||
|
usedIndices.sort((a, b) => a - b) |
||||||
|
let plainText = '' |
||||||
|
let textStart = 0 |
||||||
|
|
||||||
|
// Remove duplicate indices and merge overlapping ranges
|
||||||
|
const ranges: Array<[number, number]> = [] |
||||||
|
for (let i = 0; i < usedIndices.length; i += 2) { |
||||||
|
const start = usedIndices[i] |
||||||
|
const end = usedIndices[i + 1] || usedIndices[i] |
||||||
|
ranges.push([start, end]) |
||||||
|
} |
||||||
|
|
||||||
|
// Sort and merge overlapping ranges
|
||||||
|
ranges.sort((a, b) => a[0] - b[0]) |
||||||
|
const merged: Array<[number, number]> = [] |
||||||
|
for (const range of ranges) { |
||||||
|
if (merged.length === 0 || merged[merged.length - 1][1] < range[0]) { |
||||||
|
merged.push(range) |
||||||
|
} else { |
||||||
|
merged[merged.length - 1][1] = Math.max(merged[merged.length - 1][1], range[1]) |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// Extract plain text from gaps between used ranges
|
||||||
|
for (const [start, end] of merged) { |
||||||
|
if (textStart < start) { |
||||||
|
const segment = normalizedQuery.substring(textStart, start).trim() |
||||||
|
if (segment) { |
||||||
|
plainText += (plainText ? ' ' : '') + segment |
||||||
|
} |
||||||
|
} |
||||||
|
textStart = Math.max(textStart, end) |
||||||
|
} |
||||||
|
|
||||||
|
// Add remaining text
|
||||||
|
if (textStart < normalizedQuery.length) { |
||||||
|
const remaining = normalizedQuery.substring(textStart).trim() |
||||||
|
if (remaining) { |
||||||
|
plainText += (plainText ? ' ' : '') + remaining |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
// If we have plain text and no other parameters, use it as d-tag
|
||||||
|
if (plainText && !Object.keys(params).length) { |
||||||
|
params.dtag = normalizeToDTag(plainText) |
||||||
|
} else if (plainText) { |
||||||
|
// Plain text can also be used for d-tag even with other params
|
||||||
|
params.dtag = normalizeToDTag(plainText) |
||||||
|
} |
||||||
|
|
||||||
|
return params |
||||||
|
} |
||||||
|
|
||||||
Loading…
Reference in new issue