Compare commits

...

11 Commits

Author SHA1 Message Date
Jim Liu 宝玉 a7d6b5c69b chore: release v1.42.0 2026-02-28 11:51:46 -06:00
Jim Liu 宝玉 6e18725906 feat(baoyu-post-to-wechat): add default color preference and modern theme support 2026-02-28 11:48:59 -06:00
Jim Liu 宝玉 7133d54f04 feat(baoyu-markdown-to-html): consolidate heritage and warm into modern theme with per-theme color defaults 2026-02-28 11:48:55 -06:00
Jim Liu 宝玉 208bb20e98 chore: release v1.41.0 2026-02-28 11:25:22 -06:00
Jim Liu 宝玉 6edc1b7822 feat(baoyu-markdown-to-html): rename themes and add color presets with theme defaults
Rename red→heritage and orange→warm themes. Add 13 named color presets,
serif-cjk font family, and per-theme style defaults with CLI override support.
2026-02-28 11:18:16 -06:00
Jim Liu 宝玉 e754eae5ae chore: release v1.40.1 2026-02-28 01:38:40 -06:00
Jim Liu 宝玉 cc016f3b16 feat(baoyu-image-gen): clarify model resolution priority and add model display requirement
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-28 01:38:10 -06:00
Jim Liu 宝玉 2188160418 chore: release v1.40.0 2026-02-28 01:15:53 -06:00
Jim Liu 宝玉 77204d2e4a feat(baoyu-markdown-to-html): add CLI customization options and EXTEND.md config support
Add --color, --font-family, --font-size, --code-theme, --mac-code-block,
--line-number, --cite, --count, --legend options. Support EXTEND.md for
default preferences. Add code highlight theme fetching and mermaid fix.
2026-02-28 01:15:16 -06:00
Jim Liu 宝玉 35eeb62f10 Merge pull request #57 from zhao-newname/main
feat(baoyu-image-gen): support OpenAI chat completions endpoint for i…
2026-02-28 01:13:19 -06:00
jeff.zhao 7dfa2b6e6c feat(baoyu-image-gen): support OpenAI chat completions endpoint for image generation
Add OPENAI_IMAGE_USE_CHAT env var (true|false) to route image generation
through /chat/completions instead of /images/generations. Extracts base64
image data URLs from the chat response, enabling compatibility with
OpenAI-compatible providers that return images via chat API.
2026-02-28 14:46:05 +08:00
16 changed files with 543 additions and 289 deletions
+1 -1
View File
@@ -6,7 +6,7 @@
},
"metadata": {
"description": "Skills shared by Baoyu for improving daily work efficiency",
"version": "1.39.0"
"version": "1.42.0"
},
"plugins": [
{
+22
View File
@@ -2,6 +2,28 @@
English | [中文](./CHANGELOG.zh.md)
## 1.42.0 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: consolidate heritage and warm into single modern theme, add per-theme color defaults (default→blue, grace→purple, simple→green, modern→orange)
- `baoyu-post-to-wechat`: add default color preference support in EXTEND.md, add modern theme option to first-time setup
## 1.41.0 - 2026-02-28
### Features
- `baoyu-markdown-to-html`: rename themes (red→heritage, orange→warm), add 13 named color presets, serif-cjk font family, and per-theme style defaults
## 1.40.1 - 2026-02-28
### Features
- `baoyu-image-gen`: clarify model resolution priority (EXTEND.md overrides env vars) and display current model with switch hints during generation
## 1.40.0 - 2026-02-28
### Features
- `baoyu-image-gen`: support OpenAI chat completions endpoint for image generation (by @zhao-newname)
- `baoyu-markdown-to-html`: add CLI customization options (--color, --font-family, --font-size, --code-theme, --mac-code-block, --line-number, --cite, --count, --legend) and EXTEND.md config support
## 1.39.0 - 2026-02-28
### Features
+22
View File
@@ -2,6 +2,28 @@
[English](./CHANGELOG.md) | 中文
## 1.42.0 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:合并 heritage 和 warm 为 modern 主题,新增主题默认颜色(default→蓝、grace→紫、simple→绿、modern→橙)
- `baoyu-post-to-wechat`:EXTEND.md 新增默认颜色配置,首次设置增加 modern 主题和颜色选择
## 1.41.0 - 2026-02-28
### 新功能
- `baoyu-markdown-to-html`:重命名主题(red→heritage、orange→warm),新增 13 个颜色预设、serif-cjk 字体、主题级样式默认值
## 1.40.1 - 2026-02-28
### 新功能
- `baoyu-image-gen`:明确模型解析优先级(EXTEND.md 优先于环境变量),生成图片时显示当前模型及切换方式
## 1.40.0 - 2026-02-28
### 新功能
- `baoyu-image-gen`:支持 OpenAI Chat Completions 端点生成图片 (by @zhao-newname)
- `baoyu-markdown-to-html`:新增 CLI 自定义选项(--color、--font-family、--font-size、--code-theme、--mac-code-block、--line-number、--cite、--count、--legend)及 EXTEND.md 配置支持
## 1.39.0 - 2026-02-28
### 新功能
+14 -6
View File
@@ -110,14 +110,22 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts --prompt "A cat" --image out.png --provi
**Load Priority**: CLI args > EXTEND.md > env vars > `<cwd>/.baoyu-skills/.env` > `~/.baoyu-skills/.env`
## Replicate Model Configuration
## Model Resolution
When using `--provider replicate`, the model can be configured in the following ways (highest priority first):
Model priority (highest → lowest), applies to all providers:
1. CLI flag: `--model <owner/name>`
2. EXTEND.md: `default_model.replicate`
3. Env var: `REPLICATE_IMAGE_MODEL`
4. Built-in default: `google/nano-banana-pro`
1. CLI flag: `--model <id>`
2. EXTEND.md: `default_model.[provider]`
3. Env var: `<PROVIDER>_IMAGE_MODEL` (e.g., `GOOGLE_IMAGE_MODEL`)
4. Built-in default
**EXTEND.md overrides env vars**. If both EXTEND.md `default_model.google: "gemini-3-pro-image-preview"` and env var `GOOGLE_IMAGE_MODEL=gemini-3.1-flash-image-preview` exist, EXTEND.md wins.
**Agent MUST display model info** before each generation:
- Show: `Using [provider] / [model]`
- Show switch hint: `Switch model: --model <id> | EXTEND.md default_model.[provider] | env <PROVIDER>_IMAGE_MODEL`
### Replicate Models
Supported model formats:
+1
View File
@@ -36,6 +36,7 @@ Environment variables:
DASHSCOPE_IMAGE_MODEL Default DashScope model (z-image-turbo)
REPLICATE_IMAGE_MODEL Default Replicate model (google/nano-banana-pro)
OPENAI_BASE_URL Custom OpenAI endpoint
OPENAI_IMAGE_USE_CHAT Use /chat/completions instead of /images/generations (true|false)
GOOGLE_BASE_URL Custom Google endpoint
DASHSCOPE_BASE_URL Custom DashScope endpoint
REPLICATE_BASE_URL Custom Replicate endpoint
@@ -70,6 +70,10 @@ export async function generateImage(
if (!apiKey) throw new Error("OPENAI_API_KEY is required");
if (process.env.OPENAI_IMAGE_USE_CHAT === "true") {
return generateWithChatCompletions(baseURL, apiKey, prompt, model);
}
const size = args.size || getOpenAISize(model, args.aspectRatio, args.quality);
if (args.referenceImages.length > 0) {
@@ -84,6 +88,40 @@ export async function generateImage(
return generateWithOpenAIGenerations(baseURL, apiKey, prompt, model, size, args.quality);
}
async function generateWithChatCompletions(
baseURL: string,
apiKey: string,
prompt: string,
model: string
): Promise<Uint8Array> {
const res = await fetch(`${baseURL}/chat/completions`, {
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${apiKey}`,
},
body: JSON.stringify({
model,
messages: [{ role: "user", content: prompt }],
}),
});
if (!res.ok) {
const err = await res.text();
throw new Error(`OpenAI API error: ${err}`);
}
const result = (await res.json()) as { choices: Array<{ message: { content: string } }> };
const content = result.choices[0]?.message?.content ?? "";
const match = content.match(/data:image\/[^;]+;base64,([A-Za-z0-9+/=]+)/);
if (match) {
return Uint8Array.from(Buffer.from(match[1]!, "base64"));
}
throw new Error("No image found in chat completions response");
}
async function generateWithOpenAIGenerations(
baseURL: string,
apiKey: string,
+27 -5
View File
@@ -92,8 +92,7 @@ test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && grep -o 'default
| `default` (Recommended) | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 |
| `red` | 红色主题 - 红金配色,宋体排版,传统书法风格 |
| `orange` | 橙色主题 - 暖色调现代风,宽松行距,圆角装饰 |
| `modern` | 现代主题 - 大圆角、药丸形标题、宽松行距(搭配 `--color red` 还原传统红金风格 |
### Step 2: Convert
@@ -115,11 +114,32 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts <markdown_file> [options]
| Option | Description | Default |
|--------|-------------|---------|
| `--theme <name>` | Theme name (default, grace, simple, red, orange) | default |
| `--theme <name>` | Theme name (default, grace, simple, modern) | default |
| `--color <name\|hex>` | Primary color: preset name or hex value | theme default |
| `--font-family <name>` | Font: sans, serif, serif-cjk, mono, or CSS value | theme default |
| `--font-size <N>` | Font size: 14px, 15px, 16px, 17px, 18px | 16px |
| `--title <title>` | Override title from frontmatter | |
| `--keep-title` | Keep the first heading in content | false (removed) |
| `--help` | Show help | |
**Color Presets:**
| Name | Hex | Label |
|------|-----|-------|
| blue | #0F4C81 | 经典蓝 |
| green | #009874 | 翡翠绿 |
| vermilion | #FA5151 | 活力橘 |
| yellow | #FECE00 | 柠檬黄 |
| purple | #92617E | 薰衣紫 |
| sky | #55C9EA | 天空蓝 |
| rose | #B76E79 | 玫瑰金 |
| olive | #556B2F | 橄榄绿 |
| black | #333333 | 石墨黑 |
| gray | #A9A9A9 | 雾烟灰 |
| pink | #FFB7C5 | 樱花粉 |
| red | #A93226 | 中国红 |
| orange | #D97757 | 暖橘 (modern default) |
**Examples:**
```bash
@@ -129,6 +149,9 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts article.md
# With specific theme
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --theme grace
# Theme with custom color
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --theme modern --color red
# Keep the first heading in content
npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --keep-title
@@ -171,8 +194,7 @@ npx -y bun ${SKILL_DIR}/scripts/main.ts article.md --title "My Article"
| `default` | 经典主题 - 传统排版,标题居中带底边,二级标题白字彩底 |
| `grace` | 优雅主题 - 文字阴影,圆角卡片,精致引用块 (by @brzhang) |
| `simple` | 简洁主题 - 现代极简风,不对称圆角,清爽留白 (by @okooo5km) |
| `red` | 红色主题 - 红金配色,宋体排版,传统书法风格 |
| `orange` | 橙色主题 - 暖色调现代风,宽松行距,圆角装饰 |
| `modern` | 现代主题 - 大圆角、药丸形标题、宽松行距(搭配 `--color red` 还原传统红金风格 |
## Supported Markdown Features
@@ -1,6 +1,7 @@
#!/usr/bin/env npx tsx
import fs from "node:fs";
import { homedir } from "node:os";
import path from "node:path";
import { fileURLToPath } from "node:url";
import frontMatter from "front-matter";
@@ -40,13 +41,91 @@ const EXTERNAL_THEME_DIR =
|| path.resolve(path.dirname(EXTERNAL_THEME_CONFIG_PATH), "theme-css");
const FALLBACK_THEMES: ThemeName[] = ["default", "grace", "simple"];
const DEFAULT_STYLE = {
const FONT_FAMILY_MAP: Record<string, string> = {
sans: `-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif`,
serif: `Optima-Regular, Optima, PingFangSC-light, PingFangTC-light, 'PingFang SC', Cambria, Cochin, Georgia, Times, 'Times New Roman', serif`,
"serif-cjk": `"Source Han Serif SC", "Noto Serif CJK SC", "Source Han Serif CN", STSong, SimSun, serif`,
mono: `Menlo, Monaco, 'Courier New', monospace`,
};
const FONT_SIZE_OPTIONS = ["14px", "15px", "16px", "17px", "18px"];
const COLOR_PRESETS: Record<string, string> = {
blue: "#0F4C81",
green: "#009874",
vermilion: "#FA5151",
yellow: "#FECE00",
purple: "#92617E",
sky: "#55C9EA",
rose: "#B76E79",
olive: "#556B2F",
black: "#333333",
gray: "#A9A9A9",
pink: "#FFB7C5",
red: "#A93226",
orange: "#D97757",
};
const CODE_BLOCK_THEMES = [
"1c-light", "a11y-dark", "a11y-light", "agate", "an-old-hope",
"androidstudio", "arduino-light", "arta", "ascetic",
"atom-one-dark-reasonable", "atom-one-dark", "atom-one-light",
"brown-paper", "codepen-embed", "color-brewer", "dark", "default",
"devibeans", "docco", "far", "felipec", "foundation",
"github-dark-dimmed", "github-dark", "github", "gml", "googlecode",
"gradient-dark", "gradient-light", "grayscale", "hybrid", "idea",
"intellij-light", "ir-black", "isbl-editor-dark", "isbl-editor-light",
"kimbie-dark", "kimbie-light", "lightfair", "lioshi", "magula",
"mono-blue", "monokai-sublime", "monokai", "night-owl", "nnfx-dark",
"nnfx-light", "nord", "obsidian", "panda-syntax-dark",
"panda-syntax-light", "paraiso-dark", "paraiso-light", "pojoaque",
"purebasic", "qtcreator-dark", "qtcreator-light", "rainbow", "routeros",
"school-book", "shades-of-purple", "srcery", "stackoverflow-dark",
"stackoverflow-light", "sunburst", "tokyo-night-dark", "tokyo-night-light",
"tomorrow-night-blue", "tomorrow-night-bright", "vs", "vs2015", "xcode",
"xt256",
];
const HLJS_CDN_BASE = "https://cdn-doocs.oss-cn-shenzhen.aliyuncs.com/npm/highlightjs/11.11.1";
interface StyleConfig {
primaryColor: string;
fontFamily: string;
fontSize: string;
foreground: string;
blockquoteBackground: string;
accentColor: string;
containerBg: string;
}
const DEFAULT_STYLE: StyleConfig = {
primaryColor: "#0F4C81",
fontFamily:
"-apple-system-font,BlinkMacSystemFont, Helvetica Neue, PingFang SC, Hiragino Sans GB , Microsoft YaHei UI , Microsoft YaHei ,Arial,sans-serif",
fontFamily: FONT_FAMILY_MAP.sans!,
fontSize: "16px",
foreground: "0 0% 3.9%",
blockquoteBackground: "#f7f7f7",
accentColor: "#6B7280",
containerBg: "transparent",
};
const THEME_STYLE_DEFAULTS: Record<string, Partial<StyleConfig>> = {
default: {
primaryColor: COLOR_PRESETS.blue,
},
grace: {
primaryColor: COLOR_PRESETS.purple,
},
simple: {
primaryColor: COLOR_PRESETS.green,
},
modern: {
primaryColor: COLOR_PRESETS.orange,
accentColor: "#E4B1A0",
containerBg: "rgba(250, 249, 245, 1)",
fontFamily: FONT_FAMILY_MAP.sans,
fontSize: "15px",
blockquoteBackground: "rgba(255, 255, 255, 0.6)",
},
};
Object.entries(COMMON_LANGUAGES).forEach(([name, lang]) => {
@@ -490,46 +569,137 @@ function printUsage(): void {
console.error(
[
"Usage:",
" npx tsx src/md/render.ts <markdown_file> [--theme <name>]",
" npx tsx src/md/render.ts <markdown_file> [options]",
"",
"Options:",
` --theme Theme name (${THEME_NAMES.join(", ")})`,
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
` --color <name|hex> Primary color: ${Object.keys(COLOR_PRESETS).join(", ")}, or hex`,
` --font-family <name> Font: ${Object.keys(FONT_FAMILY_MAP).join(", ")}, or CSS value`,
` --font-size <N> Font size: ${FONT_SIZE_OPTIONS.join(", ")} (default: 16px)`,
` --code-theme <name> Code highlight theme (default: github)`,
` --mac-code-block Show Mac-style code block header`,
` --line-number Show line numbers in code blocks`,
` --cite Enable footnote citations`,
` --count Show reading time / word count`,
` --legend <value> Image caption: title-alt, alt-title, title, alt, none`,
` --keep-title Keep the first heading in output`,
].join("\n")
);
}
function parseArgValue(argv: string[], i: number, flag: string): string | null {
const arg = argv[i]!;
if (arg.includes("=")) {
return arg.slice(flag.length + 1);
}
const next = argv[i + 1];
return next ?? null;
}
function resolveFontFamily(value: string): string {
return FONT_FAMILY_MAP[value] ?? value;
}
function resolveColor(value: string): string {
return COLOR_PRESETS[value] ?? value;
}
function parseArgs(argv: string[]): CliOptions | null {
const ext = loadExtendConfig();
let inputPath = "";
let theme: ThemeName = "default";
let keepTitle = false;
let theme: ThemeName = ext.default_theme ?? "default";
let keepTitle = ext.keep_title ?? false;
let primaryColor: string | undefined = ext.default_color ? resolveColor(ext.default_color) : undefined;
let fontFamily: string | undefined = ext.default_font_family ? resolveFontFamily(ext.default_font_family) : undefined;
let fontSize: string | undefined = ext.default_font_size ?? undefined;
let codeTheme = ext.default_code_theme ?? "github";
let isMacCodeBlock = ext.mac_code_block ?? true;
let isShowLineNumber = ext.show_line_number ?? false;
let citeStatus = ext.cite ?? false;
let countStatus = ext.count ?? false;
let legend = ext.legend ?? "alt";
for (let i = 0; i < argv.length; i += 1) {
const arg = argv[i];
const arg = argv[i]!;
if (!arg.startsWith("--") && !inputPath) {
inputPath = arg;
continue;
}
if (arg === "--theme") {
theme = (argv[i + 1] || "") as ThemeName;
i += 1;
continue;
}
if (arg.startsWith("--theme=")) {
theme = arg.slice("--theme=".length) as ThemeName;
continue;
}
if (arg === "--keep-title") {
keepTitle = true;
continue;
}
if (arg === "--help" || arg === "-h") {
return null;
}
if (arg === "--keep-title") { keepTitle = true; continue; }
if (arg === "--mac-code-block") { isMacCodeBlock = true; continue; }
if (arg === "--no-mac-code-block") { isMacCodeBlock = false; continue; }
if (arg === "--line-number") { isShowLineNumber = true; continue; }
if (arg === "--cite") { citeStatus = true; continue; }
if (arg === "--count") { countStatus = true; continue; }
if (arg === "--theme" || arg.startsWith("--theme=")) {
const val = parseArgValue(argv, i, "--theme");
if (!val) { console.error("Missing value for --theme"); return null; }
theme = val as ThemeName;
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--color" || arg.startsWith("--color=")) {
const val = parseArgValue(argv, i, "--color");
if (!val) { console.error("Missing value for --color"); return null; }
primaryColor = resolveColor(val);
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--font-family" || arg.startsWith("--font-family=")) {
const val = parseArgValue(argv, i, "--font-family");
if (!val) { console.error("Missing value for --font-family"); return null; }
fontFamily = resolveFontFamily(val);
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--font-size" || arg.startsWith("--font-size=")) {
const val = parseArgValue(argv, i, "--font-size");
if (!val) { console.error("Missing value for --font-size"); return null; }
fontSize = val.endsWith("px") ? val : `${val}px`;
if (!FONT_SIZE_OPTIONS.includes(fontSize)) {
console.error(`Invalid font size: ${fontSize}. Valid: ${FONT_SIZE_OPTIONS.join(", ")}`);
return null;
}
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--code-theme" || arg.startsWith("--code-theme=")) {
const val = parseArgValue(argv, i, "--code-theme");
if (!val) { console.error("Missing value for --code-theme"); return null; }
codeTheme = val;
if (!CODE_BLOCK_THEMES.includes(codeTheme)) {
console.error(`Unknown code theme: ${codeTheme}`);
return null;
}
if (!arg.includes("=")) i += 1;
continue;
}
if (arg === "--legend" || arg.startsWith("--legend=")) {
const val = parseArgValue(argv, i, "--legend");
if (!val) { console.error("Missing value for --legend"); return null; }
const valid = ["title-alt", "alt-title", "title", "alt", "none"];
if (!valid.includes(val)) {
console.error(`Invalid legend: ${val}. Valid: ${valid.join(", ")}`);
return null;
}
legend = val;
if (!arg.includes("=")) i += 1;
continue;
}
console.error(`Unknown argument: ${arg}`);
return null;
}
@@ -544,9 +714,8 @@ function parseArgs(argv: string[]): CliOptions | null {
}
return {
inputPath,
theme,
keepTitle,
inputPath, theme, keepTitle, primaryColor, fontFamily, fontSize,
codeTheme, isMacCodeBlock, isShowLineNumber, citeStatus, countStatus, legend,
};
}
@@ -554,6 +723,78 @@ interface CliOptions {
inputPath: string;
theme: ThemeName;
keepTitle: boolean;
primaryColor?: string;
fontFamily?: string;
fontSize?: string;
codeTheme: string;
isMacCodeBlock: boolean;
isShowLineNumber: boolean;
citeStatus: boolean;
countStatus: boolean;
legend: string;
}
interface ExtendConfig {
default_theme: string | null;
default_color: string | null;
default_font_family: string | null;
default_font_size: string | null;
default_code_theme: string | null;
mac_code_block: boolean | null;
show_line_number: boolean | null;
cite: boolean | null;
count: boolean | null;
legend: string | null;
keep_title: boolean | null;
}
function extractYamlFrontMatter(content: string): string | null {
const match = content.match(/^---\s*\n([\s\S]*?)\n---\s*$/m);
return match ? match[1]! : null;
}
function parseExtendYaml(yaml: string): Partial<ExtendConfig> {
const config: Partial<ExtendConfig> = {};
for (const line of yaml.split("\n")) {
const trimmed = line.trim();
if (!trimmed || trimmed.startsWith("#")) continue;
const colonIdx = trimmed.indexOf(":");
if (colonIdx < 0) continue;
const key = trimmed.slice(0, colonIdx).trim();
let value = trimmed.slice(colonIdx + 1).trim().replace(/^['"]|['"]$/g, "");
if (value === "null" || value === "") continue;
if (key === "default_theme") config.default_theme = value;
else if (key === "default_color") config.default_color = value;
else if (key === "default_font_family") config.default_font_family = value;
else if (key === "default_font_size") config.default_font_size = value.endsWith("px") ? value : `${value}px`;
else if (key === "default_code_theme") config.default_code_theme = value;
else if (key === "mac_code_block") config.mac_code_block = value === "true";
else if (key === "show_line_number") config.show_line_number = value === "true";
else if (key === "cite") config.cite = value === "true";
else if (key === "count") config.count = value === "true";
else if (key === "legend") config.legend = value;
else if (key === "keep_title") config.keep_title = value === "true";
}
return config;
}
function loadExtendConfig(): Partial<ExtendConfig> {
const paths = [
path.join(process.cwd(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
path.join(homedir(), ".baoyu-skills", "baoyu-markdown-to-html", "EXTEND.md"),
];
for (const p of paths) {
try {
const content = fs.readFileSync(p, "utf-8");
const yaml = extractYamlFrontMatter(content);
if (!yaml) continue;
return parseExtendYaml(yaml);
} catch {
continue;
}
}
return {};
}
function preprocessCjkEmphasis(markdown: string): string {
@@ -679,14 +920,16 @@ function loadThemeCss(theme: ThemeName): {
};
}
function buildCss(baseCss: string, themeCss: string): string {
function buildCss(baseCss: string, themeCss: string, style: StyleConfig = DEFAULT_STYLE): string {
const variables = `
:root {
--md-primary-color: ${DEFAULT_STYLE.primaryColor};
--md-font-family: ${DEFAULT_STYLE.fontFamily};
--md-font-size: ${DEFAULT_STYLE.fontSize};
--foreground: ${DEFAULT_STYLE.foreground};
--blockquote-background: ${DEFAULT_STYLE.blockquoteBackground};
--md-primary-color: ${style.primaryColor};
--md-font-family: ${style.fontFamily};
--md-font-size: ${style.fontSize};
--foreground: ${style.foreground};
--blockquote-background: ${style.blockquoteBackground};
--md-accent-color: ${style.accentColor};
--md-container-bg: ${style.containerBg};
}
body {
@@ -708,13 +951,28 @@ function normalizeThemeCss(css: string): string {
return stripOutputScope(css);
}
async function fetchCodeThemeCss(themeName: string): Promise<string> {
const url = `${HLJS_CDN_BASE}/styles/${themeName}.min.css`;
try {
const res = await fetch(url);
if (!res.ok) {
console.error(`Failed to fetch code theme CSS: ${res.status} ${url}`);
return "";
}
return await res.text();
} catch (error) {
console.error(`Failed to fetch code theme CSS from ${url}:`, error);
return "";
}
}
interface HtmlDocumentMeta {
title: string;
author?: string;
description?: string;
}
function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): string {
function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string, codeThemeCss?: string): string {
const lines = [
"<!doctype html>",
"<html>",
@@ -729,8 +987,11 @@ function buildHtmlDocument(meta: HtmlDocumentMeta, css: string, html: string): s
if (meta.description) {
lines.push(` <meta name="description" content="${meta.description}" />`);
}
lines.push(` <style>${css}</style>`);
if (codeThemeCss) {
lines.push(` <style>${codeThemeCss}</style>`);
}
lines.push(
` <style>${css}</style>`,
"</head>",
"<body>",
' <div id="output">',
@@ -758,34 +1019,38 @@ async function inlineCss(html: string): Promise<string> {
}
}
function normalizeCssText(cssText: string): string {
function normalizeCssText(cssText: string, style: StyleConfig = DEFAULT_STYLE): string {
return cssText
.replace(/var\(--md-primary-color\)/g, DEFAULT_STYLE.primaryColor)
.replace(/var\(--md-font-family\)/g, DEFAULT_STYLE.fontFamily)
.replace(/var\(--md-font-size\)/g, DEFAULT_STYLE.fontSize)
.replace(/var\(--blockquote-background\)/g, DEFAULT_STYLE.blockquoteBackground)
.replace(/var\(--md-primary-color\)/g, style.primaryColor)
.replace(/var\(--md-font-family\)/g, style.fontFamily)
.replace(/var\(--md-font-size\)/g, style.fontSize)
.replace(/var\(--blockquote-background\)/g, style.blockquoteBackground)
.replace(/var\(--md-accent-color\)/g, style.accentColor)
.replace(/var\(--md-container-bg\)/g, style.containerBg)
.replace(/hsl\(var\(--foreground\)\)/g, "#3f3f3f")
.replace(/--md-primary-color:\s*[^;"']+;?/g, "")
.replace(/--md-font-family:\s*[^;"']+;?/g, "")
.replace(/--md-font-size:\s*[^;"']+;?/g, "")
.replace(/--blockquote-background:\s*[^;"']+;?/g, "")
.replace(/--md-accent-color:\s*[^;"']+;?/g, "")
.replace(/--md-container-bg:\s*[^;"']+;?/g, "")
.replace(/--foreground:\s*[^;"']+;?/g, "");
}
function normalizeInlineCss(html: string): string {
function normalizeInlineCss(html: string, style: StyleConfig = DEFAULT_STYLE): string {
let output = html;
output = output.replace(
/<style([^>]*)>([\s\S]*?)<\/style>/gi,
(_match, attrs: string, cssText: string) =>
`<style${attrs}>${normalizeCssText(cssText)}</style>`
`<style${attrs}>${normalizeCssText(cssText, style)}</style>`
);
output = output.replace(
/style="([^"]*)"/gi,
(_match, cssText: string) => `style="${normalizeCssText(cssText)}"`
(_match, cssText: string) => `style="${normalizeCssText(cssText, style)}"`
);
output = output.replace(
/style='([^']*)'/gi,
(_match, cssText: string) => `style='${normalizeCssText(cssText)}'`
(_match, cssText: string) => `style='${normalizeCssText(cssText, style)}'`
);
return output;
}
@@ -824,11 +1089,29 @@ async function main(): Promise<void> {
options.inputPath.replace(/\.md$/i, ".html")
);
const themeDefaults = THEME_STYLE_DEFAULTS[options.theme] ?? {};
const style: StyleConfig = {
...DEFAULT_STYLE,
...themeDefaults,
...(options.primaryColor !== undefined ? { primaryColor: options.primaryColor } : {}),
...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
...(options.fontSize !== undefined ? { fontSize: options.fontSize } : {}),
};
const { baseCss, themeCss } = loadThemeCss(options.theme);
const css = normalizeThemeCss(buildCss(baseCss, themeCss));
const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
const codeThemeCss = await fetchCodeThemeCss(options.codeTheme);
const markdown = fs.readFileSync(inputPath, "utf-8");
const renderer = initRenderer({});
const renderer = initRenderer({
legend: options.legend,
citeStatus: options.citeStatus,
countStatus: options.countStatus,
isMacCodeBlock: options.isMacCodeBlock,
isShowLineNumber: options.isShowLineNumber,
});
const { yamlData } = renderer.parseFrontMatterAndContent(markdown);
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(
markdown,
@@ -855,8 +1138,8 @@ async function main(): Promise<void> {
author: stripQuotes(yamlData.author),
description: stripQuotes(yamlData.description) || stripQuotes(yamlData.summary),
};
const html = buildHtmlDocument(meta, css, content);
const inlinedHtml = normalizeInlineCss(await inlineCss(html));
const html = buildHtmlDocument(meta, css, content, codeThemeCss);
const inlinedHtml = normalizeInlineCss(await inlineCss(html), style);
const finalHtml = modifyHtmlStructure(inlinedHtml);
let backupPath = "";
@@ -32,3 +32,8 @@ blockquote {
#output section > :first-child {
margin-top: 0 !important;
}
.mermaid-diagram .nodeLabel p {
color: unset !important;
letter-spacing: unset !important;
}
@@ -269,6 +269,7 @@ pre.code__pre,
padding: 0 !important;
line-height: 1.5;
margin: 10px 8px;
box-shadow: inset 0 0 10px rgba(0,0,0,0.05);
}
/* ==================== 图片 ==================== */
@@ -1,23 +1,23 @@
/**
* MD 橙色主题
* 暖色调现代风橙色主色宽松行距
* MD 现代主题 (modern)
* 大圆角药丸形标题宽松行距现代感
*/
/* ==================== 容器样式覆盖 ==================== */
section,
container {
font-family: PingFang SC, system-ui, -apple-system, BlinkMacSystemFont, Helvetica Neue, Hiragino Sans GB, Microsoft YaHei UI, Microsoft YaHei, Arial, sans-serif;
font-size: 15px;
font-family: var(--md-font-family);
font-size: var(--md-font-size);
line-height: 2;
background-color: rgba(250, 249, 245, 1);
background-color: var(--md-container-bg);
border: 1px solid rgba(255, 255, 255, 0.01);
border-radius: 25px;
padding: 12px 12px;
}
#output {
font-family: PingFang SC, system-ui, -apple-system, BlinkMacSystemFont, Helvetica Neue, Hiragino Sans GB, Microsoft YaHei UI, Microsoft YaHei, Arial, sans-serif;
font-size: 15px;
font-family: var(--md-font-family);
font-size: var(--md-font-size);
line-height: 2;
}
@@ -26,8 +26,8 @@ h1 {
display: table;
padding: 0.3em 1em;
margin: 20px auto;
color: #3B3B38;
background: #D97757;
color: hsl(var(--foreground));
background: var(--md-primary-color);
border-radius: 15px;
font-size: 28px;
font-weight: bold;
@@ -41,16 +41,16 @@ h2 {
padding-bottom: 0;
margin: 0 auto 20px;
width: 100%;
color: #D97757;
color: var(--md-primary-color);
font-size: 20px;
font-weight: bold;
border-bottom: 2px solid #E4B1A0;
border-bottom: 2px solid var(--md-accent-color);
}
/* ==================== 三级标题 ==================== */
h3 {
margin: 0 8px 10px;
color: #3B3B38;
color: hsl(var(--foreground));
font-size: 18px;
font-weight: bold;
}
@@ -58,7 +58,7 @@ h3 {
/* ==================== 四级标题 ==================== */
h4 {
margin: 0 8px 10px;
color: #D97757;
color: var(--md-primary-color);
font-size: 16px;
font-weight: bold;
}
@@ -68,7 +68,7 @@ h5 {
display: inline-block;
margin: 0 8px 10px;
padding: 4px 10px;
color: #3B3B38;
color: hsl(var(--foreground));
background: rgba(255, 255, 255, 0.7);
border: 1px solid rgb(189, 224, 254);
border-radius: 20px;
@@ -79,7 +79,7 @@ h5 {
/* ==================== 六级标题 ==================== */
h6 {
margin: 0 8px 10px;
color: #D97757;
color: var(--md-primary-color);
font-size: 16px;
font-weight: bold;
}
@@ -87,7 +87,7 @@ h6 {
/* ==================== 段落 ==================== */
p {
margin: 20px 0;
color: #3B3B38;
color: hsl(var(--foreground));
line-height: 2;
letter-spacing: 0px;
font-size: 15px;
@@ -97,15 +97,15 @@ p {
blockquote {
font-style: normal;
padding: 15px 12px;
border-left: 7px solid rgba(228, 177, 160, 1);
border-left: 7px solid var(--md-accent-color);
border-radius: 10px;
color: #3B3B38;
background-color: rgba(255, 255, 255, 0.6);
color: hsl(var(--foreground));
background-color: var(--blockquote-background);
margin: 12px 0;
}
blockquote > p {
color: #3B3B38;
color: hsl(var(--foreground));
font-size: 14px;
margin: 0;
}
@@ -114,6 +114,7 @@ blockquote > p {
pre.code__pre,
.hljs.code__pre {
border-radius: 10px;
box-shadow: inset 0 0 10px rgba(0, 0, 0, 0.05);
}
/* ==================== 图片 ==================== */
@@ -138,7 +139,7 @@ ul {
li {
margin: 0.2em 0;
color: #3B3B38;
color: hsl(var(--foreground));
font-size: 15px;
}
@@ -146,35 +147,35 @@ li {
hr {
border-style: solid;
border-width: 1px 0 0;
border-color: #D97757;
border-color: var(--md-primary-color);
margin: 0;
}
/* ==================== 强调 ==================== */
strong {
color: #3B3B38;
color: hsl(var(--foreground));
font-weight: bold;
}
/* ==================== 标记高亮 ==================== */
.markup-highlight {
background-color: #3B3B3B;
background-color: hsl(var(--foreground));
padding: 10px;
color: #FAF9F5;
color: var(--md-container-bg);
}
.markup-underline {
text-decoration: underline;
text-decoration-color: #E4B1A0;
text-decoration-color: var(--md-accent-color);
}
/* ==================== 链接 ==================== */
a {
color: #D97757;
color: var(--md-primary-color);
text-decoration: none;
}
/* ==================== 表格 ==================== */
th {
background: rgba(217, 119, 87, 0.1);
background: color-mix(in srgb, var(--md-primary-color) 10%, transparent);
}
@@ -1,182 +0,0 @@
/**
* MD 红色主题
* 传统书法风格,红金配色,宋体排版
*/
/* ==================== 容器字体覆盖 ==================== */
section,
container {
font-family: "Source Han Serif SC", "Noto Serif CJK SC", "Source Han Serif CN", STSong, SimSun, serif;
font-size: 16px;
line-height: 1.8;
background-color: rgba(255, 251, 240, 1);
border: 4px solid rgba(169, 50, 38, 1);
border-radius: 12px;
padding: 6px 12px;
}
#output {
font-family: "Source Han Serif SC", "Noto Serif CJK SC", "Source Han Serif CN", STSong, SimSun, serif;
font-size: 16px;
line-height: 1.8;
}
/* ==================== 一级标题 ==================== */
h1 {
display: table;
padding: 0 1em;
border-bottom: 2px solid #A93226;
margin: 30px auto 20px;
color: #A93226;
font-size: 24px;
font-weight: bold;
text-align: center;
}
/* ==================== 二级标题 ==================== */
h2 {
display: block;
width: fit-content;
padding: 6px 20px;
margin: 30px auto 20px;
color: #FFFFFF;
background-color: #A93226;
font-size: 18px;
font-weight: bold;
text-align: center;
border-radius: 8px;
box-shadow: rgba(0, 0, 0, 0.1) 0px 4px 6px;
line-height: 1.6em;
}
/* ==================== 三级标题 ==================== */
h3 {
padding-left: 8px;
border-left: 3px solid #A93226;
margin: 20px 8px 10px 0;
color: #A93226;
font-size: 17px;
font-weight: normal;
line-height: 1.2;
}
/* ==================== 四级标题 ==================== */
h4 {
margin: 10px 8px;
color: #D4AC0D;
font-size: 16px;
font-weight: normal;
}
/* ==================== 五级标题 ==================== */
h5 {
margin: 10px 8px;
color: #797D7F;
font-size: 15px;
font-weight: normal;
}
/* ==================== 六级标题 ==================== */
h6 {
margin: 10px 8px;
color: #797D7F;
font-size: 14px;
font-weight: normal;
}
/* ==================== 段落 ==================== */
p {
margin: 16px 0;
letter-spacing: 0.5px;
color: #2C2C2C;
line-height: 1.8;
}
/* ==================== 引用块 ==================== */
blockquote {
font-style: normal;
padding: 15px 12px;
border-left: 2px solid rgba(169, 50, 38, 0.8);
border-radius: 4px;
color: #5D4037;
background-color: rgba(253, 237, 236, 1);
margin: 12px 0;
}
blockquote > p {
color: #5D4037;
font-size: 14px;
margin: 0;
}
/* ==================== 代码块 ==================== */
pre.code__pre,
.hljs.code__pre {
font-size: 13px;
border: 1px solid #D4AC0D;
}
/* ==================== 图片 ==================== */
img {
border-radius: 4px;
border: 1px solid #D4AC0D;
margin: 20px auto;
padding: 5px;
}
/* ==================== 列表 ==================== */
ol {
padding-left: 1em;
margin: 15px 0;
line-height: 1.6;
}
ul {
list-style: none;
padding-left: 0;
margin: 15px 0;
line-height: 1.6;
}
li {
margin: 0.2em 0;
color: #2C2C2C;
font-size: 16px;
}
/* ==================== 分隔线 ==================== */
hr {
border-style: solid;
border-width: 2px 0 0;
border-color: #D4AC0D;
margin: 40px 0;
}
/* ==================== 强调 ==================== */
strong {
color: #A93226;
font-weight: bold;
}
/* ==================== 标记高亮 ==================== */
.markup-highlight {
background-color: #FADBD8;
padding: 5px;
color: #A93226;
}
.markup-underline {
text-decoration: underline;
text-decoration-color: #D4AC0D;
}
/* ==================== 链接 ==================== */
a {
color: #A93226;
text-decoration: none;
}
/* ==================== 表格 ==================== */
th {
background: rgba(169, 50, 38, 0.1);
}
+18 -6
View File
@@ -48,7 +48,7 @@ test -f "$HOME/.baoyu-skills/baoyu-post-to-wechat/EXTEND.md" && echo "user"
│ Not found │ Run first-time setup ([references/config/first-time-setup.md](references/config/first-time-setup.md)) → Save → Continue │
└───────────┴───────────────────────────────────────────────────────────────────────────┘
**EXTEND.md Supports**: Default theme | Default publishing method (api/browser) | Default author | Default open-comment switch | Default fans-only-comment switch | Chrome profile path
**EXTEND.md Supports**: Default theme | Default color | Default publishing method (api/browser) | Default author | Default open-comment switch | Default fans-only-comment switch | Chrome profile path
First-time setup: [references/config/first-time-setup.md](references/config/first-time-setup.md)
@@ -64,6 +64,7 @@ First-time setup: [references/config/first-time-setup.md](references/config/firs
```md
default_theme: default
default_color: blue
default_publish_method: api
default_author: 宝玉
need_open_comment: 1
@@ -71,6 +72,10 @@ only_fans_can_comment: 0
chrome_profile_path: /path/to/chrome/profile
```
**Theme options**: default, grace, simple, modern
**Color presets**: blue, green, vermilion, yellow, purple, sky, rose, olive, black, gray, pink, red, orange (or hex value)
**Value priority**:
1. CLI arguments
2. Frontmatter
@@ -134,6 +139,8 @@ Check and load EXTEND.md settings (see Preferences section above).
**CRITICAL**: If not found, complete first-time setup BEFORE any other steps or questions.
Resolve and store these defaults for later steps:
- `default_theme` (default `default`)
- `default_color` (omit if not set — theme default applies)
- `default_author`
- `need_open_comment` (default `1`)
- `only_fans_can_comment` (default `0`)
@@ -201,13 +208,18 @@ B) Continue - provide HTML file manually
- EXTEND.md `default_theme` (loaded in Step 0)
- Fallback: `default`
2. **Execute conversion** (using the discovered skill), **always pass `--theme`**:
2. **Resolve color** (first match wins):
- CLI `--color` argument
- EXTEND.md `default_color` (loaded in Step 0)
- Omit if not set (theme default applies)
3. **Execute conversion** (using the discovered skill), **always pass `--theme`**:
```bash
npx -y bun ${MD_TO_HTML_SKILL_DIR}/scripts/main.ts <markdown_file> --theme <theme>
npx -y bun ${MD_TO_HTML_SKILL_DIR}/scripts/main.ts <markdown_file> --theme <theme> [--color <color>]
```
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`.
**CRITICAL**: Always include `--theme` parameter. Never omit it, even if using `default`. Only include `--color` if explicitly set by user or EXTEND.md.
3. **Parse JSON output** to get: `htmlPath`, `title`, `author`, `summary`, `contentImages`
@@ -307,7 +319,7 @@ WeChat Publishing Complete!
Input: [type] - [path]
Method: API
Theme: [theme name]
Theme: [theme name] [color if set]
Article:
• Title: [title]
@@ -334,7 +346,7 @@ WeChat Publishing Complete!
Input: [type] - [path]
Method: Browser
Theme: [theme name]
Theme: [theme name] [color if set]
Article:
• Title: [title]
@@ -20,9 +20,9 @@ npx -y bun ./scripts/wechat-article.ts --markdown article.md --author "作者名
| Parameter | Description |
|-----------|-------------|
| `--markdown <path>` | Markdown file to convert and post |
| `--theme <name>` | Theme: default, grace, or simple |
| `--theme <name>` | Theme: default, grace, simple, modern |
| `--title <text>` | Override title (auto-extracted from markdown) |
| `--author <name>` | Author name (default: 宝玉) |
| `--author <name>` | Author name |
| `--summary <text>` | Article summary |
| `--html <path>` | Pre-rendered HTML file (alternative to markdown) |
| `--profile <dir>` | Chrome profile directory |
@@ -49,14 +49,34 @@ header: "Theme"
question: "Default theme for article conversion?"
options:
- label: "default (Recommended)"
description: "Classic layout - centered title with border, white-on-color H2"
description: "Classic layout - centered title with border, white-on-color H2 (default: blue)"
- label: "grace"
description: "Elegant - text shadows, rounded cards, refined blockquotes"
description: "Elegant - text shadows, rounded cards, refined blockquotes (default: purple)"
- label: "simple"
description: "Minimal modern - asymmetric rounded corners, clean whitespace"
description: "Minimal modern - asymmetric rounded corners, clean whitespace (default: green)"
- label: "modern"
description: "Large rounded corners, pill headings, spacious (default: orange)"
```
### Question 2: Default Publishing Method
### Question 2: Default Color
```yaml
header: "Color"
question: "Default color preset? (theme default if not set)"
options:
- label: "Theme default (Recommended)"
description: "Use the theme's built-in default color"
- label: "blue"
description: "#0F4C81 经典蓝"
- label: "red"
description: "#A93226 中国红"
- label: "green"
description: "#009874 翡翠绿"
```
Note: User can choose "Other" to type any preset name (vermilion, yellow, purple, sky, rose, olive, black, gray, pink, orange) or hex value.
### Question 3: Default Publishing Method
```yaml
header: "Method"
@@ -68,7 +88,7 @@ options:
description: "Slow, requires Chrome and login session"
```
### Question 3: Default Author
### Question 4: Default Author
```yaml
header: "Author"
@@ -80,7 +100,7 @@ options:
Note: User will likely choose "Other" to type their author name.
### Question 4: Open Comments
### Question 5: Open Comments
```yaml
header: "Comments"
@@ -92,7 +112,7 @@ options:
description: "Disable comments by default"
```
### Question 5: Fans-Only Comments
### Question 6: Fans-Only Comments
```yaml
header: "Fans only"
@@ -104,7 +124,7 @@ options:
description: "Only followers can comment"
```
### Question 6: Save Location
### Question 7: Save Location
```yaml
header: "Save"
@@ -133,7 +153,8 @@ options:
## EXTEND.md Template
```md
default_theme: [default/grace/simple]
default_theme: [default/grace/simple/modern]
default_color: [preset name, hex, or empty for theme default]
default_publish_method: [api/browser]
default_author: [author name or empty]
need_open_comment: [1/0]
@@ -673,8 +673,8 @@ Options:
--content <text> Article content (use with --image)
--html <path> HTML file to paste (alternative to --content)
--markdown <path> Markdown file to convert and post (recommended)
--theme <name> Theme for markdown (default, grace, simple)
--author <name> Author name (default: 宝玉)
--theme <name> Theme for markdown (default, grace, simple, modern)
--author <name> Author name
--summary <text> Article summary
--image <path> Content image, can repeat (only with --content)
--submit Save as draft