import type { FuseResultMatch, MatchIndices } from "./core/types"; /** * Simple utility to remove HTML tags from a string. */ export function stripHtmlTags(html: string): string { if (!html) return ""; return html.replace(/<[^>]*>/g, "").replace("\n", " "); } /** * Removes HTML tags from a string, but preserves tags. */ export function stripHtmlButKeepHighlights(html: string): string { if (!html) return ""; // Use a placeholder for highlight tags, strip others, then restore placeholders. const highlightOpenPlaceholder = "__HIGHLIGHT_OPEN__"; const highlightClosePlaceholder = "__HIGHLIGHT_CLOSE__"; let processed = html.replace( //g, highlightOpenPlaceholder, ); processed = processed.replace(/<\/span>/g, (match, offset, fullString) => { // Only replace if it likely corresponds to our highlight span // This is imperfect but helps avoid replacing unrelated spans. // Look backwards for the nearest opening placeholder. const lastPlaceholder = fullString.lastIndexOf( highlightOpenPlaceholder, offset, ); if (lastPlaceholder !== -1) { // Check if there's another opening tag between the placeholder and the closing span const interveningContent = fullString.substring( lastPlaceholder + highlightOpenPlaceholder.length, offset, ); if (!/ if unsure }); // Strip all remaining HTML tags processed = processed.replace(/<[^>]*>/g, ""); // Restore the highlight tags processed = processed.replace( new RegExp(highlightOpenPlaceholder, "g"), '', ); processed = processed.replace( new RegExp(highlightClosePlaceholder, "g"), "", ); return processed; } export function highlightMatch( text: string, term: string, matches?: readonly FuseResultMatch[], ): string { if (!term.trim() || !matches || matches.length === 0) return text; try { // Find matches for the text field or allContent that contains the text const fieldMatches = matches.find( (match) => match.key === "text" || (match.key === "allContent" && match.value?.includes(text)), ); if ( !fieldMatches || !fieldMatches.indices || fieldMatches.indices.length === 0 ) { return text; } // Create a map of character positions to mark which ones need highlighting const highlightMap = new Array(text.length).fill(false); fieldMatches.indices.forEach((indices: MatchIndices) => { const start = indices[0]; const end = indices[1]; if (fieldMatches.key === "allContent") { // Find where our text appears in the allContent const allContent = fieldMatches.value; const textPos = allContent?.indexOf(text) ?? -1; // Only highlight if the match overlaps with our text if (textPos >= 0) { // Adjust start and end to be relative to our text field const relStart = start - textPos; const relEnd = end - textPos; // Only highlight if the match actually overlaps with our text field if (relEnd >= 0 && relStart < text.length) { // Mark the overlapping characters for ( let i = Math.max(0, relStart); i <= Math.min(text.length - 1, relEnd); i++ ) { highlightMap[i] = true; } } } } else { // Regular text field match - ensure indices are within bounds if (start >= 0 && end < text.length) { for (let i = start; i <= end; i++) { highlightMap[i] = true; } } } }); let result = ""; let inHighlight = false; for (let i = 0; i < text.length; i++) { if (highlightMap[i] && !inHighlight) { result += ''; inHighlight = true; } else if (!highlightMap[i] && inHighlight) { result += ""; inHighlight = false; } result += text.charAt(i); } if (inHighlight) { result += ""; } return result; } catch (e) { console.error("Error highlighting match:", e); return text; } } // Function to extract and highlight content snippet using Fuse matches export function highlightSnippet( content: string, term: string, matches?: readonly FuseResultMatch[], ): string { if (!content || !term.trim() || !matches || matches.length === 0) return content; try { // Find matches for content field or allContent that contains the content const contentMatches = matches.find( (match) => match.key === "content" || (match.key === "allContent" && match.value?.includes(content)), ); if ( !contentMatches || !contentMatches.indices || contentMatches.indices.length === 0 ) { // No content matches, return plain content return content.length > 100 ? content.substring(0, 100) + "..." : content; } // Find the match indices let allIndices: MatchIndices[] = contentMatches.indices as MatchIndices[]; // If matching against allContent, adjust indices to be relative to content if (contentMatches.key === "allContent") { const allContent = contentMatches.value; const contentPos = allContent?.indexOf(content) ?? -1; if (contentPos >= 0) { // Adjust indices to be relative to the content field allIndices = allIndices .map( (indices) => [ indices[0] - contentPos, indices[1] - contentPos, ] as MatchIndices, ) .filter((indices) => indices[1] >= 0 && indices[0] < content.length); } } if (allIndices.length === 0) { return content.length > 100 ? content.substring(0, 100) + "..." : content; } // Find a good center point for our snippet (average of first match) const firstMatch = allIndices[0]; const matchCenter = Math.floor((firstMatch[0] + firstMatch[1]) / 2); // Extract a window around the match const windowSize = 100; const start = Math.max(0, matchCenter - windowSize / 2); const end = Math.min(content.length, matchCenter + windowSize / 2); // Create the basic snippet let snippet = content.substring(start, end); if (start > 0) snippet = "..." + snippet; if (end < content.length) snippet += "..."; // Create a highlighting map for the snippet const snippetLength = snippet.length; const highlightMap = new Array(snippetLength).fill(false); // Calculate offset for the highlighting const startOffset = start > 0 ? start - 3 : start; // Account for '...' if present // Mark each matched character in the snippet allIndices.forEach((indices: MatchIndices) => { const matchStart = indices[0]; const matchEnd = indices[1]; // Skip matches outside our snippet window if (matchEnd < start || matchStart > end) return; // Adjust match indices to be relative to snippet const snippetMatchStart = Math.max(0, matchStart - startOffset); const snippetMatchEnd = Math.min( snippetLength - 1, matchEnd - startOffset, ); // Mark characters for highlighting for (let i = snippetMatchStart; i <= snippetMatchEnd; i++) { if (i >= 0 && i < snippetLength) { highlightMap[i] = true; } } }); // Build the highlighted snippet let result = ""; let inHighlight = false; for (let i = 0; i < snippetLength; i++) { // If highlighting state changes, add appropriate tags if (highlightMap[i] && !inHighlight) { result += ''; inHighlight = true; } else if (!highlightMap[i] && inHighlight) { result += ""; inHighlight = false; } // Add the current character result += snippet.charAt(i); } // Close highlight tag if we're still in one at the end if (inHighlight) { result += ""; } return result; } catch (e) { console.error("Error highlighting snippet:", e); return content.length > 100 ? content.substring(0, 100) + "..." : content; } }