fix(whatsapp): stop markdownToWhatsApp dropping code spans followed by a digit (#93409)

The inline-code/fence restore step matched the placeholder index with a
greedy `(\d+)`, so a digit in user text immediately after a code span
(e.g. `code`5) was absorbed into the index, resolved to undefined, and
`?? ""` deleted both the code span and the digit. Terminate the
placeholder index with the existing NUL marker so the index boundary is
unambiguous.

Co-authored-by: Dr Rushindra Sinha <5796457+rushindrasinha@users.noreply.github.com>
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dr Rushindra Sinha
2026-06-16 00:35:28 +01:00
committed by GitHub
parent f08b24e63c
commit eccfacb02c
2 changed files with 19 additions and 4 deletions

View File

@@ -8,6 +8,9 @@ import { CONFIG_DIR, resolveUserPath } from "openclaw/plugin-sdk/text-utility-ru
const WHATSAPP_FENCE_PLACEHOLDER = "\x00FENCE";
const WHATSAPP_INLINE_CODE_PLACEHOLDER = "\x00CODE";
// Terminates the numeric index in a placeholder so the restore regex cannot
// absorb a digit from adjacent user text (e.g. `code`5) into the index.
const WHATSAPP_PLACEHOLDER_TERMINATOR = "\x00";
export type WebChannel = "web";
@@ -197,25 +200,26 @@ export function markdownToWhatsApp(text: string): string {
const fences: string[] = [];
let result = text.replace(/```[\s\S]*?```/g, (match) => {
fences.push(match);
return `${WHATSAPP_FENCE_PLACEHOLDER}${fences.length - 1}`;
return `${WHATSAPP_FENCE_PLACEHOLDER}${fences.length - 1}${WHATSAPP_PLACEHOLDER_TERMINATOR}`;
});
const inlineCodes: string[] = [];
result = result.replace(/`[^`\n]+`/g, (match) => {
inlineCodes.push(match);
return `${WHATSAPP_INLINE_CODE_PLACEHOLDER}${inlineCodes.length - 1}`;
return `${WHATSAPP_INLINE_CODE_PLACEHOLDER}${inlineCodes.length - 1}${WHATSAPP_PLACEHOLDER_TERMINATOR}`;
});
result = result.replace(/\*\*(.+?)\*\*/g, "*$1*");
result = result.replace(/__(.+?)__/g, "*$1*");
result = result.replace(/~~(.+?)~~/g, "~$1~");
const terminator = escapeRegExp(WHATSAPP_PLACEHOLDER_TERMINATOR);
result = result.replace(
new RegExp(`${escapeRegExp(WHATSAPP_INLINE_CODE_PLACEHOLDER)}(\\d+)`, "g"),
new RegExp(`${escapeRegExp(WHATSAPP_INLINE_CODE_PLACEHOLDER)}(\\d+)${terminator}`, "g"),
(_, idx) => inlineCodes[Number(idx)] ?? "",
);
result = result.replace(
new RegExp(`${escapeRegExp(WHATSAPP_FENCE_PLACEHOLDER)}(\\d+)`, "g"),
new RegExp(`${escapeRegExp(WHATSAPP_FENCE_PLACEHOLDER)}(\\d+)${terminator}`, "g"),
(_, idx) => fences[Number(idx)] ?? "",
);
return result;

View File

@@ -41,6 +41,12 @@ describe("markdownToWhatsApp", () => {
["returns empty string for empty input", "", ""],
["returns plain text unchanged", "no formatting here", "no formatting here"],
["handles bold inside a sentence", "This is **very** important", "This is *very* important"],
// Regression: a digit immediately after an inline-code span must not be
// absorbed into the placeholder index (which previously dropped both).
["preserves inline code immediately followed by a digit", "`a`5", "`a`5"],
["preserves inline code followed by a number", "`status`200 done", "`status`200 done"],
["preserves two adjacent code+digit spans", "`x`1 and `y`2", "`x`1 and `y`2"],
["preserves inline code with a space before a digit", "`a` 5", "`a` 5"],
] as const)("handles markdown-to-whatsapp conversion: %s", (_name, input, expected) => {
expect(markdownToWhatsApp(input)).toBe(expected);
});
@@ -50,6 +56,11 @@ describe("markdownToWhatsApp", () => {
expect(markdownToWhatsApp(input)).toBe(input);
});
it("preserves a fenced code block immediately followed by a digit", () => {
const input = "```code```7 done";
expect(markdownToWhatsApp(input)).toBe(input);
});
it("preserves code block with formatting inside", () => {
const input = "Before ```**bold** and ~~strike~~``` after **real bold**";
expect(markdownToWhatsApp(input)).toBe(