fix(telegram): preserve spacing before numbered sections

This commit is contained in:
evgyur
2026-05-04 02:31:29 +03:00
committed by Ayaan Zaidi
parent 5710a89e6c
commit d2649e0410
2 changed files with 68 additions and 2 deletions

View File

@@ -95,6 +95,33 @@ describe("markdownToTelegramHtml", () => {
expect(res).toBe("<tg-spoiler><b>secret</b> text</tg-spoiler>");
});
it("preserves spacing between Telegram bullet blocks and following numbered sections", () => {
const input = [
"2. Main invariants:",
"",
" • Raw Log is source of truth.",
" • Autonomy starts only with report/draft.",
"3. Cognee is a candidate:",
"",
" • bake-off first;",
" • decide keep/adopt/hybrid later.",
"4. Project Flow slices:",
].join("\n");
const res = markdownToTelegramHtml(input, { wrapFileRefs: false });
expect(res).toContain("report/draft.\n\n3. Cognee");
expect(res).toContain("keep/adopt/hybrid later.\n\n4. Project");
});
it("does not insert Telegram list boundary spacing inside fenced code", () => {
const input = ["```", " • literal bullet", "3. literal number", "```"].join("\n");
const res = markdownToTelegramHtml(input, { wrapFileRefs: false });
expect(res).toBe("<pre><code> • literal bullet\n3. literal number\n</code></pre>");
});
it("does not treat single pipe as spoiler", () => {
const res = markdownToTelegramHtml("( ̄_ ̄|) face");
expect(res).not.toContain("tg-spoiler");

View File

@@ -72,11 +72,50 @@ function renderTelegramHtml(ir: MarkdownIR): string {
});
}
function leadingWhitespaceLength(line: string): number {
return line.match(/^[ \t]*/)?.[0]?.length ?? 0;
}
function isTelegramBulletLine(line: string): boolean {
return /^[ \t]*(?:[•*+-])[ \t]+\S/.test(line);
}
function isTelegramListBoundaryLine(line: string): boolean {
return /^[ \t]*(?:\d+\.|#{1,6})[ \t]+\S/.test(line);
}
function preserveTelegramListBoundarySpacing(markdown: string): string {
const lines = markdown.split("\n");
const out: string[] = [];
let inFence = false;
for (const line of lines) {
const normalizedLine = line.replace(/\r$/, "");
const isFenceLine = /^[ \t]*(?:```|~~~)/.test(normalizedLine);
if (!inFence && out.length > 0) {
const previous = out[out.length - 1] ?? "";
if (
isTelegramBulletLine(previous) &&
isTelegramListBoundaryLine(normalizedLine) &&
leadingWhitespaceLength(normalizedLine) <= leadingWhitespaceLength(previous)
) {
out.push("");
}
}
out.push(line);
if (isFenceLine) {
inFence = !inFence;
}
}
return out.join("\n");
}
export function markdownToTelegramHtml(
markdown: string,
options: { tableMode?: MarkdownTableMode; wrapFileRefs?: boolean } = {},
): string {
const ir = markdownToIR(markdown ?? "", {
const ir = markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), {
linkify: true,
enableSpoilers: true,
headingStyle: "none",
@@ -458,7 +497,7 @@ export function markdownToTelegramChunks(
limit: number,
options: { tableMode?: MarkdownTableMode } = {},
): TelegramFormattedChunk[] {
const ir = markdownToIR(markdown ?? "", {
const ir = markdownToIR(preserveTelegramListBoundarySpacing(markdown ?? ""), {
linkify: true,
enableSpoilers: true,
headingStyle: "none",