import clsx from 'clsx'; import styles from './assets/highlight.module.css'; const parseTableRow = (line: string): string[] => line.split('|').slice(1, -1).map(cell => cell.trim()); const isSeparatorRow = (line: string): boolean => /^\|[\s|:\-]+\|$/.test(line.trim()); export const parseList = (block: string, ordered: boolean): string => { const marker = ordered ? /^\d+\. / : /^[-+] /; const items = block.trim().split('\n') .map(l => `
  • ${l.trim().replace(marker, '')}
  • `) .join(''); const tag = ordered ? 'ol' : 'ul'; return `<${tag} class="${styles.list}">${items}`; }; export const parseTable = (table: string): string => { const lines = table.trim().split('\n').map(l => l.trim()).filter(l => l.startsWith('|')); if (lines.length < 2) return table; const sepIndex = lines.findIndex(isSeparatorRow); if (sepIndex === -1) return table; const headerLines = lines.slice(0, sepIndex); const bodyLines = lines.slice(sepIndex + 1); const renderCells = (cells: string[], tag: 'th' | 'td') => cells.map(cell => `<${tag}>${cell}`).join(''); const headers = headerLines .map(l => `${renderCells(parseTableRow(l), 'th')}`) .join(''); const rows = bodyLines .map(l => `${renderCells(parseTableRow(l), 'td')}`) .join(''); return `${headers}${rows}
    `; }; export const highlight = (message: string, keepMarkup = true): string => { let resultHTML = ''; const tokenRegex = /(\*{1,3}|"|```|`|(?:^|\n)#{1,3} |(?:^|\n)> |\n)|---/g; const headerRegex = /#{1,3} $/; const blockquoteRegex = /> $/; const stack: string[] = []; let inCodeBlock = false; let inMonospaced = false; let inHeader = false; let inBlockquote = false; let lastIndex = 0; let match: RegExpExecArray | null; while ((match = tokenRegex.exec(message)) !== null) { resultHTML += message.slice(lastIndex, match.index); lastIndex = tokenRegex.lastIndex; const token = match[0]; const isClose = stack.at(-1) === token; const keepToken = keepMarkup || token === '"'; if (inCodeBlock) { if (token === '```' && isClose) { inCodeBlock = false; stack.pop(); resultHTML += `${keepToken ? token : ''}`; } else { resultHTML += token; } continue; } if (inMonospaced) { if (token === '`' && isClose) { inMonospaced = false; stack.pop(); resultHTML += `${keepToken ? token : ''}`; } else { resultHTML += token; } continue; } const headerMatch = token.match(headerRegex); if (headerMatch) { if (inHeader) resultHTML += ''; if (inBlockquote) { resultHTML += ''; inBlockquote = false; } const markup = keepMarkup ? headerMatch[0] : ''; const len = headerMatch[0].length; inHeader = true; resultHTML += `${token.slice(0, -len)}${markup}`; continue; } const blockquoteMatch = token.match(blockquoteRegex); if (blockquoteMatch) { if (inHeader) { resultHTML += ''; inHeader = false; } if (inBlockquote) resultHTML += ''; const markup = keepMarkup ? '> ' : ''; inBlockquote = true; resultHTML += `${token.slice(0, -2)}${markup}`; continue; } if (token === '\n') { if (inHeader) { resultHTML += `${keepMarkup ? '\n' : ''}`; inHeader = false; } else if (inBlockquote) { resultHTML += `${keepMarkup ? '\n' : ''}`; inBlockquote = false; } else { resultHTML += '\n'; } continue; } if (token === '***' || token === '---') { resultHTML += `${keepToken ? token : ''}`; } else if (isClose) { stack.pop(); resultHTML += `${keepToken ? token : ''}`; } else if (token === '*') { stack.push(token); resultHTML += `${keepToken ? token : ''}`; } else if (token === '**') { stack.push(token); resultHTML += `${keepToken ? token : ''}`; } else if (token === '"') { stack.push(token); resultHTML += `"`; } else if (token === '```') { stack.push(token); inCodeBlock = true; resultHTML += `${keepToken ? token : ''}`; } else if (token === '`') { stack.push(token); inMonospaced = true; resultHTML += `${keepToken ? token : ''}`; } } resultHTML += message.slice(lastIndex); if (inHeader) resultHTML += ''; if (inBlockquote) resultHTML += ''; resultHTML += ''.repeat(stack.length); if (!keepMarkup) { resultHTML = resultHTML.replace(/!\[([^\]]*)\]\(([^)]+)\)/g, (_, alt, src) => `${alt}` ); resultHTML = resultHTML.replace(/((?:(?:^|\n)\|.+)+)/g, match => parseTable(match)); resultHTML = resultHTML.replace(/((?:(?:^|\n)[-+] .+)+)/g, match => parseList(match, false)); resultHTML = resultHTML.replace(/((?:(?:^|\n)\d+\. .+)+)/g, match => parseList(match, true)); } return resultHTML; }