From c9a50cc908d0473f5d754efdbe08cbe387714f63 Mon Sep 17 00:00:00 2001 From: wangruofeng Date: Sun, 28 Jun 2026 14:42:07 +0800 Subject: [PATCH] =?UTF-8?q?fix(baoyu-post-to-x):=20=E4=BF=AE=E5=A4=8D?= =?UTF-8?q?=E7=B4=A7=E8=B4=B4=E4=B8=AD=E6=96=87=E7=9A=84=E5=8A=A0=E7=B2=97?= =?UTF-8?q?/=E6=96=9C=E4=BD=93=E5=9C=A8=20X=20Articles=20=E6=B8=B2?= =?UTF-8?q?=E6=9F=93=E4=B8=BA=E5=AD=97=E9=9D=A2=20**=20=E7=9A=84=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#189)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(baoyu-post-to-x): render CJK-adjacent bold/italics in articles md-to-html.ts preprocessed markdown with remark + remark-cjk-friendly, then re-stringified and re-parsed with marked. remark-cjk-friendly parses emphasis adjacent to CJK correctly, but remark-stringify re-emits the **bold*/*italic* markers, which marked then FAILS to parse: its flanking test does not treat a CJK character after the closing delimiter as valid punctuation/whitespace. Result: literal **...** asterisks leak into the pasted X Article body whenever the closing delimiter is directly followed by a CJK character. Fix: convert strong/emphasis mdast nodes (already identified correctly by remark-cjk-friendly) into raw inline / HTML before stringify, so marked passes them through untouched. A small recursive serializer preserves nested links, inline code, images, etc. - add remarkStrongEmToHtml remark plugin + inline mdast serializer - add unist-util-visit as an explicit dependency - add regression test covering CJK-adjacent bold and italics Co-Authored-By: Claude * fix(baoyu-post-to-x): 修复紧贴中文的加粗/斜体在 X Articles 渲染为字面 ** 的问题 按维护者建议改用 remark-cjk-friendly 标准用法,替代自定义插件/序列化器: - 删除 remarkStrongEmToHtml 插件与 serializeMdastNode 序列化器 - preprocessCjkMarkdown 简化为 remarkParse + remarkCjkFriendly + remarkStringify 往返 - 移除原 markdown 阶段的 &#x...; 实体解码(该解码会还原 cjk-friendly 为 骗过 marked flanking 判定而生成的实体,正是 CJK 加粗失败的根因) - 改在 marked 渲染出最终 HTML 后再 decodeHtmlEntities,此时 markdown 语法 已不存在,解码安全且保持输出干净 - 移除不再使用的 unist-util-visit 显式依赖 - 测试补充引用式链接 **[docs][d]** 在强调内的回归断言(顺带修复 Codex 指出的 linkReference 渲染为纯文本的 P2 问题) Co-Authored-By: Claude * fix(baoyu-post-to-x): 移除渲染后无差别实体解码,避免篡改作者字面实体 回应 Codex review (P2):decodeHtmlEntities 对整个渲染 HTML 做无差别 &#x...; 解码,会把作者为显示字面 HTML 而写的实体(如 <b>) 解码成真实标签,存在内容篡改与 HTML 注入风险。 该解码本就只是为了让输出 HTML「好看」,而 remark-cjk-friendly 引入的 实体是合法 HTML 字符引用,粘进 X 编辑器时能正确渲染;且这些实体只会 出现在强调 span 的外侧(边界字符),不会出现在 span 内部。因此直接 移除解码,既消除过度解码风险,又无功能损失。 - 删除 decodeHtmlEntities 及其在 convertMarkdownToHtml 的调用 - 新增测试:作者字面实体 <b> 不被解码为真实 ,CJK 加粗 仍正常渲染 - bun test 8 个用例全部通过;真实文章端到端验证 17 处加粗、0 字面 **、 0 真实标签注入 Co-Authored-By: Claude --------- Co-authored-by: ruofeng Co-authored-by: Claude --- .../scripts/md-to-html.test.ts | 65 +++++++++++++++++++ skills/baoyu-post-to-x/scripts/md-to-html.ts | 18 ++++- 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/skills/baoyu-post-to-x/scripts/md-to-html.test.ts b/skills/baoyu-post-to-x/scripts/md-to-html.test.ts index 848add1..ae163c9 100644 --- a/skills/baoyu-post-to-x/scripts/md-to-html.test.ts +++ b/skills/baoyu-post-to-x/scripts/md-to-html.test.ts @@ -98,3 +98,68 @@ test('parseMarkdown resolves encoded spaces and literal percent image paths', as assert.equal(result.contentImages[0]?.localPath, path.join(root, 'Pasted image.png')); assert.equal(result.contentImages[1]?.localPath, path.join(root, '100%.png')); }); + +test('parseMarkdown renders CJK-adjacent bold and italics (no literal asterisks)', async (t) => { + const root = await makeTempDir('x-md-to-html-cjk-bold-'); + t.after(() => fs.rm(root, { recursive: true, force: true })); + + const markdownPath = path.join(root, 'post.md'); + const tempDir = path.join(root, 'tmp'); + await fs.mkdir(tempDir, { recursive: true }); + await fs.writeFile( + markdownPath, + [ + '# 标题', + '', + '分工在变细。**国际大厂卷基础设施,中文项目卷场景落地。**这其实是生态成熟的表现。', + '', + '半角场景 **Top 10 里平均有 8 个** 项目。', + '', + '斜体 *数据来源 GitHub* 收尾。', + '', + '参考 **[docs][d]** 了解更多。', + '', + '[d]: https://example.com', + ].join('\n'), + ); + + const result = await parseMarkdown(markdownPath, { tempDir }); + + // Bold directly adjacent to CJK (closing ** followed by CJK) must render. + assert.match(result.html, /国际大厂卷基础设施,中文项目卷场景落地。<\/strong>/); + assert.match(result.html, /Top 10 里平均有 8 个<\/strong>/); + // Italics. + assert.match(result.html, /数据来源 GitHub<\/em>/); + // Reference-style links inside emphasis must render as links, not plain text. + assert.match(result.html, /docs<\/a><\/strong>/); + // No literal emphasis delimiters should leak into the output. + assert.doesNotMatch(result.html, /\*\*/); + assert.doesNotMatch(result.html, /(? { + const root = await makeTempDir('x-md-to-html-entities-'); + t.after(() => fs.rm(root, { recursive: true, force: true })); + + const markdownPath = path.join(root, 'post.md'); + const tempDir = path.join(root, 'tmp'); + await fs.mkdir(tempDir, { recursive: true }); + await fs.writeFile( + markdownPath, + [ + '# 标题', + '', + '正文中写 <b>literal</b> 想显示字面标签。**加粗**收尾。', + '', + '代码里写 `<b>` 同样保留。', + ].join('\n'), + ); + + const result = await parseMarkdown(markdownPath, { tempDir }); + + // CJK-adjacent bold still renders. + assert.match(result.html, /加粗<\/strong>/); + // Author-written literal entities must NOT be decoded into real tags. + assert.doesNotMatch(result.html, /literal<\/b>/); + assert.match(result.html, /<b>literal<\/b>/); +}); diff --git a/skills/baoyu-post-to-x/scripts/md-to-html.ts b/skills/baoyu-post-to-x/scripts/md-to-html.ts index 5c3debd..f7e01d8 100644 --- a/skills/baoyu-post-to-x/scripts/md-to-html.ts +++ b/skills/baoyu-post-to-x/scripts/md-to-html.ts @@ -136,6 +136,21 @@ function highlightCode(code: string, lang: string): string { } } +// Normalize CJK-adjacent emphasis so `marked` renders it correctly. +// +// `marked`'s emphasis tokenizer treats a closing `**`/`*` directly followed by a +// CJK character as not right-flanking, so it leaves the delimiters literal +// (e.g. `**加粗**这` renders as plain text with the asterisks intact). We round-trip +// the markdown through `remark-cjk-friendly`, whose stringify serializes the +// boundary character as an HTML entity (`这`); the entity is treated as +// punctuation by `marked`'s flanking rules, so emphasis parses as expected. +// +// We deliberately do NOT decode the entities afterward. They are valid HTML +// character references that render correctly when the article HTML is pasted into +// the X editor, and `marked` only emits them for characters outside the emphasis +// span (the boundary char), never inside it. A blanket decode of the rendered +// HTML would risk turning author-written literal entities (e.g. `<b>` +// meant to display `` as text) into real tags, so we leave them intact. function preprocessCjkMarkdown(markdown: string): string { try { const processor = unified() @@ -143,8 +158,7 @@ function preprocessCjkMarkdown(markdown: string): string { .use(remarkCjkFriendly) .use(remarkStringify); - const result = String(processor.processSync(markdown)); - return result.replace(/&#x([0-9A-Fa-f]+);/g, (_, hex: string) => String.fromCodePoint(parseInt(hex, 16))); + return String(processor.processSync(markdown)); } catch { return markdown; }