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, prepareBaijiahaoArticleHtml,
prepareBaijiahaoMarkdown, prepareBaijiahaoMarkdown,
renderBaijiahaoTablesAsBlocks, renderBaijiahaoTablesAsBlocks,
renderTablesAsParagraphRows,
} from "../../../../../packages/publisher-platforms/src/baijiahao"; } from "../../../../../packages/publisher-platforms/src/baijiahao";
import { normalizeArticleHtml } from "./common"; import { normalizeArticleHtml } from "./common";
@@ -42,8 +43,8 @@ describe("baijiahao table content", () => {
expect(result).toContain("汇总"); expect(result).toContain("汇总");
}); });
it("downgrades tables into visible div blocks for Baijiahao", () => { it("downgrades tables into paragraph rows with three-space column separators", () => {
const result = renderBaijiahaoTablesAsBlocks(` const result = renderTablesAsParagraphRows(`
<table> <table>
<thead> <thead>
<tr><th>城市</th><th>商家</th></tr> <tr><th>城市</th><th>商家</th></tr>
@@ -57,11 +58,14 @@ describe("baijiahao table content", () => {
expect(result).not.toContain("<table"); expect(result).not.toContain("<table");
expect(result).not.toContain("<thead"); expect(result).not.toContain("<thead");
expect(result).not.toContain("<th"); expect(result).not.toContain("<th");
expect(result).toContain("display: flex"); expect(result).toContain("<p>城市&nbsp;&nbsp;&nbsp;商家</p>");
expect(result).toContain("城市"); expect(result).toContain("<p>合肥</p>");
expect(result).toContain("商家"); });
expect(result).toContain("合肥");
expect(result).toContain("flex: 2 2 0"); 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", () => { 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).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).toContain("<p>下一段中文说明</p>");
expect(html.indexOf("合肥全屋定制杨姐")).toBeGreaterThanOrEqual(0); expect(html.indexOf("合肥全屋定制杨姐")).toBeGreaterThanOrEqual(0);
expect(html.indexOf("合肥全屋定制杨姐")).toBeLessThan(html.indexOf("<p>下一段中文说明</p>")); expect(html.indexOf("合肥全屋定制杨姐")).toBeLessThan(html.indexOf("<p>下一段中文说明</p>"));
@@ -115,7 +121,8 @@ describe("baijiahao table content", () => {
expect(html).toContain("<h1>2026年合肥全屋定制口碑好品牌推荐 | 本地业主专属避坑指南</h1>"); expect(html).toContain("<h1>2026年合肥全屋定制口碑好品牌推荐 | 本地业主专属避坑指南</h1>");
expect(html).not.toContain("<table"); 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("合肥全屋定制杨姐"); expect(html).toContain("合肥全屋定制杨姐");
}); });
}); });
+34 -78
View File
@@ -22,50 +22,11 @@ const BAIJIAHAO_TABLE_HEADER_CELL_STYLE = [
"font-weight: 600", "font-weight: 600",
].join("; "); ].join("; ");
const BAIJIAHAO_FAUX_TABLE_STYLE = [ const TABLE_PARAGRAPH_CELL_SEPARATOR = "&nbsp;&nbsp;&nbsp;";
"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 BAIJIAHAO_TABLE_ALLOWED_ATTRS = ["colspan", "rowspan", "align", "valign", "width"]; const BAIJIAHAO_TABLE_ALLOWED_ATTRS = ["colspan", "rowspan", "align", "valign", "width"];
interface ParsedTableCell { type ParsedParagraphTableRow = string[];
content: string;
header: boolean;
colSpan: number;
}
type ParsedTableRow = ParsedTableCell[];
function hasUnescapedPipe(value: string): boolean { function hasUnescapedPipe(value: string): boolean {
let escaped = false; let escaped = false;
@@ -216,12 +177,6 @@ function normalizeCellContent(value: string): string {
return trimmed ? trimmed : "<br/>"; 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 { function normalizeTableCell(sourceTag: string, content: string, header: boolean): string {
const attrs = allowedTableAttributes(sourceTag); const attrs = allowedTableAttributes(sourceTag);
const attrPrefix = attrs ? `${attrs} ` : ""; 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)); return html.replace(/<table\b[^>]*>[\s\S]*?<\/table>/gi, (tableHtml) => normalizeSingleTable(tableHtml));
} }
function parseTableRows(tableHtml: string): ParsedTableRow[] { function normalizeParagraphCellContent(value: string): string {
const rows: ParsedTableRow[] = []; 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)) { for (const rowMatch of tableHtml.matchAll(/<tr\b[^>]*>([\s\S]*?)<\/tr>/gi)) {
const cells: ParsedTableRow = []; const cells: ParsedParagraphTableRow = [];
const rowContent = rowMatch[1] ?? ""; const rowContent = rowMatch[1] ?? "";
for (const cellMatch of rowContent.matchAll(/<(th|td)\b([^>]*)>([\s\S]*?)<\/\1>/gi)) { for (const cellMatch of rowContent.matchAll(/<(?:th|td)\b[^>]*>([\s\S]*?)<\/(?:th|td)>/gi)) {
const sourceTag = `<${cellMatch[1]}${cellMatch[2] ?? ""}>`; const content = normalizeParagraphCellContent(cellMatch[1] ?? "");
cells.push({ if (content) {
content: normalizeCellContent(cellMatch[3] ?? ""), cells.push(content);
header: cellMatch[1]?.toLowerCase() === "th", }
colSpan: normalizeSpanAttribute(sourceTag, "colspan"),
});
} }
if (cells.length > 0) { if (cells.length > 0) {
rows.push(cells); rows.push(cells);
@@ -283,36 +245,30 @@ function parseTableRows(tableHtml: string): ParsedTableRow[] {
return rows; return rows;
} }
function renderFauxTableCell(cell: ParsedTableCell, rowIndex: number): string { function renderSingleTableAsParagraphRows(tableHtml: string): string {
const header = cell.header || rowIndex === 0; const rows = parseParagraphTableRows(tableHtml);
const baseStyle = header ? BAIJIAHAO_FAUX_TABLE_HEADER_CELL_STYLE : BAIJIAHAO_FAUX_TABLE_CELL_STYLE; if (!rows.length) {
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) {
return ""; return "";
} }
return `<div style="${BAIJIAHAO_FAUX_TABLE_STYLE}">${rows return rows
.map((row, rowIndex) => `<div style="${BAIJIAHAO_FAUX_TABLE_ROW_STYLE}">${row .map((row) => `<p>${row.join(TABLE_PARAGRAPH_CELL_SEPARATOR)}</p>`)
.map((cell) => renderFauxTableCell(cell, rowIndex)) .join("");
.join("")}</div>`)
.join("")}</div>`;
} }
export function renderBaijiahaoTablesAsBlocks(html: string): string { export function renderTablesAsParagraphRows(html: string): string {
if (!/<table\b/i.test(html)) { if (!/<table\b/i.test(html)) {
return 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 { export function prepareBaijiahaoArticleHtml(html: string): string {
// Baijiahao's preview/final article page does not consistently render native table tags, return renderTablesAsParagraphRows(html).trim();
// so publish uses ordinary div blocks to preserve visible tabular content.
return renderBaijiahaoTablesAsBlocks(html).trim();
} }