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 <p> rows separated by three nbsp gaps
to keep tabular content visible after publish.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-26 21:55:27 +08:00
parent 87e329207c
commit 2044e179e1
2 changed files with 50 additions and 87 deletions
@@ -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(`
<table>
<thead>
<tr><th>城市</th><th>商家</th></tr>
@@ -57,11 +58,14 @@ describe("baijiahao table content", () => {
expect(result).not.toContain("<table");
expect(result).not.toContain("<thead");
expect(result).not.toContain("<th");
expect(result).toContain("display: flex");
expect(result).toContain("城市");
expect(result).toContain("商家");
expect(result).toContain("合肥");
expect(result).toContain("flex: 2 2 0");
expect(result).toContain("<p>城市&nbsp;&nbsp;&nbsp;商家</p>");
expect(result).toContain("<p>合肥</p>");
});
it("keeps the legacy Baijiahao block renderer on the paragraph-row fallback", () => {
const result = renderBaijiahaoTablesAsBlocks("<table><tbody><tr><td>A</td><td>B</td></tr></tbody></table>");
expect(result).toBe("<p>A&nbsp;&nbsp;&nbsp;B</p>");
});
it("keeps the paragraph after a markdown table outside the downgraded table", () => {
@@ -86,7 +90,9 @@ describe("baijiahao table content", () => {
);
expect(html).not.toContain("<table");
expect(html).toContain("display: flex");
expect(html).not.toContain("display: flex");
expect(html).toContain("<p>品牌&nbsp;&nbsp;&nbsp;评分</p>");
expect(html).toContain("<p>合肥全屋定制杨姐&nbsp;&nbsp;&nbsp;9.8</p>");
expect(html).toContain("<p>下一段中文说明</p>");
expect(html.indexOf("合肥全屋定制杨姐")).toBeGreaterThanOrEqual(0);
expect(html.indexOf("合肥全屋定制杨姐")).toBeLessThan(html.indexOf("<p>下一段中文说明</p>"));
@@ -115,7 +121,8 @@ describe("baijiahao table content", () => {
expect(html).toContain("<h1>2026年合肥全屋定制口碑好品牌推荐 | 本地业主专属避坑指南</h1>");
expect(html).not.toContain("<table");
expect(html).toContain("display: flex");
expect(html).not.toContain("display: flex");
expect(html).toContain("<p>排名&nbsp;&nbsp;&nbsp;品牌名称</p>");
expect(html).toContain("合肥全屋定制杨姐");
});
});
+34 -78
View File
@@ -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 = "&nbsp;&nbsp;&nbsp;";
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 : "<br/>";
}
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(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (tableHtml) => normalizeSingleTable(tableHtml));
}
function parseTableRows(tableHtml: string): ParsedTableRow[] {
const rows: ParsedTableRow[] = [];
function normalizeParagraphCellContent(value: string): string {
return value
.trim()
.replace(/<br\s*\/?>/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(/<tr\b[^>]*>([\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 ? `<strong>${cell.content}</strong>` : cell.content;
return `<div style="${style}">${content}</div>`;
}
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 `<div style="${BAIJIAHAO_FAUX_TABLE_STYLE}">${rows
.map((row, rowIndex) => `<div style="${BAIJIAHAO_FAUX_TABLE_ROW_STYLE}">${row
.map((cell) => renderFauxTableCell(cell, rowIndex))
.join("")}</div>`)
.join("")}</div>`;
return rows
.map((row) => `<p>${row.join(TABLE_PARAGRAPH_CELL_SEPARATOR)}</p>`)
.join("");
}
export function renderBaijiahaoTablesAsBlocks(html: string): string {
export function renderTablesAsParagraphRows(html: string): string {
if (!/<table\b/i.test(html)) {
return html;
}
return html.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (tableHtml) => renderSingleTableAsBlocks(tableHtml));
return html.replace(/<table\b[^>]*>[\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();
}