Spaces:
Paused
Paused
File size: 1,626 Bytes
fbf0be8 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 | /**
* Build safe HTML from plain transcript with non-overlapping span highlights.
* @param {string} text
* @param {Array<{ text: string, className: string, title?: string }>} spans
*/
export function highlightTranscript(text, spans = []) {
const raw = String(text || '')
if (!raw) return ''
const escape = (s) =>
String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
const ranges = []
for (const span of spans) {
const needle = String(span.text || '').trim()
if (!needle) continue
let from = 0
while (from < raw.length) {
const idx = raw.indexOf(needle, from)
if (idx === -1) break
ranges.push({
start: idx,
end: idx + needle.length,
className: span.className,
title: span.title || '',
})
from = idx + needle.length
}
}
if (!ranges.length) return escape(raw)
ranges.sort((a, b) => a.start - b.start || b.end - a.end)
const merged = []
for (const r of ranges) {
const last = merged[merged.length - 1]
if (!last || r.start >= last.end) {
merged.push({ ...r })
continue
}
if (r.end > last.end) last.end = r.end
}
let html = ''
let cursor = 0
for (const r of merged) {
if (r.start > cursor) html += escape(raw.slice(cursor, r.start))
const titleAttr = r.title ? ` title="${escape(r.title)}"` : ''
html += `<mark class="${r.className}"${titleAttr}>${escape(raw.slice(r.start, r.end))}</mark>`
cursor = r.end
}
if (cursor < raw.length) html += escape(raw.slice(cursor))
return html
}
|