Skip to content

Commit 77eb0fd

Browse files
zhangguiping-xydtobviyus
authored andcommitted
fix(telegram): preserve rich table styling
1 parent f0be8e7 commit 77eb0fd

5 files changed

Lines changed: 55 additions & 9 deletions

File tree

extensions/telegram/src/format.test.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -254,7 +254,7 @@ describe("markdownToTelegramHtml", () => {
254254
`| ${Array.from({ length: columns }, (_, index) => String(index + 1)).join(" | ")} |`,
255255
].join("\n");
256256

257-
expect(markdownToTelegramRichHtml(table(20))).toContain("<table>");
257+
expect(markdownToTelegramRichHtml(table(20))).toContain("<table bordered striped>");
258258
expect(markdownToTelegramRichHtml(table(21))).toContain("<pre><code>");
259259
expect(markdownToTelegramRichHtml(table(2), { tableMode: "code" })).toContain("<pre><code>");
260260
expect(markdownToTelegramRichHtml(table(2), { tableMode: "code" })).not.toContain("<table>");
@@ -295,6 +295,19 @@ describe("markdownToTelegramHtml", () => {
295295
expect(html).toContain('<td><a href="https://example.com">docs</a></td>');
296296
});
297297

298+
it("preserves markdown table column alignment in rich tables", () => {
299+
const html = markdownToTelegramRichHtml(
300+
"| Feature | Status | Count |\n| :--- | :---: | ---: |\n| Rich tables | Fixed | 2 |",
301+
);
302+
303+
expect(html).toContain('<th align="left">Feature</th>');
304+
expect(html).toContain('<th align="center">Status</th>');
305+
expect(html).toContain('<th align="right">Count</th>');
306+
expect(html).toContain('<td align="left">Rich tables</td>');
307+
expect(html).toContain('<td align="center">Fixed</td>');
308+
expect(html).toContain('<td align="right">2</td>');
309+
});
310+
298311
it("does not auto-linkify bare URLs when entity detection is skipped", () => {
299312
expect(markdownToTelegramRichHtml("https://example.com", { skipEntityDetection: true })).toBe(
300313
"https://example.com",

extensions/telegram/src/format.ts

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import {
88
markdownToIRWithMeta,
99
type MarkdownLinkSpan,
1010
type MarkdownIR,
11+
type MarkdownTableAlignment,
1112
type MarkdownTableCell,
1213
type MarkdownTableMeta,
1314
renderMarkdownIRChunksWithinLimit,
@@ -972,19 +973,25 @@ function renderTelegramRichHtmlTable(table: MarkdownTableMeta): string {
972973
}
973974
const renderCellValue = (cell: MarkdownTableCell | undefined) =>
974975
cell ? renderTelegramHtml(cell) : "";
975-
const renderCell = (tag: "td" | "th", value: MarkdownTableCell | undefined) =>
976-
`<${tag}>${renderCellValue(value)}</${tag}>`;
976+
const renderCell = (
977+
tag: "td" | "th",
978+
value: MarkdownTableCell | undefined,
979+
align: MarkdownTableAlignment | undefined,
980+
) => {
981+
const alignAttr = align ? ` align="${align}"` : "";
982+
return `<${tag}${alignAttr}>${renderCellValue(value)}</${tag}>`;
983+
};
977984
const head = table.headers.length
978-
? `<thead><tr>${table.headerCells.map((cell) => renderCell("th", cell)).join("")}</tr></thead>`
985+
? `<thead><tr>${table.headerCells.map((cell, index) => renderCell("th", cell, table.aligns?.[index])).join("")}</tr></thead>`
979986
: "";
980987
const bodyRows = table.rowCells
981988
.map(
982989
(row) =>
983-
`<tr>${Array.from({ length: columnCount }, (_value, index) => renderCell("td", row[index])).join("")}</tr>`,
990+
`<tr>${Array.from({ length: columnCount }, (_value, index) => renderCell("td", row[index], table.aligns?.[index])).join("")}</tr>`,
984991
)
985992
.join("");
986993
const body = bodyRows ? `<tbody>${bodyRows}</tbody>` : "";
987-
return `<table>${head}${body}</table>\n\n`;
994+
return `<table bordered striped>${head}${body}</table>\n\n`;
988995
}
989996

990997
function renderTelegramRichHtmlDocument(

extensions/telegram/src/send.test.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -953,7 +953,7 @@ describe("sendMessageTelegram", () => {
953953

954954
expect(botRawApi.sendRichMessage).toHaveBeenCalledTimes(1);
955955
const richMessage = botRawApi.sendRichMessage.mock.calls[0]?.[0]?.rich_message;
956-
expect(richMessage?.html).toContain("<table>");
956+
expect(richMessage?.html).toContain("<table bordered striped>");
957957
});
958958

959959
it("skips rich entity detection for provider-prefixed email text", async () => {

packages/markdown-core/src/ir.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -78,9 +78,12 @@ function createStyleSpan(params: MarkdownStyleSpan): MarkdownStyleSpan {
7878
return span;
7979
}
8080

81+
export type MarkdownTableAlignment = "left" | "center" | "right";
82+
8183
export type MarkdownTableData = {
8284
headers: string[];
8385
rows: string[][];
86+
aligns?: (MarkdownTableAlignment | undefined)[];
8487
};
8588

8689
export type MarkdownTableCell = {
@@ -113,6 +116,7 @@ type TableCell = MarkdownTableCell;
113116
type TableState = {
114117
headers: TableCell[];
115118
rows: TableCell[][];
119+
aligns: (MarkdownTableAlignment | undefined)[];
116120
currentRow: TableCell[];
117121
currentCell: RenderTarget | null;
118122
inHeader: boolean;
@@ -172,6 +176,20 @@ function getAttr(token: MarkdownToken, name: string): string | null {
172176
return null;
173177
}
174178

179+
function markdownTableAlignmentFromToken(token: MarkdownToken): MarkdownTableAlignment | undefined {
180+
const value = getAttr(token, "style") ?? "";
181+
if (/text-align\s*:\s*left/i.test(value)) {
182+
return "left";
183+
}
184+
if (/text-align\s*:\s*center/i.test(value)) {
185+
return "center";
186+
}
187+
if (/text-align\s*:\s*right/i.test(value)) {
188+
return "right";
189+
}
190+
return undefined;
191+
}
192+
175193
function createTextToken(base: MarkdownToken, content: string): MarkdownToken {
176194
return { ...base, type: "text", content, children: undefined };
177195
}
@@ -432,6 +450,7 @@ function initTableState(): TableState {
432450
return {
433451
headers: [],
434452
rows: [],
453+
aligns: [],
435454
currentRow: [],
436455
currentCell: null,
437456
inHeader: false,
@@ -517,13 +536,15 @@ function collectTableBlock(state: RenderState) {
517536
}
518537
const headerCells = state.table.headers.map(trimCell);
519538
const rowCells = state.table.rows.map((row) => row.map(trimCell));
520-
state.collectedTables.push({
539+
const table = {
521540
headers: headerCells.map((cell) => cell.text),
522541
rows: rowCells.map((row) => row.map((cell) => cell.text)),
523542
headerCells,
524543
rowCells,
525544
placeholderOffset: state.text.length,
526-
});
545+
...(state.table.aligns.some(Boolean) ? { aligns: [...state.table.aligns] } : {}),
546+
};
547+
state.collectedTables.push(table);
527548
}
528549

529550
function appendTableBulletValue(
@@ -874,6 +895,10 @@ function renderTokens(tokens: MarkdownToken[], state: RenderState): void {
874895
case "td_open":
875896
if (state.table) {
876897
state.table.currentCell = initRenderTarget();
898+
if (token.type === "th_open" && state.table.inHeader) {
899+
state.table.aligns[state.table.currentRow.length] =
900+
markdownTableAlignmentFromToken(token);
901+
}
877902
}
878903
break;
879904
case "th_close":

src/plugin-sdk/text-chunking.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ export {
2525
type MarkdownParseOptions,
2626
type MarkdownStyle,
2727
type MarkdownStyleSpan,
28+
type MarkdownTableAlignment,
2829
type MarkdownTableCell,
2930
type MarkdownTableMeta,
3031
} from "../../packages/markdown-core/src/ir.js";

0 commit comments

Comments
 (0)