/**
 * safe-highlight - XSS-safe rendering of server-provided highlight HTML.
 *
 * The search API returns highlightedQuery with a small set of wrapping tags
 * (mark, b, strong, em, i) around matched fragments.
 * Rather than using dangerouslySetInnerHTML we parse the string, allow only
 * the known safe tags, and return a React.ReactNode tree.
 *
 * Allowed tags: mark, b, strong, em, i
 * Everything else is stripped to its text content.
 * Attributes on allowed tags are dropped entirely.
 */
import React from "react";
const ALLOWED_TAGS = new Set(["mark", "b", "strong", "em", "i"]);
/**
 * Parse a highlighted HTML string and return a React.ReactNode.
 *
 * Strategy:
 * - In a browser environment: use DOMParser to parse the fragment as HTML,
 *   then walk the resulting DOM, keeping only allowed tags.
 * - SSR / environments without DOMParser: use a regex-based fallback that
 *   strips or passes through the same allowed tags.
 *
 * @param html  Server-provided highlighted string, e.g. "run<mark>ning</mark>"
 * @returns     A React.ReactNode safe to render without raw innerHTML
 */
export function safeHighlight(html) {
    if (!html)
        return null;
    if (typeof window !== "undefined" && typeof DOMParser !== "undefined") {
        return domParserStrategy(html);
    }
    return regexStrategy(html);
}
// ============================================================================
// DOMParser strategy (browser)
// ============================================================================
function domParserStrategy(html) {
    const parser = new DOMParser();
    // Wrap in a body element so the parser constructs a full document.
    const doc = parser.parseFromString("<body>" + html + "</body>", "text/html");
    const nodes = Array.from(doc.body.childNodes);
    return React.createElement(React.Fragment, null, ...nodes.map(nodeToReact));
}
function nodeToReact(node, idx) {
    if (node.nodeType === Node.TEXT_NODE) {
        return node.textContent ?? "";
    }
    if (node.nodeType === Node.ELEMENT_NODE) {
        const el = node;
        const tagName = el.tagName.toLowerCase();
        if (ALLOWED_TAGS.has(tagName)) {
            // Strip all attributes - we only allow the semantic tag itself.
            const children = Array.from(el.childNodes).map((child, i) => nodeToReact(child, i));
            return React.createElement(tagName, { key: idx }, ...children);
        }
        // Not an allowed tag - render its text content only (strips the tag).
        return el.textContent ?? "";
    }
    return null;
}
// ============================================================================
// Regex fallback (SSR / no DOMParser)
// ============================================================================
// Matches either an opening allowed tag, a closing allowed tag, or text.
const TOKEN_RE = /<\/?(?:mark|b|strong|em|i)(?:\s[^>]*)?>|[^<]+|<[^>]*>/g;
function regexStrategy(html) {
    const parts = [];
    let key = 0;
    // We handle only single-level nesting for the fallback (sufficient for
    // highlight use-case where the API wraps a single run of text).
    let pendingTag = null;
    let pendingChildren = [];
    const flushPending = () => {
        if (pendingTag) {
            parts.push(React.createElement(pendingTag, { key: key++ }, ...pendingChildren));
            pendingTag = null;
            pendingChildren = [];
        }
    };
    for (const token of html.matchAll(TOKEN_RE)) {
        const t = token[0];
        if (t.startsWith("</")) {
            // Closing tag - flush.
            flushPending();
        }
        else if (t.startsWith("<")) {
            // Opening or self-closing tag.
            const tagMatch = t.match(/^<(\w+)/);
            if (!tagMatch)
                continue;
            const tagName = tagMatch[1].toLowerCase();
            if (ALLOWED_TAGS.has(tagName)) {
                flushPending();
                pendingTag = tagName;
            }
            // Unknown/disallowed opening tag - discard the tag itself (keep text).
        }
        else {
            // Text node.
            if (pendingTag) {
                pendingChildren.push(t);
            }
            else {
                parts.push(t);
            }
        }
    }
    flushPending();
    if (parts.length === 0)
        return null;
    if (parts.length === 1)
        return parts[0];
    return React.createElement(React.Fragment, null, ...parts);
}
