Skip to content

Commit 5909778

Browse files
nicknmortysteipete
andauthored
fix(markdown-core): CJK-friendly emphasis flanking so **标签:**正文 renders bold (#101120) (#101230)
* test(telegram): reproduce CJK emphasis flanking bug * fix(markdown-core): support CJK emphasis flanking * fix(markdown-core): type CJK delimiter state (#101120) * fix(markdown-core): mirror markdown-it Unicode whitespace in CJK delimiter override Preserves markdown-it isWhiteSpace classification (U+3000, U+00A0, U+2000-200A, etc.) before forcing CJK-adjacent delimiter flags, and adds U+3000/U+2009 regressions. Addresses clawsweeper P1 finding on #101230. * refactor(markdown): use maintained CJK flanking plugin --------- Co-authored-by: Peter Steinberger <[email protected]>
1 parent bbd01c1 commit 5909778

6 files changed

Lines changed: 98 additions & 0 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ Docs: https://docs.openclaw.ai
2424
### Fixes
2525

2626
- **Android hardware keyboard chat:** send with unmodified Enter on physical keyboards while preserving Shift+Enter and other modified Enter combinations for multiline input. (#101239) Thanks @3ninyt3nin-creator.
27+
- **CJK Markdown emphasis:** render adjacent Chinese, Japanese, and Korean emphasis punctuation through the shared Markdown pipeline instead of leaking literal markers across channels. (#101230, #101120) Thanks @nicknmorty.
2728
- **Codex yielded native subagents:** keep the parent app-server subscription and shared client alive until yielded native subagent completion delivery settles, preventing lost wakeups and leaked one-shot cleanup.
2829
- **Discord streamed finals:** send completion replies as fresh messages so inactive channels become unread, while preserving targeted mentions without escalating `@everyone` or `@here`. (#99711, #99662) Thanks @davelutztx.
2930
- **OpenAI-compatible SSE parsing:** recognize event streams mislabeled as JSON without prepending a second `data:` prefix, preserving valid streamed responses from non-conforming providers. (#96503) Thanks @ZengWen-DT.

extensions/telegram/src/format.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,12 @@ describe("markdownToTelegramHtml", () => {
9494
expect(markdownToTelegramRichHtml("<sup>1</sup>")).toBe("<sup>1</sup>");
9595
});
9696

97+
it("renders bold spans that close before CJK punctuation in rich markdown", () => {
98+
expect(markdownToTelegramRichHtml("边界:**社区显示:**Fable 与 **有效**。")).toBe(
99+
"边界:<b>社区显示:</b>Fable 与 <b>有效</b>。",
100+
);
101+
});
102+
97103
it("materializes inline and paragraph newlines as <br> for rich messages", () => {
98104
// The exact reported symptom: literal "• " bullets (not Markdown list markers)
99105
// joined by soft breaks, which Bot API 10.1 rich messages collapse without <br>.

packages/markdown-core/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
},
5858
"dependencies": {
5959
"markdown-it": "14.3.0",
60+
"markdown-it-cjk-friendly": "2.0.2",
6061
"yaml": "2.9.0"
6162
}
6263
}
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
// Markdown Core tests cover CJK-friendly emphasis flanking.
2+
import { describe, expect, it } from "vitest";
3+
import { markdownToIR } from "./ir.js";
4+
5+
function styledText(markdown: string, style: "bold" | "italic" = "bold"): string[] {
6+
const ir = markdownToIR(markdown);
7+
return ir.styles
8+
.filter((span) => span.style === style)
9+
.map((span) => ir.text.slice(span.start, span.end));
10+
}
11+
12+
describe("markdownToIR CJK emphasis flanking", () => {
13+
it("closes strong emphasis before adjacent Chinese text", () => {
14+
expect(styledText("前**加粗:**后")).toEqual(["加粗:"]);
15+
});
16+
17+
it("closes strong emphasis before adjacent Japanese text", () => {
18+
expect(styledText("これは**強調。**です")).toEqual(["強調。"]);
19+
});
20+
21+
it("closes strong emphasis before adjacent Korean text", () => {
22+
expect(styledText("이것은 **강조:**입니다")).toEqual(["강조:"]);
23+
});
24+
25+
it("handles supplementary CJK and variation selectors at emphasis boundaries", () => {
26+
expect(styledText("𰻞𰻞**(ビャンビャン)**麺")).toEqual(["(ビャンビャン)"]);
27+
expect(styledText("葛󠄀**(こちらが正式表記)**城市")).toEqual(["(こちらが正式表記)"]);
28+
});
29+
30+
it("supports CJK-friendly underscore flanking without enabling Latin intraword emphasis", () => {
31+
expect(styledText("__注意__:注意事項")).toEqual(["注意"]);
32+
expect(styledText("foo_bar_baz", "italic")).toEqual([]);
33+
});
34+
35+
it("keeps ASCII CommonMark emphasis behavior", () => {
36+
expect(styledText("**bold** text")).toEqual(["bold"]);
37+
expect(styledText("foo**bar**baz")).toEqual(["bar"]);
38+
});
39+
40+
it("treats ideographic space (U+3000) as whitespace, not a flanking CJK char", () => {
41+
// Opening delimiter followed by U+3000 must not be forced open.
42+
expect(styledText("前**\u3000加粗**后")).toEqual([]);
43+
// U+3000-separated emphasis keeps normal CommonMark behavior.
44+
expect(styledText("前\u3000**加粗**\u3000后")).toEqual(["加粗"]);
45+
});
46+
47+
it("treats Unicode thin space (U+2009) as whitespace in delimiter scanning", () => {
48+
// Opening delimiter followed by U+2009 must not be forced open.
49+
expect(styledText("前**\u2009加粗**后")).toEqual([]);
50+
// Closing delimiter after CJK punctuation still closes when followed by U+2009.
51+
expect(styledText("前**加粗:**\u2009后")).toEqual(["加粗:"]);
52+
});
53+
54+
it("leaves code spans and links on their existing paths", () => {
55+
const code = markdownToIR("`前**加粗:**后`");
56+
expect(code.text).toBe("前**加粗:**后");
57+
expect(code.styles.map((span) => span.style)).toEqual(["code"]);
58+
59+
const linked = markdownToIR("[前**加粗:**后](https://example.com)");
60+
expect(linked.text).toBe("前加粗:后");
61+
expect(linked.links).toEqual([
62+
{ start: 0, end: linked.text.length, href: "https://example.com" },
63+
]);
64+
expect(linked.styles.filter((span) => span.style === "bold")).toEqual([
65+
{ start: 1, end: 4, style: "bold" },
66+
]);
67+
});
68+
});

packages/markdown-core/src/ir.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
// Markdown Core module implements ir behavior.
22
import MarkdownIt from "markdown-it";
3+
import markdownItCjkFriendly from "markdown-it-cjk-friendly";
34
import { chunkText } from "./chunk-text.js";
45
import type { MarkdownTableMode } from "./types.js";
56

@@ -150,6 +151,7 @@ function createMarkdownIt(options: MarkdownParseOptions): MarkdownIt {
150151
breaks: false,
151152
typographer: false,
152153
});
154+
md.use(markdownItCjkFriendly);
153155
md.enable("strikethrough");
154156
if (options.tableMode && options.tableMode !== "off") {
155157
md.enable("table");

pnpm-lock.yaml

Lines changed: 20 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)