fix(markdown): require paired || delimiters for spoiler detection (#26105)

* fix(markdown): require paired || delimiters for spoiler detection

An unpaired || (odd count across all inline tokens) would open a
spoiler that never closes, causing closeRemainingStyles to extend it
to the end of the text. This made all content after an unpaired ||
appear as hidden/spoiler in Telegram.

Pre-count || delimiters across the entire inline token group and skip
spoiler injection entirely when the count is less than 2 or odd. This
prevents single | characters and unpaired || from triggering spoiler
formatting.

Closes #26068

Co-authored-by: Cursor <cursoragent@cursor.com>

* fix: preserve valid spoiler pairs with trailing unmatched delimiters (#26105) (thanks @Sid-Qin)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Peter Steinberger <steipete@gmail.com>
This commit is contained in:
Sid
2026-02-25 12:54:51 +08:00
committed by GitHub
parent 156f13aa64
commit 2e84017f23
3 changed files with 47 additions and 0 deletions

View File

@@ -144,8 +144,31 @@ function applySpoilerTokens(tokens: MarkdownToken[]): void {
}
function injectSpoilersIntoInline(tokens: MarkdownToken[]): MarkdownToken[] {
let totalDelims = 0;
for (const token of tokens) {
if (token.type !== "text") {
continue;
}
const content = token.content ?? "";
let i = 0;
while (i < content.length) {
const next = content.indexOf("||", i);
if (next === -1) {
break;
}
totalDelims += 1;
i = next + 2;
}
}
if (totalDelims < 2) {
return tokens;
}
const usableDelims = totalDelims - (totalDelims % 2);
const result: MarkdownToken[] = [];
const state = { spoilerOpen: false };
let consumedDelims = 0;
for (const token of tokens) {
if (token.type !== "text") {
@@ -168,9 +191,14 @@ function injectSpoilersIntoInline(tokens: MarkdownToken[]): MarkdownToken[] {
}
break;
}
if (consumedDelims >= usableDelims) {
result.push(createTextToken(token, content.slice(index)));
break;
}
if (next > index) {
result.push(createTextToken(token, content.slice(index, next)));
}
consumedDelims += 1;
state.spoilerOpen = !state.spoilerOpen;
result.push({
type: state.spoilerOpen ? "spoiler_open" : "spoiler_close",

View File

@@ -94,4 +94,22 @@ describe("markdownToTelegramHtml", () => {
const res = markdownToTelegramHtml("||**secret** text||");
expect(res).toBe("<tg-spoiler><b>secret</b> text</tg-spoiler>");
});
it("does not treat single pipe as spoiler", () => {
const res = markdownToTelegramHtml("( ̄_ ̄|) face");
expect(res).not.toContain("tg-spoiler");
expect(res).toContain("|");
});
it("does not treat unpaired || as spoiler", () => {
const res = markdownToTelegramHtml("before || after");
expect(res).not.toContain("tg-spoiler");
expect(res).toContain("||");
});
it("keeps valid spoiler pairs when a trailing || is unmatched", () => {
const res = markdownToTelegramHtml("||secret|| trailing ||");
expect(res).toContain("<tg-spoiler>secret</tg-spoiler>");
expect(res).toContain("trailing ||");
});
});