feat(publish): render jianshu html images as markdown
Jianshu's HTML editor strips uploaded <img> tags during save, so previously uploaded images vanished from the rendered article. After image upload, convert each <img> (and surrounding <p> 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) <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,5 @@
|
|||||||
import type { StoredPlatformState } from "../platforms";
|
import type { StoredPlatformState } from "../platforms";
|
||||||
|
import { renderJianshuHtmlImagesAsMarkdown } from "../../../../packages/publisher-platforms/src/jianshu";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
articleMarkdown,
|
articleMarkdown,
|
||||||
@@ -146,6 +147,7 @@ async function publishJianshu(context: AdapterContext): Promise<PlatformPublishR
|
|||||||
const htmlProcessed = useMarkdownMode
|
const htmlProcessed = useMarkdownMode
|
||||||
? null
|
? null
|
||||||
: await uploadHtmlImages(normalizeArticleHtml(context.article), async (sourceUrl) => uploadImage(noteId, sourceUrl));
|
: await uploadHtmlImages(normalizeArticleHtml(context.article), async (sourceUrl) => uploadImage(noteId, sourceUrl));
|
||||||
|
const htmlContent = htmlProcessed ? renderJianshuHtmlImagesAsMarkdown(htmlProcessed.html) : "";
|
||||||
|
|
||||||
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}`, {
|
await fetchJson(`https://www.jianshu.com/author/notes/${noteId}`, {
|
||||||
method: "PUT",
|
method: "PUT",
|
||||||
@@ -154,7 +156,7 @@ async function publishJianshu(context: AdapterContext): Promise<PlatformPublishR
|
|||||||
id: noteId,
|
id: noteId,
|
||||||
autosave_control: 1,
|
autosave_control: 1,
|
||||||
title: context.article.title,
|
title: context.article.title,
|
||||||
content: useMarkdownMode ? (markdownProcessed?.markdown ?? rawMarkdown) : (htmlProcessed?.html ?? ""),
|
content: useMarkdownMode ? (markdownProcessed?.markdown ?? rawMarkdown) : htmlContent,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import { describe, expect, it } from "vitest";
|
||||||
|
|
||||||
|
import { renderJianshuHtmlImagesAsMarkdown } from "../../../../../packages/publisher-platforms/src/jianshu";
|
||||||
|
|
||||||
|
describe("jianshu article content", () => {
|
||||||
|
it("converts editor image paragraphs to markdown image syntax", () => {
|
||||||
|
const result = renderJianshuHtmlImagesAsMarkdown(
|
||||||
|
'<p class="article-editor-image article-editor-image--center" align="center"><img src="https://upload-images.jianshu.io/upload_images/2370478-707abbd68000b249.png" alt="" width="365" height="521" data-asset-id="14" /></p>',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toBe("");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("keeps normal text while replacing raw image tags", () => {
|
||||||
|
const result = renderJianshuHtmlImagesAsMarkdown(
|
||||||
|
'<p>正文段落</p><img src="https://upload-images.jianshu.io/upload_images/2370478-0e05fd986e33dae9.jpg?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240" alt="微信图片_2026-04-18_141100_730.jpg">',
|
||||||
|
);
|
||||||
|
|
||||||
|
expect(result).toContain("<p>正文段落</p>");
|
||||||
|
expect(result).toContain(
|
||||||
|
"",
|
||||||
|
);
|
||||||
|
expect(result).not.toContain("<img");
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -127,6 +127,46 @@ function parseJianshuErrorMessage(error: unknown, fallback: string): string {
|
|||||||
return fallback;
|
return fallback;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractHtmlAttribute(sourceTag: string, attributeName: string): string | null {
|
||||||
|
const matcher = new RegExp(`\\s${attributeName}\\s*=\\s*(?:"([^"]*)"|'([^']*)'|([^\\s"'>]+))`, "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 ``;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function renderJianshuHtmlImagesAsMarkdown(content: string): string {
|
||||||
|
if (!/<img\b/i.test(content)) {
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
|
||||||
|
let next = content.replace(
|
||||||
|
/<p\b[^>]*>\s*(<img\b[^>]*\/?>)\s*<\/p>/gi,
|
||||||
|
(_match, imgTag: string) => `\n\n${markdownImageFromHtmlTag(imgTag)}\n\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
next = next.replace(
|
||||||
|
/<img\b[^>]*\/?>/gi,
|
||||||
|
(imgTag) => `\n\n${markdownImageFromHtmlTag(imgTag)}\n\n`,
|
||||||
|
);
|
||||||
|
|
||||||
|
return next.replace(/\n{3,}/g, "\n\n").trim();
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchJianshuMediaSnapshot(
|
export async function fetchJianshuMediaSnapshot(
|
||||||
fetchJson: <T>(input: string, init?: RequestInit) => Promise<T>,
|
fetchJson: <T>(input: string, init?: RequestInit) => Promise<T>,
|
||||||
): Promise<JianshuMediaSnapshot | null> {
|
): Promise<JianshuMediaSnapshot | null> {
|
||||||
@@ -265,7 +305,7 @@ async function processContentImages(
|
|||||||
for (const [from, to] of replacements.entries()) {
|
for (const [from, to] of replacements.entries()) {
|
||||||
next = next.split(from).join(to);
|
next = next.split(from).join(to);
|
||||||
}
|
}
|
||||||
return next;
|
return renderJianshuHtmlImagesAsMarkdown(next);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function attachCoverImage(
|
async function attachCoverImage(
|
||||||
|
|||||||
Reference in New Issue
Block a user