"use client";
import { useSearchParams } from "next/navigation";
import { useEffect, useRef } from "react";
import { buildHighlightRegex } from "./utils/search-match";
function escapeHtml(input: string) {
return input.replace(/[&<>]/g, (c) =>
c === "&" ? "&" : c === "<" ? "<" : ">",
);
}
function highlight(root: HTMLElement, query: string) {
const regex = buildHighlightRegex(query);
if (!regex) return;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null);
const textNodes: Text[] = [];
let node = walker.nextNode();
while (node) {
if (node instanceof Text) {
textNodes.push(node);
}
node = walker.nextNode();
}
for (const textNode of textNodes) {
const nodeValue = textNode.nodeValue;
// `g`-flag .test is stateful — reset so a hit in one node can't skip the next.
regex.lastIndex = 0;
if (!nodeValue || !regex.test(nodeValue)) continue;
const span = document.createElement("span");
// Escape first — the text node's value must not parse as HTML.
span.innerHTML = escapeHtml(nodeValue).replace(
regex,
(match) => `${match}`,
);
textNode.parentNode?.replaceChild(span, textNode);
}
}
export function HighlightText({ children }: { children: React.ReactNode }) {
const ref = useRef(null);
const searchParams = useSearchParams();
const q = searchParams.get("q");
useEffect(() => {
if (ref.current && q) {
highlight(ref.current, q);
}
}, [q]);
return {children}
;
}