import { Fragment, type ReactNode } from 'react';
import {
hasMarkdown,
parseInline,
parseMarkdownBlocks,
type InlineToken,
type MarkdownBlock
} from '../../shared/markdown';
import { highlightText } from '../HighlightText/highlight-text';
import { ResizableResultTable } from '../ResizableResultTable/ResizableResultTable';
import { PathText, isPathField, isWholePath } from '../PathText/PathText';
interface Props {
value: string;
search?: string;
onResults?: () => void;
resizableTables?: boolean;
label?: string;
}
const formattedTags = /<\/?(?:p|br|b|strong|i|em|u|ul|ol|li|a)\b/i;
const encodedTags = /<\/?(?:p|br|b|strong|i|em|u|ul|ol|li|a)\b/i;
const discardedTags = new Set(['script', 'style', 'iframe', 'object', 'embed', 'svg', 'math']);
function decodeEntities(value: string): string {
const textarea = document.createElement('textarea');
textarea.innerHTML = value;
return textarea.value;
}
function safeHref(value: string | null): string | undefined {
if (!value) return undefined;
const href = value.trim();
return /^(?:https?:|mailto:)/i.test(href) ? href : undefined;
}
const bareUrl = /https?:\/\/[^\s<>"']+/g;
const trailingPunctuation = /[.,;:!?)\]}]+$/;
const resultTriggerPattern = /(Show\s+Assessment\s+Evidence|Show\s+Rule\s+Result\s+XML|(?:--?\s*)?\bMore\b)/gi;
function linkifyText(text: string, key: string, search?: string): ReactNode {
if (!text || !/https?:\/\//i.test(text)) return ;
const parts: ReactNode[] = [];
let lastIndex = 0;
for (const match of text.matchAll(bareUrl)) {
const raw = match[0];
const href = raw.replace(trailingPunctuation, '');
const start = match.index ?? 0;
if (start > lastIndex) parts.push(highlightText(text.slice(lastIndex, start), search));
parts.push({highlightText(href, search)});
if (href.length < raw.length) parts.push(highlightText(raw.slice(href.length), search));
lastIndex = start + raw.length;
}
if (lastIndex < text.length) parts.push(highlightText(text.slice(lastIndex), search));
return {parts};
}
function renderTextWithTriggers(text: string, key: string, search?: string, onResults?: () => void): ReactNode {
if (!text) return null;
resultTriggerPattern.lastIndex = 0;
if (!resultTriggerPattern.test(text)) return linkifyText(text, key, search);
resultTriggerPattern.lastIndex = 0;
const parts: ReactNode[] = [];
let lastIndex = 0;
let match: RegExpExecArray | null;
while ((match = resultTriggerPattern.exec(text)) !== null) {
const raw = match[0];
const triggerText = match[1];
const start = match.index;
if (start > lastIndex) {
parts.push(linkifyText(text.slice(lastIndex, start), `${key}-p-${start}`, search));
}
const isXml = /rule\s*result\s*xml/i.test(triggerText);
if (isXml) {
const handleXmlClick = (e: React.MouseEvent) => {
e.preventDefault();
const details = document.querySelector('.finding-optional-detail');
if (details) {
details.setAttribute('open', 'true');
details.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
}
};
parts.push(
);
} else if (onResults) {
parts.push(
);
} else {
parts.push(
{highlightText(raw, search)}
);
}
lastIndex = start + raw.length;
}
if (lastIndex < text.length) {
parts.push(linkifyText(text.slice(lastIndex), `${key}-p-end`, search));
}
return {parts};
}
function renderNode(node: Node, key: string, insideAnchor = false, search?: string, onResults?: () => void): ReactNode {
if (node.nodeType === Node.TEXT_NODE) {
if (insideAnchor) return highlightText(node.textContent ?? '', search);
return {renderInline(parseInline(node.textContent ?? ''), key, search, onResults)};
}
if (node.nodeType !== Node.ELEMENT_NODE) return null;
const element = node as Element;
const tag = element.tagName.toLowerCase();
if (discardedTags.has(tag)) return null;
const children = Array.from(element.childNodes).map((child, index) => renderNode(child, `${key}-${index}`, insideAnchor || tag === 'a', search, onResults));
switch (tag) {
case 'p': return
{children}
;
case 'br': return ;
case 'b':
case 'strong': return {children};
case 'i':
case 'em': return {children};
case 'u': return {children};
case 'ul': return