feat(baoyu-post-to-wechat): default bottom citations for markdown input

Enable bottom citations by default for markdown articles. Add --no-cite
flag to all posting scripts (wechat-api, wechat-article, md-to-wechat)
to optionally disable. WeChat mp links stay inline.
This commit is contained in:
Jim Liu 宝玉
2026-03-06 23:38:23 -06:00
parent 8cea550627
commit 970f5e741a
5 changed files with 65 additions and 15 deletions
@@ -112,10 +112,14 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
return { frontmatter, body: match[2]! };
}
export async function convertMarkdown(markdownPath: string, options?: { title?: string; theme?: string; color?: string }): Promise<ParsedResult> {
export async function convertMarkdown(
markdownPath: string,
options?: { title?: string; theme?: string; color?: string; citeStatus?: boolean }
): Promise<ParsedResult> {
const baseDir = path.dirname(markdownPath);
const content = fs.readFileSync(markdownPath, 'utf-8');
const theme = options?.theme ?? 'default';
const citeStatus = options?.citeStatus ?? true;
const { frontmatter, body } = parseFrontmatter(content);
@@ -185,8 +189,9 @@ export async function convertMarkdown(markdownPath: string, options?: { title?:
const renderArgs = ['-y', 'bun', renderScript, tempMdPath, '--theme', theme];
if (options?.color) renderArgs.push('--color', options.color);
if (citeStatus) renderArgs.push('--cite');
console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}${options?.color ? `, color: ${options.color}` : ''}`);
console.error(`[md-to-wechat] Rendering markdown with theme: ${theme}${options?.color ? `, color: ${options.color}` : ''}, citeStatus: ${citeStatus}`);
const result = spawnSync('npx', renderArgs, {
stdio: ['inherit', 'pipe', 'pipe'],
@@ -232,6 +237,7 @@ Options:
--title <title> Override title
--theme <name> Theme name (default, grace, simple, modern)
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--no-cite Disable bottom citations for ordinary external links
--help Show this help
Output JSON format:
@@ -251,6 +257,7 @@ Example:
npx -y bun md-to-wechat.ts article.md
npx -y bun md-to-wechat.ts article.md --theme grace
npx -y bun md-to-wechat.ts article.md --theme modern --color blue
npx -y bun md-to-wechat.ts article.md --no-cite
`);
process.exit(0);
}
@@ -265,6 +272,7 @@ async function main(): Promise<void> {
let title: string | undefined;
let theme: string | undefined;
let color: string | undefined;
let citeStatus = true;
for (let i = 0; i < args.length; i++) {
const arg = args[i]!;
@@ -274,6 +282,10 @@ async function main(): Promise<void> {
theme = args[++i];
} else if (arg === '--color' && args[i + 1]) {
color = args[++i];
} else if (arg === '--cite') {
citeStatus = true;
} else if (arg === '--no-cite') {
citeStatus = false;
} else if (!arg.startsWith('-')) {
markdownPath = arg;
}
@@ -289,7 +301,7 @@ async function main(): Promise<void> {
process.exit(1);
}
const result = await convertMarkdown(markdownPath, { title, theme, color });
const result = await convertMarkdown(markdownPath, { title, theme, color, citeStatus });
console.log(JSON.stringify(result, null, 2));
}
@@ -307,7 +307,12 @@ function parseFrontmatter(content: string): { frontmatter: Record<string, string
return { frontmatter, body: match[2]! };
}
function renderMarkdownToHtml(markdownPath: string, theme: string = "default", color?: string): string {
function renderMarkdownToHtml(
markdownPath: string,
theme: string = "default",
color?: string,
citeStatus: boolean = true
): string {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const renderScript = path.join(__dirname, "md", "render.ts");
@@ -315,8 +320,9 @@ function renderMarkdownToHtml(markdownPath: string, theme: string = "default", c
const renderArgs = ["-y", "bun", renderScript, markdownPath, "--theme", theme];
if (color) renderArgs.push("--color", color);
if (citeStatus) renderArgs.push("--cite");
console.error(`[wechat-api] Rendering markdown with theme: ${theme}${color ? `, color: ${color}` : ""}`);
console.error(`[wechat-api] Rendering markdown with theme: ${theme}${color ? `, color: ${color}` : ""}, citeStatus: ${citeStatus}`);
const result = spawnSync("npx", renderArgs, {
stdio: ["inherit", "pipe", "pipe"],
cwd: baseDir,
@@ -362,6 +368,7 @@ Options:
--theme <name> Theme name for markdown (default, grace, simple, modern). Default: default
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--cover <path> Cover image path (local or URL)
--no-cite Disable bottom citations for ordinary external links in markdown mode
--dry-run Parse and render only, don't publish
--help Show this help
@@ -390,6 +397,7 @@ Example:
npx -y bun wechat-api.ts article.html --title "My Article"
npx -y bun wechat-api.ts images/ --type newspic --title "Photo Album"
npx -y bun wechat-api.ts article.md --dry-run
npx -y bun wechat-api.ts article.md --no-cite
`);
process.exit(0);
}
@@ -404,6 +412,7 @@ interface CliArgs {
theme: string;
color?: string;
cover?: string;
citeStatus: boolean;
dryRun: boolean;
}
@@ -417,6 +426,7 @@ function parseArgs(argv: string[]): CliArgs {
isHtml: false,
articleType: "news",
theme: "default",
citeStatus: true,
dryRun: false,
};
@@ -439,6 +449,10 @@ function parseArgs(argv: string[]): CliArgs {
args.color = argv[++i];
} else if (arg === "--cover" && argv[i + 1]) {
args.cover = argv[++i];
} else if (arg === "--cite") {
args.citeStatus = true;
} else if (arg === "--no-cite") {
args.citeStatus = false;
} else if (arg === "--dry-run") {
args.dryRun = true;
} else if (arg.startsWith("--") && argv[i + 1] && !argv[i + 1]!.startsWith("-")) {
@@ -513,8 +527,8 @@ async function main(): Promise<void> {
if (!author) author = frontmatter.author || "";
if (!digest) digest = frontmatter.digest || frontmatter.summary || frontmatter.description || "";
console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}`);
htmlPath = renderMarkdownToHtml(filePath, args.theme, args.color);
console.error(`[wechat-api] Theme: ${args.theme}${args.color ? `, color: ${args.color}` : ""}, citeStatus: ${args.citeStatus}`);
htmlPath = renderMarkdownToHtml(filePath, args.theme, args.color, args.citeStatus);
console.error(`[wechat-api] HTML generated: ${htmlPath}`);
htmlContent = extractHtmlContent(htmlPath);
}
@@ -20,6 +20,7 @@ interface ArticleOptions {
markdownFile?: string;
theme?: string;
color?: string;
citeStatus?: boolean;
author?: string;
summary?: string;
images?: string[];
@@ -182,13 +183,19 @@ async function pasteFromClipboardInEditor(session: ChromeSession): Promise<void>
await sleep(1000);
}
async function parseMarkdownWithPlaceholders(markdownPath: string, theme?: string, color?: string): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> {
async function parseMarkdownWithPlaceholders(
markdownPath: string,
theme?: string,
color?: string,
citeStatus: boolean = true
): Promise<{ title: string; author: string; summary: string; htmlPath: string; contentImages: ImageInfo[] }> {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const mdToWechatScript = path.join(__dirname, 'md-to-wechat.ts');
const args = ['-y', 'bun', mdToWechatScript, markdownPath];
if (theme) args.push('--theme', theme);
if (color) args.push('--color', color);
if (!citeStatus) args.push('--no-cite');
const result = spawnSync('npx', args, { stdio: ['inherit', 'pipe', 'pipe'] });
if (result.status !== 0) {
@@ -383,7 +390,7 @@ async function removeExtraEmptyLineAfterImage(session: ChromeSession): Promise<b
}
export async function postArticle(options: ArticleOptions): Promise<void> {
const { title, content, htmlFile, markdownFile, theme, color, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
const { title, content, htmlFile, markdownFile, theme, color, citeStatus = true, author, summary, images = [], submit = false, profileDir, cdpPort } = options;
let { contentImages = [] } = options;
let effectiveTitle = title || '';
let effectiveAuthor = author || '';
@@ -392,7 +399,7 @@ export async function postArticle(options: ArticleOptions): Promise<void> {
if (markdownFile) {
console.log(`[wechat] Parsing markdown: ${markdownFile}`);
const parsed = await parseMarkdownWithPlaceholders(markdownFile, theme, color);
const parsed = await parseMarkdownWithPlaceholders(markdownFile, theme, color, citeStatus);
effectiveTitle = effectiveTitle || parsed.title;
effectiveAuthor = effectiveAuthor || parsed.author;
effectiveSummary = effectiveSummary || parsed.summary;
@@ -677,6 +684,7 @@ Options:
--markdown <path> Markdown file to convert and post (recommended)
--theme <name> Theme for markdown (default, grace, simple, modern)
--color <name|hex> Primary color (blue, green, vermilion, etc. or hex)
--no-cite Disable bottom citations for ordinary external links in markdown mode
--author <name> Author name
--summary <text> Article summary
--image <path> Content image, can repeat (only with --content)
@@ -687,13 +695,15 @@ Options:
Examples:
npx -y bun wechat-article.ts --markdown article.md
npx -y bun wechat-article.ts --markdown article.md --theme grace --submit
npx -y bun wechat-article.ts --markdown article.md --no-cite
npx -y bun wechat-article.ts --title "标题" --content "内容" --image img.png
npx -y bun wechat-article.ts --title "标题" --html article.html --submit
Markdown mode:
Images in markdown are converted to placeholders. After pasting HTML,
each placeholder is selected, scrolled into view, deleted, and replaced
with the actual image via paste.
with the actual image via paste. Ordinary external links are converted to
bottom citations by default.
`);
process.exit(0);
}
@@ -709,6 +719,7 @@ async function main(): Promise<void> {
let markdownFile: string | undefined;
let theme: string | undefined;
let color: string | undefined;
let citeStatus = true;
let author: string | undefined;
let summary: string | undefined;
let submit = false;
@@ -723,6 +734,8 @@ async function main(): Promise<void> {
else if (arg === '--markdown' && args[i + 1]) markdownFile = args[++i];
else if (arg === '--theme' && args[i + 1]) theme = args[++i];
else if (arg === '--color' && args[i + 1]) color = args[++i];
else if (arg === '--cite') citeStatus = true;
else if (arg === '--no-cite') citeStatus = false;
else if (arg === '--author' && args[i + 1]) author = args[++i];
else if (arg === '--summary' && args[i + 1]) summary = args[++i];
else if (arg === '--image' && args[i + 1]) images.push(args[++i]!);
@@ -734,7 +747,7 @@ async function main(): Promise<void> {
if (!markdownFile && !htmlFile && !title) { console.error('Error: --title is required (or use --markdown/--html)'); process.exit(1); }
if (!markdownFile && !htmlFile && !content) { console.error('Error: --content, --html, or --markdown is required'); process.exit(1); }
await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, color, author, summary, images, submit, profileDir, cdpPort });
await postArticle({ title: title || '', content, htmlFile, markdownFile, theme, color, citeStatus, author, summary, images, submit, profileDir, cdpPort });
}
await main().then(() => {