mirror of
https://github.com/dergigi/boris.git
synced 2025-12-17 06:34:24 +01:00
Merge pull request #35 from dergigi/fuzzy2
perf(highlights): optimize highlight application performance
This commit is contained in:
@@ -64,10 +64,36 @@ export function tryMarkInTextNodes(
|
|||||||
let actualIndex = index
|
let actualIndex = index
|
||||||
if (useNormalized) {
|
if (useNormalized) {
|
||||||
// Map normalized index back to original text
|
// Map normalized index back to original text
|
||||||
let normalizedIdx = 0
|
// Build normalized text while tracking original positions
|
||||||
for (let i = 0; i < text.length && normalizedIdx < index; i++) {
|
let normalizedPos = 0
|
||||||
if (!/\s/.test(text[i]) || (i > 0 && !/\s/.test(text[i-1]))) normalizedIdx++
|
let prevWasWs = false
|
||||||
actualIndex = i + 1
|
for (let i = 0; i < text.length; i++) {
|
||||||
|
const ch = text[i]
|
||||||
|
const isWs = /\s/.test(ch)
|
||||||
|
|
||||||
|
if (isWs) {
|
||||||
|
// Whitespace: count only at start of whitespace sequence
|
||||||
|
if (!prevWasWs) {
|
||||||
|
if (normalizedPos === index) {
|
||||||
|
actualIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
normalizedPos++
|
||||||
|
}
|
||||||
|
prevWasWs = true
|
||||||
|
} else {
|
||||||
|
// Non-whitespace: count each character
|
||||||
|
if (normalizedPos === index) {
|
||||||
|
actualIndex = i
|
||||||
|
break
|
||||||
|
}
|
||||||
|
normalizedPos++
|
||||||
|
prevWasWs = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we didn't find exact match, use last position
|
||||||
|
if (normalizedPos < index) {
|
||||||
|
actualIndex = text.length
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,54 @@
|
|||||||
import { Highlight } from '../../types/highlights'
|
import { Highlight } from '../../types/highlights'
|
||||||
import { tryMarkInTextNodes } from './domUtils'
|
import { tryMarkInTextNodes } from './domUtils'
|
||||||
|
|
||||||
|
interface CacheEntry {
|
||||||
|
html: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simple in-memory cache for highlighted HTML results
|
||||||
|
const highlightCache = new Map<string, CacheEntry>()
|
||||||
|
const CACHE_TTL = 5 * 60 * 1000 // 5 minutes
|
||||||
|
const MAX_CACHE_SIZE = 50 // FIFO eviction after this many entries
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate cache key from content and highlights
|
||||||
|
*/
|
||||||
|
function getCacheKey(html: string, highlights: Highlight[], highlightStyle: string): string {
|
||||||
|
// Create a stable key from content hash (first 200 chars) and highlight IDs
|
||||||
|
const contentHash = html.slice(0, 200).replace(/\s+/g, ' ').trim()
|
||||||
|
const highlightIds = highlights
|
||||||
|
.map(h => h.id)
|
||||||
|
.sort()
|
||||||
|
.join(',')
|
||||||
|
return `${contentHash.length}:${highlightIds}:${highlightStyle}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clean up old cache entries and enforce size limit
|
||||||
|
*/
|
||||||
|
function cleanupCache(): void {
|
||||||
|
const now = Date.now()
|
||||||
|
const entries = Array.from(highlightCache.entries())
|
||||||
|
|
||||||
|
// Remove expired entries
|
||||||
|
for (const [key, entry] of entries) {
|
||||||
|
if (now - entry.timestamp > CACHE_TTL) {
|
||||||
|
highlightCache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enforce size limit with FIFO eviction (oldest first)
|
||||||
|
if (highlightCache.size > MAX_CACHE_SIZE) {
|
||||||
|
const sortedEntries = Array.from(highlightCache.entries())
|
||||||
|
.sort((a, b) => a[1].timestamp - b[1].timestamp)
|
||||||
|
const toRemove = sortedEntries.slice(0, highlightCache.size - MAX_CACHE_SIZE)
|
||||||
|
for (const [key] of toRemove) {
|
||||||
|
highlightCache.delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Apply highlights to HTML content by injecting mark tags using DOM manipulation
|
* Apply highlights to HTML content by injecting mark tags using DOM manipulation
|
||||||
*/
|
*/
|
||||||
@@ -13,19 +61,24 @@ export function applyHighlightsToHTML(
|
|||||||
return html
|
return html
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check cache
|
||||||
|
const cacheKey = getCacheKey(html, highlights, highlightStyle)
|
||||||
|
const cached = highlightCache.get(cacheKey)
|
||||||
|
if (cached && Date.now() - cached.timestamp < CACHE_TTL) {
|
||||||
|
return cached.html
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up cache periodically
|
||||||
|
cleanupCache()
|
||||||
|
|
||||||
const tempDiv = document.createElement('div')
|
const tempDiv = document.createElement('div')
|
||||||
tempDiv.innerHTML = html
|
tempDiv.innerHTML = html
|
||||||
|
|
||||||
// CRITICAL: Remove any existing highlight marks to start with clean HTML
|
// Collect all text nodes once before processing highlights (performance optimization)
|
||||||
// This prevents old broken highlights from corrupting the new rendering
|
const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null)
|
||||||
const existingMarks = tempDiv.querySelectorAll('mark[data-highlight-id]')
|
const textNodes: Text[] = []
|
||||||
existingMarks.forEach(mark => {
|
let node: Node | null
|
||||||
// Replace the mark with its text content
|
while ((node = walker.nextNode())) textNodes.push(node as Text)
|
||||||
const textNode = document.createTextNode(mark.textContent || '')
|
|
||||||
mark.parentNode?.replaceChild(textNode, mark)
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
for (const highlight of highlights) {
|
for (const highlight of highlights) {
|
||||||
const searchText = highlight.content.trim()
|
const searchText = highlight.content.trim()
|
||||||
@@ -34,14 +87,6 @@ export function applyHighlightsToHTML(
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Collect all text nodes
|
|
||||||
const walker = document.createTreeWalker(tempDiv, NodeFilter.SHOW_TEXT, null)
|
|
||||||
const textNodes: Text[] = []
|
|
||||||
let node: Node | null
|
|
||||||
while ((node = walker.nextNode())) textNodes.push(node as Text)
|
|
||||||
|
|
||||||
|
|
||||||
// Try exact match first, then normalized match
|
// Try exact match first, then normalized match
|
||||||
const found = tryMarkInTextNodes(textNodes, searchText, highlight, false, highlightStyle) ||
|
const found = tryMarkInTextNodes(textNodes, searchText, highlight, false, highlightStyle) ||
|
||||||
tryMarkInTextNodes(textNodes, searchText, highlight, true, highlightStyle)
|
tryMarkInTextNodes(textNodes, searchText, highlight, true, highlightStyle)
|
||||||
@@ -51,7 +96,14 @@ export function applyHighlightsToHTML(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const result = tempDiv.innerHTML
|
||||||
|
|
||||||
return tempDiv.innerHTML
|
// Store in cache
|
||||||
|
highlightCache.set(cacheKey, {
|
||||||
|
html: result,
|
||||||
|
timestamp: Date.now()
|
||||||
|
})
|
||||||
|
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user