43 lines
1.1 KiB
TypeScript
43 lines
1.1 KiB
TypeScript
import type { ReactNode } from "react";
|
|
|
|
// Must match the backend's search::HL_PRE / HL_POST sentinel characters
|
|
// (U+0002 / U+0003). Written as escapes so they survive copy-paste.
|
|
const PRE = "\x02";
|
|
const POST = "\x03";
|
|
|
|
/** Renders a sentinel-marked snippet: matched spans become <mark>, the rest is text.
|
|
* Pure string handling — no HTML is injected, so this is XSS-safe. */
|
|
export function Highlight({ text }: { text: string }) {
|
|
const nodes: ReactNode[] = [];
|
|
let rest = text;
|
|
let key = 0;
|
|
|
|
while (rest.length > 0) {
|
|
const start = rest.indexOf(PRE);
|
|
|
|
if (start === -1) {
|
|
nodes.push(rest);
|
|
break;
|
|
}
|
|
|
|
if (start > 0) nodes.push(rest.slice(0, start));
|
|
|
|
const end = rest.indexOf(POST, start + PRE.length);
|
|
|
|
if (end === -1) {
|
|
// Malformed: no closing marker. Emit the remainder verbatim, minus the marker.
|
|
nodes.push(rest.slice(start + PRE.length));
|
|
break;
|
|
}
|
|
|
|
nodes.push(
|
|
<mark key={key++} className="bg-yellow-200">
|
|
{rest.slice(start + PRE.length, end)}
|
|
</mark>,
|
|
);
|
|
rest = rest.slice(end + POST.length);
|
|
}
|
|
|
|
return <>{nodes}</>;
|
|
}
|