From 2044e179e11d2ba7e3914fd0194b0968369fbc40 Mon Sep 17 00:00:00 2001 From: liangxu Date: Sun, 26 Apr 2026 21:55:27 +0800 Subject: [PATCH] refactor(publish): downgrade baijiahao tables to paragraph rows Baijiahao does not consistently render the previous flex-based faux table markup, so reduce tables to plain

rows separated by three nbsp gaps to keep tabular content visible after publish. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/main/adapters/baijiahao.test.ts | 25 ++-- packages/publisher-platforms/src/baijiahao.ts | 112 ++++++------------ 2 files changed, 50 insertions(+), 87 deletions(-) diff --git a/apps/desktop-client/src/main/adapters/baijiahao.test.ts b/apps/desktop-client/src/main/adapters/baijiahao.test.ts index 1c577ff..9a79192 100644 --- a/apps/desktop-client/src/main/adapters/baijiahao.test.ts +++ b/apps/desktop-client/src/main/adapters/baijiahao.test.ts @@ -5,6 +5,7 @@ import { prepareBaijiahaoArticleHtml, prepareBaijiahaoMarkdown, renderBaijiahaoTablesAsBlocks, + renderTablesAsParagraphRows, } from "../../../../../packages/publisher-platforms/src/baijiahao"; import { normalizeArticleHtml } from "./common"; @@ -42,8 +43,8 @@ describe("baijiahao table content", () => { expect(result).toContain("汇总"); }); - it("downgrades tables into visible div blocks for Baijiahao", () => { - const result = renderBaijiahaoTablesAsBlocks(` + it("downgrades tables into paragraph rows with three-space column separators", () => { + const result = renderTablesAsParagraphRows(` @@ -57,11 +58,14 @@ describe("baijiahao table content", () => { expect(result).not.toContain("城市   商家

"); + expect(result).toContain("

合肥

"); + }); + + it("keeps the legacy Baijiahao block renderer on the paragraph-row fallback", () => { + const result = renderBaijiahaoTablesAsBlocks("
城市商家
AB
"); + + expect(result).toBe("

A   B

"); }); it("keeps the paragraph after a markdown table outside the downgraded table", () => { @@ -86,7 +90,9 @@ describe("baijiahao table content", () => { ); expect(html).not.toContain("品牌   评分

"); + expect(html).toContain("

合肥全屋定制杨姐   9.8

"); expect(html).toContain("

下一段中文说明

"); expect(html.indexOf("合肥全屋定制杨姐")).toBeGreaterThanOrEqual(0); expect(html.indexOf("合肥全屋定制杨姐")).toBeLessThan(html.indexOf("

下一段中文说明

")); @@ -115,7 +121,8 @@ describe("baijiahao table content", () => { expect(html).toContain("

2026年合肥全屋定制口碑好品牌推荐 | 本地业主专属避坑指南

"); expect(html).not.toContain("排名   品牌名称

"); expect(html).toContain("合肥全屋定制杨姐"); }); }); diff --git a/packages/publisher-platforms/src/baijiahao.ts b/packages/publisher-platforms/src/baijiahao.ts index a45293b..a2f589c 100644 --- a/packages/publisher-platforms/src/baijiahao.ts +++ b/packages/publisher-platforms/src/baijiahao.ts @@ -22,50 +22,11 @@ const BAIJIAHAO_TABLE_HEADER_CELL_STYLE = [ "font-weight: 600", ].join("; "); -const BAIJIAHAO_FAUX_TABLE_STYLE = [ - "width: 100%", - "margin: 12px 0", - "border-top: 1px solid #d9d9d9", - "border-left: 1px solid #d9d9d9", - "overflow-x: auto", -].join("; "); - -const BAIJIAHAO_FAUX_TABLE_ROW_STYLE = [ - "display: flex", - "align-items: stretch", - "width: 100%", -].join("; "); - -const BAIJIAHAO_FAUX_TABLE_CELL_STYLE = [ - "flex: 1 1 0", - "min-width: 96px", - "box-sizing: border-box", - "border-right: 1px solid #d9d9d9", - "border-bottom: 1px solid #d9d9d9", - "padding: 8px 10px", - "vertical-align: middle", - "word-break: break-word", - "font-size: 15px", - "line-height: 1.6", - "color: #1f2937", - "background: #ffffff", -].join("; "); - -const BAIJIAHAO_FAUX_TABLE_HEADER_CELL_STYLE = [ - BAIJIAHAO_FAUX_TABLE_CELL_STYLE, - "background: #f5f7fa", - "font-weight: 600", -].join("; "); +const TABLE_PARAGRAPH_CELL_SEPARATOR = "   "; const BAIJIAHAO_TABLE_ALLOWED_ATTRS = ["colspan", "rowspan", "align", "valign", "width"]; -interface ParsedTableCell { - content: string; - header: boolean; - colSpan: number; -} - -type ParsedTableRow = ParsedTableCell[]; +type ParsedParagraphTableRow = string[]; function hasUnescapedPipe(value: string): boolean { let escaped = false; @@ -216,12 +177,6 @@ function normalizeCellContent(value: string): string { return trimmed ? trimmed : "
"; } -function normalizeSpanAttribute(sourceTag: string, attributeName: "colspan" | "rowspan"): number { - const value = extractAttribute(sourceTag, attributeName); - const parsed = value ? Number.parseInt(value, 10) : 1; - return Number.isFinite(parsed) && parsed > 1 ? Math.min(parsed, 12) : 1; -} - function normalizeTableCell(sourceTag: string, content: string, header: boolean): string { const attrs = allowedTableAttributes(sourceTag); const attrPrefix = attrs ? `${attrs} ` : ""; @@ -263,18 +218,25 @@ export function normalizeBaijiahaoTables(html: string): string { return html.replace(/]*>[\s\S]*?<\/table>/gi, (tableHtml) => normalizeSingleTable(tableHtml)); } -function parseTableRows(tableHtml: string): ParsedTableRow[] { - const rows: ParsedTableRow[] = []; +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: ParsedTableRow = []; + const cells: ParsedParagraphTableRow = []; const rowContent = rowMatch[1] ?? ""; - for (const cellMatch of rowContent.matchAll(/<(th|td)\b([^>]*)>([\s\S]*?)<\/\1>/gi)) { - const sourceTag = `<${cellMatch[1]}${cellMatch[2] ?? ""}>`; - cells.push({ - content: normalizeCellContent(cellMatch[3] ?? ""), - header: cellMatch[1]?.toLowerCase() === "th", - colSpan: normalizeSpanAttribute(sourceTag, "colspan"), - }); + 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); @@ -283,36 +245,30 @@ function parseTableRows(tableHtml: string): ParsedTableRow[] { return rows; } -function renderFauxTableCell(cell: ParsedTableCell, rowIndex: number): string { - const header = cell.header || rowIndex === 0; - const baseStyle = header ? BAIJIAHAO_FAUX_TABLE_HEADER_CELL_STYLE : BAIJIAHAO_FAUX_TABLE_CELL_STYLE; - const style = `${baseStyle}; flex: ${cell.colSpan} ${cell.colSpan} 0`; - const content = header ? `${cell.content}` : cell.content; - return `
${content}
`; -} - -function renderSingleTableAsBlocks(tableHtml: string): string { - const rows = parseTableRows(tableHtml); - if (rows.length === 0) { +function renderSingleTableAsParagraphRows(tableHtml: string): string { + const rows = parseParagraphTableRows(tableHtml); + if (!rows.length) { return ""; } - return `
${rows - .map((row, rowIndex) => `
${row - .map((cell) => renderFauxTableCell(cell, rowIndex)) - .join("")}
`) - .join("")}
`; + return rows + .map((row) => `

${row.join(TABLE_PARAGRAPH_CELL_SEPARATOR)}

`) + .join(""); } -export function renderBaijiahaoTablesAsBlocks(html: string): string { +export function renderTablesAsParagraphRows(html: string): string { if (!/]*>[\s\S]*?<\/table>/gi, (tableHtml) => renderSingleTableAsBlocks(tableHtml)); + return html.replace(/]*>[\s\S]*?<\/table>/gi, (tableHtml) => + renderSingleTableAsParagraphRows(tableHtml), + ); +} + +export function renderBaijiahaoTablesAsBlocks(html: string): string { + return renderTablesAsParagraphRows(html); } export function prepareBaijiahaoArticleHtml(html: string): string { - // Baijiahao's preview/final article page does not consistently render native table tags, - // so publish uses ordinary div blocks to preserve visible tabular content. - return renderBaijiahaoTablesAsBlocks(html).trim(); + return renderTablesAsParagraphRows(html).trim(); }