From dbce8515e7c8ea094c37f7bc431ef015f6c20079 Mon Sep 17 00:00:00 2001 From: liangxu Date: Mon, 27 Apr 2026 11:02:50 +0800 Subject: [PATCH] feat(publish): render jianshu html images as markdown Jianshu's HTML editor strips uploaded tags during save, so previously uploaded images vanished from the rendered article. After image upload, convert each (and surrounding

wrappers) to a markdown image block in both the desktop and extension publish paths so the references survive. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../browser-extension/src/adapters/jianshu.ts | 4 +- .../src/main/adapters/jianshu.test.ts | 25 +++++++++++ packages/publisher-platforms/src/jianshu.ts | 42 ++++++++++++++++++- 3 files changed, 69 insertions(+), 2 deletions(-) create mode 100644 apps/desktop-client/src/main/adapters/jianshu.test.ts diff --git a/apps/browser-extension/src/adapters/jianshu.ts b/apps/browser-extension/src/adapters/jianshu.ts index 09ecf99..5e91a1a 100644 --- a/apps/browser-extension/src/adapters/jianshu.ts +++ b/apps/browser-extension/src/adapters/jianshu.ts @@ -1,4 +1,5 @@ import type { StoredPlatformState } from "../platforms"; +import { renderJianshuHtmlImagesAsMarkdown } from "../../../../packages/publisher-platforms/src/jianshu"; import { articleMarkdown, @@ -146,6 +147,7 @@ async function publishJianshu(context: AdapterContext): Promise uploadImage(noteId, sourceUrl)); + const htmlContent = htmlProcessed ? renderJianshuHtmlImagesAsMarkdown(htmlProcessed.html) : ""; await fetchJson(`https://www.jianshu.com/author/notes/${noteId}`, { method: "PUT", @@ -154,7 +156,7 @@ async function publishJianshu(context: AdapterContext): Promise { + it("converts editor image paragraphs to markdown image syntax", () => { + const result = renderJianshuHtmlImagesAsMarkdown( + '

', + ); + + expect(result).toBe("![](https://upload-images.jianshu.io/upload_images/2370478-707abbd68000b249.png)"); + }); + + it("keeps normal text while replacing raw image tags", () => { + const result = renderJianshuHtmlImagesAsMarkdown( + '

正文段落

微信图片_2026-04-18_141100_730.jpg', + ); + + expect(result).toContain("

正文段落

"); + expect(result).toContain( + "![微信图片_2026-04-18_141100_730.jpg](https://upload-images.jianshu.io/upload_images/2370478-0e05fd986e33dae9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)", + ); + expect(result).not.toContain("]+))`, "i"); + const match = matcher.exec(sourceTag); + const value = match?.[1] ?? match?.[2] ?? match?.[3] ?? ""; + const trimmed = value.trim(); + return trimmed ? trimmed : null; +} + +function escapeMarkdownAlt(value: string): string { + return value.replace(/\\/g, "\\\\").replace(/\]/g, "\\]"); +} + +function markdownImageFromHtmlTag(imgTag: string): string { + const src = extractHtmlAttribute(imgTag, "src"); + if (!src) { + return imgTag; + } + + const alt = extractHtmlAttribute(imgTag, "alt") ?? ""; + return `![${escapeMarkdownAlt(alt)}](${src})`; +} + +export function renderJianshuHtmlImagesAsMarkdown(content: string): string { + if (!/]*>\s*(]*\/?>)\s*<\/p>/gi, + (_match, imgTag: string) => `\n\n${markdownImageFromHtmlTag(imgTag)}\n\n`, + ); + + next = next.replace( + /]*\/?>/gi, + (imgTag) => `\n\n${markdownImageFromHtmlTag(imgTag)}\n\n`, + ); + + return next.replace(/\n{3,}/g, "\n\n").trim(); +} + export async function fetchJianshuMediaSnapshot( fetchJson: (input: string, init?: RequestInit) => Promise, ): Promise { @@ -265,7 +305,7 @@ async function processContentImages( for (const [from, to] of replacements.entries()) { next = next.split(from).join(to); } - return next; + return renderJianshuHtmlImagesAsMarkdown(next); } async function attachCoverImage(