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