1
0
Fork 0
tsgames/src/common/highlight.ts

157 lines
5.7 KiB
TypeScript

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 => `<li>${l.trim().replace(marker, '')}</li>`)
.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 => `<tr>${renderCells(parseTableRow(l), 'th')}</tr>`)
.join('');
const rows = bodyLines
.map(l => `<tr>${renderCells(parseTableRow(l), 'td')}</tr>`)
.join('');
return `<table class="${styles.table}"><thead>${headers}</thead><tbody>${rows}</tbody></table>`;
};
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 : ''}</span>`;
} else {
resultHTML += token;
}
continue;
}
if (inMonospaced) {
if (token === '`' && isClose) {
inMonospaced = false;
stack.pop();
resultHTML += `${keepToken ? token : ''}</span>`;
} else {
resultHTML += token;
}
continue;
}
const headerMatch = token.match(headerRegex);
if (headerMatch) {
if (inHeader) resultHTML += '</span>';
if (inBlockquote) { resultHTML += '</span>'; inBlockquote = false; }
const markup = keepMarkup ? headerMatch[0] : '';
const len = headerMatch[0].length;
inHeader = true;
resultHTML += `${token.slice(0, -len)}<span class="${clsx(styles.header, styles[`header${len - 1}`])}">${markup}`;
continue;
}
const blockquoteMatch = token.match(blockquoteRegex);
if (blockquoteMatch) {
if (inHeader) { resultHTML += '</span>'; inHeader = false; }
if (inBlockquote) resultHTML += '</span>';
const markup = keepMarkup ? '> ' : '';
inBlockquote = true;
resultHTML += `${token.slice(0, -2)}<span class="${styles.blockquote}">${markup}`;
continue;
}
if (token === '\n') {
if (inHeader) {
resultHTML += `${keepMarkup ? '\n' : ''}</span>`;
inHeader = false;
} else if (inBlockquote) {
resultHTML += `${keepMarkup ? '\n' : ''}</span>`;
inBlockquote = false;
} else {
resultHTML += '\n';
}
continue;
}
if (isClose) {
stack.pop();
resultHTML += `${keepToken ? token : ''}</span>`;
} else if (token === '*') {
stack.push(token);
resultHTML += `<span class="${styles.italic}">${keepToken ? token : ''}`;
} else if (token === '**') {
stack.push(token);
resultHTML += `<span class="${styles.bold}">${keepToken ? token : ''}`;
} else if (token === '"') {
stack.push(token);
resultHTML += `<span class="${styles.quote}">"`;
} else if (token === '```') {
stack.push(token);
inCodeBlock = true;
resultHTML += `<span class="${styles.codeBlock}">${keepToken ? token : ''}`;
} else if (token === '`') {
stack.push(token);
inMonospaced = true;
resultHTML += `<span class="${styles.inlineCode}">${keepToken ? token : ''}`;
}
}
resultHTML += message.slice(lastIndex);
if (inHeader) resultHTML += '</span>';
if (inBlockquote) resultHTML += '</span>';
resultHTML += '</span>'.repeat(stack.length);
if (!keepMarkup) {
resultHTML = resultHTML.replace(/((?:(?:^|\n)\|.+)+)/g, match => parseTable(match));
resultHTML = resultHTML.replace(/(^|\n)---(\n|$)/g, (_, pre, post) => `${pre}<hr class="${styles.hr}">${post}`);
resultHTML = resultHTML.replace(/((?:(?:^|\n)[-+] .+)+)/g, match => parseList(match, false));
resultHTML = resultHTML.replace(/((?:(?:^|\n)\d+\. .+)+)/g, match => parseList(match, true));
}
return resultHTML;
}