const BAIJIAHAO_TABLE_STYLE = [ 'width: 100%', 'border-collapse: collapse', 'border-spacing: 0', 'margin: 12px 0', 'font-size: 15px', 'line-height: 1.6', 'color: #1f2937', ].join('; ') const BAIJIAHAO_TABLE_CELL_STYLE = [ 'border: 1px solid #d9d9d9', 'padding: 8px 10px', 'vertical-align: middle', 'word-break: break-word', 'min-width: 64px', ].join('; ') const BAIJIAHAO_TABLE_HEADER_CELL_STYLE = [ BAIJIAHAO_TABLE_CELL_STYLE, 'background-color: #f5f7fa', 'font-weight: 600', ].join('; ') const TABLE_PARAGRAPH_CELL_SEPARATOR = '   ' const BAIJIAHAO_TABLE_ALLOWED_ATTRS = ['colspan', 'rowspan', 'align', 'valign', 'width'] type ParsedParagraphTableRow = string[] function hasUnescapedPipe(value: string): boolean { let escaped = false for (const char of value) { if (escaped) { escaped = false continue } if (char === '\\') { escaped = true continue } if (char === '|') { return true } } return false } function splitMarkdownTableCells(value: string): string[] { const cells: string[] = [] let cell = '' let escaped = false const trimmed = value.trim() const source = trimmed.startsWith('|') ? trimmed.slice(1) : trimmed const content = source.endsWith('|') && !source.endsWith('\\|') ? source.slice(0, -1) : source for (const char of content) { if (escaped) { cell += char escaped = false continue } if (char === '\\') { escaped = true cell += char continue } if (char === '|') { cells.push(cell.trim()) cell = '' continue } cell += char } cells.push(cell.trim()) return cells } function isMarkdownTableRow(value: string): boolean { const trimmed = value.trim() return trimmed !== '' && hasUnescapedPipe(trimmed) && splitMarkdownTableCells(trimmed).length >= 2 } function isMarkdownTableSeparator(value: string): boolean { const cells = splitMarkdownTableCells(value).filter(Boolean) return cells.length >= 2 && cells.every((cell) => /^:?-{2,}:?$/.test(cell.trim())) } function isMarkdownTableStart(lines: string[], index: number): boolean { return isMarkdownTableRow(lines[index] ?? '') && isMarkdownTableSeparator(lines[index + 1] ?? '') } function isFenceBoundary(value: string): boolean { return /^\s*(```|~~~)/.test(value) } export function prepareBaijiahaoMarkdown(markdown: string): string { const source = markdown.replace(/\r\n?/g, '\n').trim() if (!source) { return '' } const lines = source.split('\n') const output: string[] = [] let inCodeFence = false for (let index = 0; index < lines.length; index += 1) { const line = lines[index] ?? '' if (isFenceBoundary(line)) { inCodeFence = !inCodeFence output.push(line) continue } if (!inCodeFence && isMarkdownTableStart(lines, index)) { while (output.length > 0 && output[output.length - 1]?.trim() === '') { output.pop() } if (/^\s*#{1,6}\s*$/.test(output[output.length - 1] ?? '')) { output.pop() } if (output.length > 0 && output[output.length - 1]?.trim() !== '') { output.push('') } output.push(lines[index] ?? '') output.push(lines[index + 1] ?? '') index += 2 while (index < lines.length && isMarkdownTableRow(lines[index] ?? '')) { output.push(lines[index] ?? '') index += 1 } if (index < lines.length && lines[index]?.trim() !== '') { output.push('') } index -= 1 continue } output.push(line) } return output.join('\n').trim() } function escapeAttribute(value: string): string { return value .replace(/&/g, '&') .replace(/"/g, '"') .replace(//g, '>') } function extractAttribute(sourceTag: string, attributeName: string): string | null { const matcher = new RegExp( `\\s${attributeName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, 'i', ) const match = matcher.exec(sourceTag) const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? '' const trimmed = value.trim() return trimmed ? trimmed : null } function allowedTableAttributes(sourceTag: string): string { return BAIJIAHAO_TABLE_ALLOWED_ATTRS.map((attributeName) => { const value = extractAttribute(sourceTag, attributeName) return value ? `${attributeName}="${escapeAttribute(value)}"` : '' }) .filter(Boolean) .join(' ') } function normalizeCellContent(value: string): string { const trimmed = value.trim() return trimmed ? trimmed : '
' } function normalizeTableCell(sourceTag: string, content: string, header: boolean): string { const attrs = allowedTableAttributes(sourceTag) const attrPrefix = attrs ? `${attrs} ` : '' const style = header ? BAIJIAHAO_TABLE_HEADER_CELL_STYLE : BAIJIAHAO_TABLE_CELL_STYLE const normalizedContent = normalizeCellContent(content) const cellContent = header ? `${normalizedContent}` : normalizedContent return `${cellContent}` } function normalizeSingleTable(tableHtml: string): string { let body = tableHtml .replace(/^\s*]*>/i, '') .replace(/<\/table>\s*$/i, '') .replace(/]*>[\s\S]*?<\/caption>/gi, '') .replace(/]*>[\s\S]*?<\/colgroup>/gi, '') .replace(/]*\/?>/gi, '') .replace(/<\/?(?:thead|tbody|tfoot)\b[^>]*>/gi, '') body = body.replace(/]*)>([\s\S]*?)<\/th>/gi, (_match, attrs: string, content: string) => normalizeTableCell(``, content, true), ) body = body.replace(/]*)>([\s\S]*?)<\/td>/gi, (_match, attrs: string, content: string) => normalizeTableCell(``, content, false), ) let rowIndex = 0 body = body.replace(/]*>/gi, () => { rowIndex += 1 return rowIndex === 1 ? `` : '' }) return `${body.trim()}
` } export function normalizeBaijiahaoTables(html: string): string { if (!/]*>[\s\S]*?<\/table>/gi, (tableHtml) => normalizeSingleTable(tableHtml), ) } function normalizeParagraphCellContent(value: string): string { return value .trim() .replace(//gi, ' ') .replace( /<\/?(?:p|div|section|article|header|footer|ul|ol|li|blockquote|h[1-6])\b[^>]*>/gi, ' ', ) .replace(/\s+/g, ' ') .trim() } function parseParagraphTableRows(tableHtml: string): ParsedParagraphTableRow[] { const rows: ParsedParagraphTableRow[] = [] for (const rowMatch of tableHtml.matchAll(/]*>([\s\S]*?)<\/tr>/gi)) { const cells: ParsedParagraphTableRow = [] const rowContent = rowMatch[1] ?? '' for (const cellMatch of rowContent.matchAll(/<(?:th|td)\b[^>]*>([\s\S]*?)<\/(?:th|td)>/gi)) { const content = normalizeParagraphCellContent(cellMatch[1] ?? '') if (content) { cells.push(content) } } if (cells.length > 0) { rows.push(cells) } } return rows } function renderSingleTableAsParagraphRows(tableHtml: string): string { const rows = parseParagraphTableRows(tableHtml) if (!rows.length) { return '' } return rows.map((row) => `

${row.join(TABLE_PARAGRAPH_CELL_SEPARATOR)}

`).join('') } export function renderTablesAsParagraphRows(html: string): string { if (!/]*>[\s\S]*?<\/table>/gi, (tableHtml) => renderSingleTableAsParagraphRows(tableHtml), ) } export function renderBaijiahaoTablesAsBlocks(html: string): string { return renderTablesAsParagraphRows(html) } export function prepareBaijiahaoArticleHtml(html: string): string { return renderTablesAsParagraphRows(html).trim() }