{"version":3,"file":"highlighting.cjs","sources":["../../../src/core/highlighting.ts"],"sourcesContent":["/**\n * Match Highlighting Utilities\n * Calculates positions of matched characters for UI highlighting\n */\n\nimport type { MatchHighlight, MatchType, SearchMatch } from \"./types.js\";\n\n/**\n * Calculate highlights for a search match\n */\nexport function calculateHighlights(\n  //\n  match: SearchMatch,\n  query: string,\n  displayText: string\n): MatchHighlight[] {\n  const highlights: MatchHighlight[] = [];\n  const normalizedDisplay = displayText.toLowerCase();\n  const normalizedQuery = query.toLowerCase();\n\n  switch (match.matchType) {\n    case \"exact\":\n      // Highlight the entire word\n      highlights.push({\n        start: 0,\n        end: displayText.length,\n        type: \"exact\",\n      });\n      break;\n\n    case \"prefix\":\n      // Highlight the matching prefix\n      const prefixEnd = Math.min(normalizedQuery.length, displayText.length);\n      highlights.push({\n        start: 0,\n        end: prefixEnd,\n        type: \"prefix\",\n      });\n      break;\n\n    case \"substring\":\n      // Find where the query appears in the display text\n      const substringIndex = normalizedDisplay.indexOf(normalizedQuery);\n      if (substringIndex !== -1) {\n        highlights.push({\n          start: substringIndex,\n          end: substringIndex + normalizedQuery.length,\n          type: \"substring\",\n        });\n      }\n      break;\n\n    case \"fuzzy\":\n      // For fuzzy matches, highlight matching characters\n      highlights.push(...calculateFuzzyHighlights(normalizedQuery, normalizedDisplay, \"fuzzy\"));\n      break;\n\n    case \"ngram\":\n      // Highlight n-gram matches\n      highlights.push(...calculateNgramHighlights(normalizedQuery, normalizedDisplay));\n      break;\n\n    case \"phonetic\":\n    case \"synonym\":\n    case \"compound\":\n      // For phonetic/synonym/compound, highlight the whole word\n      highlights.push({\n        start: 0,\n        end: displayText.length,\n        type: match.matchType,\n      });\n      break;\n  }\n\n  return mergeOverlappingHighlights(highlights);\n}\n\n/**\n * Calculate highlights for fuzzy matches using edit distance alignment\n */\nfunction calculateFuzzyHighlights(query: string, text: string, type: MatchType): MatchHighlight[] {\n  const highlights: MatchHighlight[] = [];\n  let queryIdx = 0;\n  let textIdx = 0;\n\n  // Simple greedy matching - find matching characters\n  while (queryIdx < query.length && textIdx < text.length) {\n    if (query[queryIdx] === text[textIdx]) {\n      // Found a match\n      const start = textIdx;\n      let end = textIdx + 1;\n\n      // Extend the match as far as possible\n      queryIdx++;\n      textIdx++;\n      while (queryIdx < query.length && textIdx < text.length && query[queryIdx] === text[textIdx]) {\n        end++;\n        queryIdx++;\n        textIdx++;\n      }\n\n      highlights.push({ start, end, type });\n    } else {\n      textIdx++;\n    }\n  }\n\n  return highlights;\n}\n\n/**\n * Calculate highlights for n-gram matches\n */\nfunction calculateNgramHighlights(query: string, text: string): MatchHighlight[] {\n  const highlights: MatchHighlight[] = [];\n  const ngramSize = 3;\n\n  // Find all n-grams from query that appear in text\n  for (let i = 0; i <= query.length - ngramSize; i++) {\n    const ngram = query.slice(i, i + ngramSize);\n    let searchStart = 0;\n\n    // Find all occurrences of this n-gram\n    while (true) {\n      const index = text.indexOf(ngram, searchStart);\n      if (index === -1) break;\n\n      highlights.push({\n        start: index,\n        end: index + ngramSize,\n        type: \"ngram\",\n      });\n\n      searchStart = index + 1;\n    }\n  }\n\n  return highlights;\n}\n\n/**\n * Merge overlapping highlights to avoid duplicate highlighting\n */\nfunction mergeOverlappingHighlights(highlights: MatchHighlight[]): MatchHighlight[] {\n  if (highlights.length === 0) return [];\n\n  // Sort by start position\n  const sorted = [...highlights].sort((a, b) => a.start - b.start);\n  const merged: MatchHighlight[] = [sorted[0]];\n\n  for (let i = 1; i < sorted.length; i++) {\n    const current = sorted[i];\n    const last = merged[merged.length - 1];\n\n    if (current.start <= last.end) {\n      // Overlapping - merge them\n      last.end = Math.max(last.end, current.end);\n      // Keep the more specific match type\n      if (getMatchTypePriority(current.type) > getMatchTypePriority(last.type)) {\n        last.type = current.type;\n      }\n    } else {\n      // No overlap - add as new highlight\n      merged.push(current);\n    }\n  }\n\n  return merged;\n}\n\n/**\n * Get priority for match types (higher = more specific)\n */\nfunction getMatchTypePriority(type: MatchType): number {\n  const priorities: Record<MatchType, number> = {\n    exact: 10,\n    prefix: 9,\n    substring: 8,\n    fuzzy: 7,\n    ngram: 6,\n    phonetic: 5,\n    compound: 4,\n    synonym: 3,\n  };\n  return priorities[type] || 0;\n}\n\n/**\n * Format highlighted text for HTML rendering\n */\nexport function formatHighlightedHTML(\n  //\n  text: string,\n  highlights: MatchHighlight[],\n  className: string = \"highlight\"\n): string {\n  if (!highlights || highlights.length === 0) {\n    return escapeHTML(text);\n  }\n\n  let result = \"\";\n  let lastEnd = 0;\n\n  for (const highlight of highlights) {\n    // Add text before highlight\n    if (highlight.start > lastEnd) {\n      result += escapeHTML(text.slice(lastEnd, highlight.start));\n    }\n\n    // Add highlighted text\n    const highlightedText = text.slice(highlight.start, highlight.end);\n    result += `<mark ${highlight.type === \"exact\" ? 'data-type=\"exact\"' : \"\"} class=\"${className} ${className}--${highlight.type}\">${escapeHTML(highlightedText)}</mark>`;\n\n    lastEnd = highlight.end;\n  }\n\n  // Add remaining text\n  if (lastEnd < text.length) {\n    result += escapeHTML(text.slice(lastEnd));\n  }\n\n  return result;\n}\n\n/**\n * Escape HTML special characters\n */\nfunction escapeHTML(text: string): string {\n  const div = typeof document !== \"undefined\" ? document.createElement(\"div\") : null;\n  if (div) {\n    div.textContent = text;\n    return div.innerHTML;\n  }\n  // Fallback for Node.js\n  return text.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\").replace(/\"/g, \"&quot;\").replace(/'/g, \"&#039;\");\n}\n"],"names":[],"mappings":"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"}