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(); }