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:
@@ -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>城市 商家</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 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>品牌 评分</p>");
|
||||
expect(html).toContain("<p>合肥全屋定制杨姐 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>排名 品牌名称</p>");
|
||||
expect(html).toContain("合肥全屋定制杨姐");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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 : "<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();
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user