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 `
| ]*)>([\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(/ |
|---|