import { Fragment, ReactNode } from 'react'; import { FormattedLine } from '@/docker/helpers/logHelper/types'; interface Props { line: FormattedLine; /** 1-based ordinal shown in the left gutter (when line numbers are on). */ lineNumber?: number; showLineNumber: boolean; /** Case-insensitive substring to highlight (empty = no highlight). */ search: string; } // Split a span's text on the search term (case-insensitive) and wrap matches in // a . Highlighting is done per span, so a match that straddles two spans // (e.g. a coloured boundary) is not highlighted — an accepted edge case: the // formatter's spans are the colouring unit and re-tokenising them to highlight // across boundaries would fight the existing pipeline. function highlight(text: string, search: string): ReactNode { if (!search) { return text; } const lower = text.toLowerCase(); const term = search.toLowerCase(); const parts: ReactNode[] = []; let start = 0; for (;;) { const idx = lower.indexOf(term, start); if (idx === -1) { break; } if (idx > start) { parts.push(text.substring(start, idx)); } parts.push( {text.substring(idx, idx + term.length)} ); start = idx + term.length; } if (parts.length === 0) { return text; } if (start < text.length) { parts.push(text.substring(start)); } return parts.map((part, i) => {part}); } export function LogLine({ line, lineNumber, showLineNumber, search }: Props) { return (
{showLineNumber && ( )} {line.spans.length === 0 ? ( // keep empty lines visible (they still occupy a row) ' ' ) : ( line.spans.map((span, index) => ( {highlight(span.text, search)} )) )}
); }