fix(webui): render adjacent CJK emphasis

This commit is contained in:
Xubin Ren 2026-07-27 21:42:09 +08:00
parent 9c6e1b073f
commit cad9b07deb
2 changed files with 52 additions and 0 deletions

View File

@ -237,11 +237,48 @@ function remarkSafeHtmlSubset() {
};
}
// Recover a common model-output edge case that CommonMark leaves as literal
// text: `**结论。**如果`, with no separator after the closing delimiter.
const CJK_AFTER_STRONG =
/(?<!\\)\*\*([^*\r\n]+?)(?<!\\)\*\*(?=[\p{Script=Han}\p{Script=Hiragana}\p{Script=Katakana}\p{Script=Hangul}])/gu;
function normalizeCjkStrongBoundaries(node: MarkdownAstNode): void {
if (!node.children) return;
node.children = node.children.flatMap((child) => {
if (child.type !== "text" || !child.value?.includes("**")) {
normalizeCjkStrongBoundaries(child);
return [child];
}
const replacement: MarkdownAstNode[] = [];
let cursor = 0;
for (const match of child.value.matchAll(CJK_AFTER_STRONG)) {
const start = match.index;
if (start > cursor) replacement.push(safeText(child.value.slice(cursor, start)));
replacement.push({
type: "strong",
children: [safeText(match[1])],
});
cursor = start + match[0].length;
}
if (cursor === 0) return [child];
if (cursor < child.value.length) replacement.push(safeText(child.value.slice(cursor)));
return replacement;
});
}
function remarkCjkStrongBoundaries() {
return (tree: MarkdownAstNode) => {
normalizeCjkStrongBoundaries(tree);
};
}
const remarkPlugins: NonNullable<StreamdownProps["remarkPlugins"]> = [
remarkBreaks,
remarkGfm,
[remarkMath, { singleDollarTextMath: false }],
remarkTexMath,
remarkCjkStrongBoundaries,
remarkSafeHtmlSubset,
];
const rehypePlugins: NonNullable<StreamdownProps["rehypePlugins"]> = [rehypeKatex];

View File

@ -523,6 +523,21 @@ describe("MarkdownTextRenderer", () => {
expect(screen.getByRole("link", { name: "links" })).not.toHaveAttribute("node");
});
it("renders bold CJK text when more CJK text follows immediately", () => {
render(
<MarkdownTextRenderer streaming>
{
"**结论:目前看风险可控,没有发现常驻或可疑安装。**如果你之后不想再用,我可以帮你彻底卸载。"
}
</MarkdownTextRenderer>,
);
expect(
screen.getByText("结论:目前看风险可控,没有发现常驻或可疑安装。").tagName,
).toBe("STRONG");
expect(screen.getByText(/如果你之后不想再用/)).toBeInTheDocument();
});
it("adds line numbers to multiline fenced code without changing inline code", () => {
render(
<MarkdownTextRenderer highlightCode={false}>