feat(baoyu-post-to-wechat): add placeholder image upload and fix frontmatter parsing
This commit is contained in:
parent
95970480c8
commit
374a6b28fd
|
|
@ -23,6 +23,20 @@ interface PublishResponse {
|
||||||
errmsg?: string;
|
errmsg?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface ImageInfo {
|
||||||
|
placeholder: string;
|
||||||
|
localPath: string;
|
||||||
|
originalPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface MarkdownRenderResult {
|
||||||
|
title: string;
|
||||||
|
author: string;
|
||||||
|
summary: string;
|
||||||
|
htmlPath: string;
|
||||||
|
contentImages: ImageInfo[];
|
||||||
|
}
|
||||||
|
|
||||||
type ArticleType = "news" | "newspic";
|
type ArticleType = "news" | "newspic";
|
||||||
|
|
||||||
interface ArticleOptions {
|
interface ArticleOptions {
|
||||||
|
|
@ -143,18 +157,20 @@ async function uploadImage(
|
||||||
async function uploadImagesInHtml(
|
async function uploadImagesInHtml(
|
||||||
html: string,
|
html: string,
|
||||||
accessToken: string,
|
accessToken: string,
|
||||||
baseDir: string
|
baseDir: string,
|
||||||
|
contentImages: ImageInfo[] = [],
|
||||||
): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> {
|
): Promise<{ html: string; firstMediaId: string; allMediaIds: string[] }> {
|
||||||
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
const imgRegex = /<img[^>]*\ssrc=["']([^"']+)["'][^>]*>/gi;
|
||||||
const matches = [...html.matchAll(imgRegex)];
|
const matches = [...html.matchAll(imgRegex)];
|
||||||
|
|
||||||
if (matches.length === 0) {
|
if (matches.length === 0 && contentImages.length === 0) {
|
||||||
return { html, firstMediaId: "", allMediaIds: [] };
|
return { html, firstMediaId: "", allMediaIds: [] };
|
||||||
}
|
}
|
||||||
|
|
||||||
let firstMediaId = "";
|
let firstMediaId = "";
|
||||||
let updatedHtml = html;
|
let updatedHtml = html;
|
||||||
const allMediaIds: string[] = [];
|
const allMediaIds: string[] = [];
|
||||||
|
const uploadedBySource = new Map<string, UploadResponse>();
|
||||||
|
|
||||||
for (const match of matches) {
|
for (const match of matches) {
|
||||||
const [fullTag, src] = match;
|
const [fullTag, src] = match;
|
||||||
|
|
@ -172,7 +188,11 @@ async function uploadImagesInHtml(
|
||||||
|
|
||||||
console.error(`[wechat-api] Uploading image: ${imagePath}`);
|
console.error(`[wechat-api] Uploading image: ${imagePath}`);
|
||||||
try {
|
try {
|
||||||
const resp = await uploadImage(imagePath, accessToken, baseDir);
|
let resp = uploadedBySource.get(imagePath);
|
||||||
|
if (!resp) {
|
||||||
|
resp = await uploadImage(imagePath, accessToken, baseDir);
|
||||||
|
uploadedBySource.set(imagePath, resp);
|
||||||
|
}
|
||||||
const newTag = fullTag
|
const newTag = fullTag
|
||||||
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
.replace(/\ssrc=["'][^"']+["']/, ` src="${resp.url}"`)
|
||||||
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
.replace(/\sdata-local-path=["'][^"']+["']/, "");
|
||||||
|
|
@ -186,6 +206,30 @@ async function uploadImagesInHtml(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (const image of contentImages) {
|
||||||
|
if (!updatedHtml.includes(image.placeholder)) continue;
|
||||||
|
|
||||||
|
const imagePath = image.localPath || image.originalPath;
|
||||||
|
console.error(`[wechat-api] Uploading placeholder image: ${imagePath}`);
|
||||||
|
|
||||||
|
try {
|
||||||
|
let resp = uploadedBySource.get(imagePath);
|
||||||
|
if (!resp) {
|
||||||
|
resp = await uploadImage(imagePath, accessToken, baseDir);
|
||||||
|
uploadedBySource.set(imagePath, resp);
|
||||||
|
}
|
||||||
|
|
||||||
|
const replacementTag = `<img src="${resp.url}" style="display: block; width: 100%; margin: 1.5em auto;">`;
|
||||||
|
updatedHtml = replaceAllPlaceholders(updatedHtml, image.placeholder, replacementTag);
|
||||||
|
allMediaIds.push(resp.media_id);
|
||||||
|
if (!firstMediaId) {
|
||||||
|
firstMediaId = resp.media_id;
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
console.error(`[wechat-api] Failed to upload placeholder ${image.placeholder}:`, err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return { html: updatedHtml, firstMediaId, allMediaIds };
|
return { html: updatedHtml, firstMediaId, allMediaIds };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,7 +289,7 @@ async function publishToDraft(
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
function parseFrontmatter(content: string): { frontmatter: Record<string, string>; body: string } {
|
||||||
const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n([\s\S]*)$/);
|
const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||||
if (!match) return { frontmatter: {}, body: content };
|
if (!match) return { frontmatter: {}, body: content };
|
||||||
|
|
||||||
const frontmatter: Record<string, string> = {};
|
const frontmatter: Record<string, string> = {};
|
||||||
|
|
@ -266,38 +310,42 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
|
||||||
return { frontmatter, body: match[2]! };
|
return { frontmatter, body: match[2]! };
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderMarkdownToHtml(
|
function renderMarkdownWithPlaceholders(
|
||||||
markdownPath: string,
|
markdownPath: string,
|
||||||
theme: string = "default",
|
theme: string = "default",
|
||||||
color?: string,
|
color?: string,
|
||||||
citeStatus: boolean = true
|
citeStatus: boolean = true,
|
||||||
): string {
|
title?: string,
|
||||||
|
): MarkdownRenderResult {
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
const renderScript = path.join(__dirname, "md", "render.ts");
|
const mdToWechatScript = path.join(__dirname, "md-to-wechat.ts");
|
||||||
const baseDir = path.dirname(markdownPath);
|
const baseDir = path.dirname(markdownPath);
|
||||||
|
|
||||||
const renderArgs = ["-y", "bun", renderScript, markdownPath, "--theme", theme];
|
const args = ["-y", "bun", mdToWechatScript, markdownPath];
|
||||||
if (color) renderArgs.push("--color", color);
|
if (title) args.push("--title", title);
|
||||||
if (citeStatus) renderArgs.push("--cite");
|
if (theme) args.push("--theme", theme);
|
||||||
|
if (color) args.push("--color", color);
|
||||||
|
if (!citeStatus) args.push("--no-cite");
|
||||||
|
|
||||||
console.error(`[wechat-api] Rendering markdown with theme: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);
|
console.error(`[wechat-api] Rendering markdown with placeholders via md-to-wechat: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);
|
||||||
const result = spawnSync("npx", renderArgs, {
|
const result = spawnSync("npx", args, {
|
||||||
stdio: ["inherit", "pipe", "pipe"],
|
stdio: ["inherit", "pipe", "pipe"],
|
||||||
cwd: baseDir,
|
cwd: baseDir,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (result.status !== 0) {
|
if (result.status !== 0) {
|
||||||
const stderr = result.stderr?.toString() || "";
|
const stderr = result.stderr?.toString() || "";
|
||||||
throw new Error(`Render failed: ${stderr}`);
|
throw new Error(`Markdown placeholder render failed: ${stderr}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const htmlPath = markdownPath.replace(/\.md$/i, ".html");
|
const stdout = result.stdout?.toString() || "";
|
||||||
if (!fs.existsSync(htmlPath)) {
|
return JSON.parse(stdout) as MarkdownRenderResult;
|
||||||
throw new Error(`HTML file not generated: ${htmlPath}`);
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return htmlPath;
|
function replaceAllPlaceholders(html: string, placeholder: string, replacement: string): string {
|
||||||
|
const escapedPlaceholder = placeholder.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
|
return html.replace(new RegExp(escapedPlaceholder, "g"), replacement);
|
||||||
}
|
}
|
||||||
|
|
||||||
function extractHtmlContent(htmlPath: string): string {
|
function extractHtmlContent(htmlPath: string): string {
|
||||||
|
|
@ -459,6 +507,7 @@ async function main(): Promise<void> {
|
||||||
let htmlPath: string;
|
let htmlPath: string;
|
||||||
let htmlContent: string;
|
let htmlContent: string;
|
||||||
let frontmatter: Record<string, string> = {};
|
let frontmatter: Record<string, string> = {};
|
||||||
|
let contentImages: ImageInfo[] = [];
|
||||||
|
|
||||||
if (args.isHtml) {
|
if (args.isHtml) {
|
||||||
htmlPath = filePath;
|
htmlPath = filePath;
|
||||||
|
|
@ -491,8 +540,14 @@ async function main(): Promise<void> {
|
||||||
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
|
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
|
||||||
|
|
||||||
console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}, citeStatus: ${args.citeStatus}`);
|
console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}, citeStatus: ${args.citeStatus}`);
|
||||||
htmlPath = renderMarkdownToHtml(filePath, args.theme, args.color, args.citeStatus);
|
const rendered = renderMarkdownWithPlaceholders(filePath, args.theme, args.color, args.citeStatus, args.title);
|
||||||
|
htmlPath = rendered.htmlPath;
|
||||||
|
contentImages = rendered.contentImages;
|
||||||
|
if (!title) title = rendered.title;
|
||||||
|
if (!author) author = rendered.author;
|
||||||
|
if (!digest) digest = rendered.summary;
|
||||||
console.error(`[wechat-api] HTML generated: ${htmlPath}`);
|
console.error(`[wechat-api] HTML generated: ${htmlPath}`);
|
||||||
|
console.error(`[wechat-api] Placeholder images: ${contentImages.length}`);
|
||||||
htmlContent = extractHtmlContent(htmlPath);
|
htmlContent = extractHtmlContent(htmlPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -527,6 +582,7 @@ async function main(): Promise<void> {
|
||||||
digest: digest || undefined,
|
digest: digest || undefined,
|
||||||
htmlPath,
|
htmlPath,
|
||||||
contentLength: htmlContent.length,
|
contentLength: htmlContent.length,
|
||||||
|
placeholderImageCount: contentImages.length || undefined,
|
||||||
account: resolved.alias || undefined,
|
account: resolved.alias || undefined,
|
||||||
}, null, 2));
|
}, null, 2));
|
||||||
return;
|
return;
|
||||||
|
|
@ -540,7 +596,8 @@ async function main(): Promise<void> {
|
||||||
const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
|
const { html: processedHtml, firstMediaId, allMediaIds } = await uploadImagesInHtml(
|
||||||
htmlContent,
|
htmlContent,
|
||||||
accessToken,
|
accessToken,
|
||||||
baseDir
|
baseDir,
|
||||||
|
contentImages,
|
||||||
);
|
);
|
||||||
htmlContent = processedHtml;
|
htmlContent = processedHtml;
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue