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}${tag}>`;
};
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}${tag}>`).join('');
const headers = headerLines
.map(l => `${renderCells(parseTableRow(l), 'th')}
`)
.join('');
const rows = bodyLines
.map(l => `${renderCells(parseTableRow(l), 'td')}
`)
.join('');
return ``;
};
export const highlight = (message: string, keepMarkup = true): string => {
let resultHTML = '';
const tokenRegex = /(\*\*?|"|```|`|(?:^|\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)}'; 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 (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(/((?:(?:^|\n)\|.+)+)/g, match => parseTable(match));
resultHTML = resultHTML.replace(/(^|\n)---(\n|$)/g, (_, pre, post) => `${pre}
${post}`);
resultHTML = resultHTML.replace(/((?:(?:^|\n)[-+] .+)+)/g, match => parseList(match, false));
resultHTML = resultHTML.replace(/((?:(?:^|\n)\d+\. .+)+)/g, match => parseList(match, true));
}
return resultHTML;
}