mirror of
https://github.com/JimLiu/baoyu-skills.git
synced 2026-07-16 15:39:47 +08:00
Compare commits
18 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 484b00109f | |||
| ac217d5402 | |||
| 70d9f63727 | |||
| 3398509d9e | |||
| a11613c11b | |||
| cb17cb9cca | |||
| 88d6e09472 | |||
| 004236682d | |||
| 7e07c1bb84 | |||
| c151f33775 | |||
| 32003da694 | |||
| 6f38724163 | |||
| 374a6b28fd | |||
| 95970480c8 | |||
| fc324319d8 | |||
| 056fa06c3b | |||
| c62cda4424 | |||
| 12b43e166d |
@@ -6,7 +6,7 @@
|
|||||||
},
|
},
|
||||||
"metadata": {
|
"metadata": {
|
||||||
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
"description": "Skills shared by Baoyu for improving daily work efficiency",
|
||||||
"version": "1.63.0"
|
"version": "1.66.0"
|
||||||
},
|
},
|
||||||
"plugins": [
|
"plugins": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
name: Test
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
node-tests:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v4
|
||||||
|
with:
|
||||||
|
node-version: 22
|
||||||
|
|
||||||
|
- name: Run tests
|
||||||
|
run: npm test
|
||||||
@@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
English | [中文](./CHANGELOG.zh.md)
|
English | [中文](./CHANGELOG.zh.md)
|
||||||
|
|
||||||
|
## 1.66.0 - 2026-03-13
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add Jimeng (即梦) and Seedream (豆包) image generation providers (by @lindaifeng)
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-image-gen`: tighten Jimeng provider behavior
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- `baoyu-image-gen`: export functions for testability and add module entry guard
|
||||||
|
|
||||||
|
### Documentation
|
||||||
|
- `baoyu-image-gen`: add Jimeng and Seedream provider documentation to SKILL.md and READMEs
|
||||||
|
|
||||||
|
### Tests
|
||||||
|
- Add test infrastructure with CI workflow and image-gen unit tests
|
||||||
|
|
||||||
|
## 1.65.1 - 2026-03-13
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- `baoyu-translate`: replace remark/unified with markdown-it for chunk parsing, add main.ts CLI entry point
|
||||||
|
|
||||||
|
## 1.65.0 - 2026-03-13
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-post-to-wechat`: add placeholder image upload support with deduplication for markdown-embedded images
|
||||||
|
|
||||||
|
### Fixes
|
||||||
|
- `baoyu-post-to-wechat`: fix frontmatter parsing to allow leading whitespace and optional trailing newline
|
||||||
|
|
||||||
|
### Refactor
|
||||||
|
- `baoyu-post-to-wechat`: replace `renderMarkdownToHtml` with `renderMarkdownWithPlaceholders` for structured output
|
||||||
|
|
||||||
|
## 1.64.0 - 2026-03-13
|
||||||
|
|
||||||
|
### Features
|
||||||
|
- `baoyu-image-gen`: add OpenRouter provider with support for image generation, reference images, and configurable models
|
||||||
|
|
||||||
## 1.63.0 - 2026-03-13
|
## 1.63.0 - 2026-03-13
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|||||||
@@ -2,6 +2,44 @@
|
|||||||
|
|
||||||
[English](./CHANGELOG.md) | 中文
|
[English](./CHANGELOG.md) | 中文
|
||||||
|
|
||||||
|
## 1.66.0 - 2026-03-13
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增即梦(Jimeng)和豆包(Seedream)图像生成服务商 (by @lindaifeng)
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-image-gen`:收紧即梦服务商行为
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-image-gen`:导出函数以支持测试,新增模块入口守卫
|
||||||
|
|
||||||
|
### 文档
|
||||||
|
- `baoyu-image-gen`:在 SKILL.md 和 README 中添加即梦和豆包服务商文档
|
||||||
|
|
||||||
|
### 测试
|
||||||
|
- 新增测试基础设施,包含 CI 工作流和 image-gen 单元测试
|
||||||
|
|
||||||
|
## 1.65.1 - 2026-03-13
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-translate`:将 chunk 解析从 remark/unified 替换为 markdown-it,新增 main.ts CLI 入口
|
||||||
|
|
||||||
|
## 1.65.0 - 2026-03-13
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-post-to-wechat`:新增占位符图片上传支持,自动去重 Markdown 内嵌图片
|
||||||
|
|
||||||
|
### 修复
|
||||||
|
- `baoyu-post-to-wechat`:修复 frontmatter 解析,允许前导空白和可选的尾随换行
|
||||||
|
|
||||||
|
### 重构
|
||||||
|
- `baoyu-post-to-wechat`:将 `renderMarkdownToHtml` 重构为 `renderMarkdownWithPlaceholders`,输出结构化结果
|
||||||
|
|
||||||
|
## 1.64.0 - 2026-03-13
|
||||||
|
|
||||||
|
### 新功能
|
||||||
|
- `baoyu-image-gen`:新增 OpenRouter 服务商,支持图像生成、参考图和可配置模型
|
||||||
|
|
||||||
## 1.63.0 - 2026-03-13
|
## 1.63.0 - 2026-03-13
|
||||||
|
|
||||||
### 新功能
|
### 新功能
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# CLAUDE.md
|
# CLAUDE.md
|
||||||
|
|
||||||
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.63.0**.
|
Claude Code marketplace plugin providing AI-powered content generation skills. Version: **1.66.0**.
|
||||||
|
|
||||||
## Architecture
|
## Architecture
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ Execute: `${BUN_X} skills/<skill>/scripts/main.ts [options]`
|
|||||||
|
|
||||||
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
|
- **Bun**: TypeScript runtime (`bun` preferred, fallback `npx -y bun`)
|
||||||
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
|
- **Chrome**: Required for CDP-based skills (gemini-web, post-to-x/wechat/weibo, url-to-markdown). All CDP skills share a single profile, override via `BAOYU_CHROME_PROFILE_DIR` env var. Platform paths: [docs/chrome-profile.md](docs/chrome-profile.md)
|
||||||
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Google, DashScope, or Replicate) configured in EXTEND.md
|
- **Image generation APIs**: `baoyu-image-gen` requires API key (OpenAI, Google, OpenRouter, DashScope, or Replicate) configured in EXTEND.md
|
||||||
- **Gemini Web auth**: Browser cookies (first run opens Chrome for login, `--login` to refresh)
|
- **Gemini Web auth**: Browser cookies (first run opens Chrome for login, `--login` to refresh)
|
||||||
|
|
||||||
## Security
|
## Security
|
||||||
|
|||||||
@@ -665,7 +665,7 @@ AI-powered generation backends.
|
|||||||
|
|
||||||
#### baoyu-image-gen
|
#### baoyu-image-gen
|
||||||
|
|
||||||
AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyun Tongyi Wanxiang) APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
|
AI SDK-based image generation using OpenAI, Google, OpenRouter, DashScope (Aliyun Tongyi Wanxiang), Jimeng (即梦), Seedream (豆包), and Replicate APIs. Supports text-to-image, reference images, aspect ratios, and quality presets.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# Basic generation (auto-detect provider)
|
# Basic generation (auto-detect provider)
|
||||||
@@ -680,10 +680,22 @@ AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyu
|
|||||||
# Specific provider
|
# Specific provider
|
||||||
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
|
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openai
|
||||||
|
|
||||||
|
# OpenRouter
|
||||||
|
/baoyu-image-gen --prompt "A cat" --image cat.png --provider openrouter
|
||||||
|
|
||||||
# DashScope (Aliyun Tongyi Wanxiang)
|
# DashScope (Aliyun Tongyi Wanxiang)
|
||||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||||
|
|
||||||
# With reference images (Google multimodal only)
|
# Replicate
|
||||||
|
/baoyu-image-gen --prompt "A cat" --image cat.png --provider replicate
|
||||||
|
|
||||||
|
# Jimeng (即梦)
|
||||||
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||||
|
|
||||||
|
# Seedream (豆包)
|
||||||
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||||
|
|
||||||
|
# With reference images (Google, OpenAI, OpenRouter, or Replicate)
|
||||||
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
/baoyu-image-gen --prompt "Make it blue" --image out.png --ref source.png
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -693,25 +705,39 @@ AI SDK-based image generation using official OpenAI, Google and DashScope (Aliyu
|
|||||||
| `--prompt`, `-p` | Prompt text |
|
| `--prompt`, `-p` | Prompt text |
|
||||||
| `--promptfiles` | Read prompt from files (concatenated) |
|
| `--promptfiles` | Read prompt from files (concatenated) |
|
||||||
| `--image` | Output image path (required) |
|
| `--image` | Output image path (required) |
|
||||||
| `--provider` | `google`, `openai` or `dashscope` (default: google) |
|
| `--provider` | `google`, `openai`, `openrouter`, `dashscope`, `jimeng`, `seedream` or `replicate` (default: auto-detect; prefers google) |
|
||||||
| `--model`, `-m` | Model ID |
|
| `--model`, `-m` | Model ID |
|
||||||
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
| `--ar` | Aspect ratio (e.g., `16:9`, `1:1`, `4:3`) |
|
||||||
| `--size` | Size (e.g., `1024x1024`) |
|
| `--size` | Size (e.g., `1024x1024`) |
|
||||||
| `--quality` | `normal` or `2k` (default: normal) |
|
| `--quality` | `normal` or `2k` (default: `2k`) |
|
||||||
| `--ref` | Reference images (Google multimodal only) |
|
| `--ref` | Reference images (Google, OpenAI, OpenRouter or Replicate) |
|
||||||
|
|
||||||
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
|
**Environment Variables** (see [Environment Configuration](#environment-configuration) for setup):
|
||||||
| Variable | Description | Default |
|
| Variable | Description | Default |
|
||||||
|----------|-------------|---------|
|
|----------|-------------|---------|
|
||||||
| `OPENAI_API_KEY` | OpenAI API key | - |
|
| `OPENAI_API_KEY` | OpenAI API key | - |
|
||||||
|
| `OPENROUTER_API_KEY` | OpenRouter API key | - |
|
||||||
| `GOOGLE_API_KEY` | Google API key | - |
|
| `GOOGLE_API_KEY` | Google API key | - |
|
||||||
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
|
| `DASHSCOPE_API_KEY` | DashScope API key (Aliyun) | - |
|
||||||
|
| `REPLICATE_API_TOKEN` | Replicate API token | - |
|
||||||
|
| `JIMENG_ACCESS_KEY_ID` | Jimeng Volcengine access key | - |
|
||||||
|
| `JIMENG_SECRET_ACCESS_KEY` | Jimeng Volcengine secret key | - |
|
||||||
|
| `ARK_API_KEY` | Seedream Volcengine ARK API key | - |
|
||||||
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
|
| `OPENAI_IMAGE_MODEL` | OpenAI model | `gpt-image-1.5` |
|
||||||
|
| `OPENROUTER_IMAGE_MODEL` | OpenRouter model | `google/gemini-3.1-flash-image-preview` |
|
||||||
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
|
| `GOOGLE_IMAGE_MODEL` | Google model | `gemini-3-pro-image-preview` |
|
||||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `z-image-turbo` |
|
| `DASHSCOPE_IMAGE_MODEL` | DashScope model | `z-image-turbo` |
|
||||||
|
| `REPLICATE_IMAGE_MODEL` | Replicate model | `google/nano-banana-pro` |
|
||||||
|
| `JIMENG_IMAGE_MODEL` | Jimeng model | `jimeng_t2i_v40` |
|
||||||
|
| `SEEDREAM_IMAGE_MODEL` | Seedream model | `doubao-seedream-5-0-260128` |
|
||||||
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
|
| `OPENAI_BASE_URL` | Custom OpenAI endpoint | - |
|
||||||
|
| `OPENROUTER_BASE_URL` | Custom OpenRouter endpoint | `https://openrouter.ai/api/v1` |
|
||||||
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
|
| `GOOGLE_BASE_URL` | Custom Google endpoint | - |
|
||||||
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
|
| `DASHSCOPE_BASE_URL` | Custom DashScope endpoint | - |
|
||||||
|
| `REPLICATE_BASE_URL` | Custom Replicate endpoint | - |
|
||||||
|
| `JIMENG_BASE_URL` | Custom Jimeng endpoint | `https://visual.volcengineapi.com` |
|
||||||
|
| `JIMENG_REGION` | Jimeng region | `cn-north-1` |
|
||||||
|
| `SEEDREAM_BASE_URL` | Custom Seedream endpoint | `https://ark.cn-beijing.volces.com/api/v3` |
|
||||||
|
|
||||||
**Provider Auto-Selection**:
|
**Provider Auto-Selection**:
|
||||||
1. If `--provider` specified → use it
|
1. If `--provider` specified → use it
|
||||||
@@ -958,6 +984,11 @@ OPENAI_API_KEY=sk-xxx
|
|||||||
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
||||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
|
|
||||||
|
# OpenRouter
|
||||||
|
OPENROUTER_API_KEY=sk-or-xxx
|
||||||
|
OPENROUTER_IMAGE_MODEL=google/gemini-3.1-flash-image-preview
|
||||||
|
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
|
||||||
# Google
|
# Google
|
||||||
GOOGLE_API_KEY=xxx
|
GOOGLE_API_KEY=xxx
|
||||||
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
||||||
@@ -967,6 +998,23 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
|||||||
DASHSCOPE_API_KEY=sk-xxx
|
DASHSCOPE_API_KEY=sk-xxx
|
||||||
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
||||||
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||||
|
|
||||||
|
# Replicate
|
||||||
|
REPLICATE_API_TOKEN=r8_xxx
|
||||||
|
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
|
||||||
|
# REPLICATE_BASE_URL=https://api.replicate.com
|
||||||
|
|
||||||
|
# Jimeng (即梦)
|
||||||
|
JIMENG_ACCESS_KEY_ID=xxx
|
||||||
|
JIMENG_SECRET_ACCESS_KEY=xxx
|
||||||
|
JIMENG_IMAGE_MODEL=jimeng_t2i_v40
|
||||||
|
# JIMENG_BASE_URL=https://visual.volcengineapi.com
|
||||||
|
# JIMENG_REGION=cn-north-1
|
||||||
|
|
||||||
|
# Seedream (豆包)
|
||||||
|
ARK_API_KEY=xxx
|
||||||
|
SEEDREAM_IMAGE_MODEL=doubao-seedream-5-0-260128
|
||||||
|
# SEEDREAM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+53
-5
@@ -665,7 +665,7 @@ AI 驱动的生成后端。
|
|||||||
|
|
||||||
#### baoyu-image-gen
|
#### baoyu-image-gen
|
||||||
|
|
||||||
基于 AI SDK 的图像生成,使用官方 OpenAI、Google 和 DashScope(阿里通义万相)API。支持文生图、参考图、宽高比和质量预设。
|
基于 AI SDK 的图像生成,支持 OpenAI、Google、OpenRouter、DashScope(阿里通义万相)、即梦(Jimeng)、豆包(Seedream)和 Replicate API。支持文生图、参考图、宽高比和质量预设。
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 基础生成(自动检测服务商)
|
# 基础生成(自动检测服务商)
|
||||||
@@ -680,10 +680,22 @@ AI 驱动的生成后端。
|
|||||||
# 指定服务商
|
# 指定服务商
|
||||||
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
|
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openai
|
||||||
|
|
||||||
|
# OpenRouter
|
||||||
|
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider openrouter
|
||||||
|
|
||||||
# DashScope(阿里通义万相)
|
# DashScope(阿里通义万相)
|
||||||
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider dashscope
|
||||||
|
|
||||||
# 带参考图(仅 Google 多模态支持)
|
# Replicate
|
||||||
|
/baoyu-image-gen --prompt "一只猫" --image cat.png --provider replicate
|
||||||
|
|
||||||
|
# 即梦(Jimeng)
|
||||||
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider jimeng
|
||||||
|
|
||||||
|
# 豆包(Seedream)
|
||||||
|
/baoyu-image-gen --prompt "一只可爱的猫" --image cat.png --provider seedream
|
||||||
|
|
||||||
|
# 带参考图(Google、OpenAI、OpenRouter 或 Replicate)
|
||||||
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
/baoyu-image-gen --prompt "把它变成蓝色" --image out.png --ref source.png
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -693,25 +705,39 @@ AI 驱动的生成后端。
|
|||||||
| `--prompt`, `-p` | 提示词文本 |
|
| `--prompt`, `-p` | 提示词文本 |
|
||||||
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
|
| `--promptfiles` | 从文件读取提示词(多文件拼接) |
|
||||||
| `--image` | 输出图片路径(必需) |
|
| `--image` | 输出图片路径(必需) |
|
||||||
| `--provider` | `google`、`openai` 或 `dashscope`(默认:google) |
|
| `--provider` | `google`、`openai`、`openrouter`、`dashscope`、`jimeng`、`seedream` 或 `replicate`(默认:自动检测,优先 google) |
|
||||||
| `--model`, `-m` | 模型 ID |
|
| `--model`, `-m` | 模型 ID |
|
||||||
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
| `--ar` | 宽高比(如 `16:9`、`1:1`、`4:3`) |
|
||||||
| `--size` | 尺寸(如 `1024x1024`) |
|
| `--size` | 尺寸(如 `1024x1024`) |
|
||||||
| `--quality` | `normal` 或 `2k`(默认:normal) |
|
| `--quality` | `normal` 或 `2k`(默认:`2k`) |
|
||||||
| `--ref` | 参考图片(仅 Google 多模态支持) |
|
| `--ref` | 参考图片(Google、OpenAI、OpenRouter 或 Replicate) |
|
||||||
|
|
||||||
**环境变量**(配置方法见[环境配置](#环境配置)):
|
**环境变量**(配置方法见[环境配置](#环境配置)):
|
||||||
| 变量 | 说明 | 默认值 |
|
| 变量 | 说明 | 默认值 |
|
||||||
|------|------|--------|
|
|------|------|--------|
|
||||||
| `OPENAI_API_KEY` | OpenAI API 密钥 | - |
|
| `OPENAI_API_KEY` | OpenAI API 密钥 | - |
|
||||||
|
| `OPENROUTER_API_KEY` | OpenRouter API 密钥 | - |
|
||||||
| `GOOGLE_API_KEY` | Google API 密钥 | - |
|
| `GOOGLE_API_KEY` | Google API 密钥 | - |
|
||||||
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
|
| `DASHSCOPE_API_KEY` | DashScope API 密钥(阿里云) | - |
|
||||||
|
| `REPLICATE_API_TOKEN` | Replicate API Token | - |
|
||||||
|
| `JIMENG_ACCESS_KEY_ID` | 即梦火山引擎 Access Key | - |
|
||||||
|
| `JIMENG_SECRET_ACCESS_KEY` | 即梦火山引擎 Secret Key | - |
|
||||||
|
| `ARK_API_KEY` | 豆包火山引擎 ARK API 密钥 | - |
|
||||||
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
|
| `OPENAI_IMAGE_MODEL` | OpenAI 模型 | `gpt-image-1.5` |
|
||||||
|
| `OPENROUTER_IMAGE_MODEL` | OpenRouter 模型 | `google/gemini-3.1-flash-image-preview` |
|
||||||
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
|
| `GOOGLE_IMAGE_MODEL` | Google 模型 | `gemini-3-pro-image-preview` |
|
||||||
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `z-image-turbo` |
|
| `DASHSCOPE_IMAGE_MODEL` | DashScope 模型 | `z-image-turbo` |
|
||||||
|
| `REPLICATE_IMAGE_MODEL` | Replicate 模型 | `google/nano-banana-pro` |
|
||||||
|
| `JIMENG_IMAGE_MODEL` | 即梦模型 | `jimeng_t2i_v40` |
|
||||||
|
| `SEEDREAM_IMAGE_MODEL` | 豆包模型 | `doubao-seedream-5-0-260128` |
|
||||||
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
|
| `OPENAI_BASE_URL` | 自定义 OpenAI 端点 | - |
|
||||||
|
| `OPENROUTER_BASE_URL` | 自定义 OpenRouter 端点 | `https://openrouter.ai/api/v1` |
|
||||||
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
|
| `GOOGLE_BASE_URL` | 自定义 Google 端点 | - |
|
||||||
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
|
| `DASHSCOPE_BASE_URL` | 自定义 DashScope 端点 | - |
|
||||||
|
| `REPLICATE_BASE_URL` | 自定义 Replicate 端点 | - |
|
||||||
|
| `JIMENG_BASE_URL` | 自定义即梦端点 | `https://visual.volcengineapi.com` |
|
||||||
|
| `JIMENG_REGION` | 即梦区域 | `cn-north-1` |
|
||||||
|
| `SEEDREAM_BASE_URL` | 自定义豆包端点 | `https://ark.cn-beijing.volces.com/api/v3` |
|
||||||
|
|
||||||
**服务商自动选择**:
|
**服务商自动选择**:
|
||||||
1. 如果指定了 `--provider` → 使用指定的
|
1. 如果指定了 `--provider` → 使用指定的
|
||||||
@@ -958,6 +984,11 @@ OPENAI_API_KEY=sk-xxx
|
|||||||
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
OPENAI_IMAGE_MODEL=gpt-image-1.5
|
||||||
# OPENAI_BASE_URL=https://api.openai.com/v1
|
# OPENAI_BASE_URL=https://api.openai.com/v1
|
||||||
|
|
||||||
|
# OpenRouter
|
||||||
|
OPENROUTER_API_KEY=sk-or-xxx
|
||||||
|
OPENROUTER_IMAGE_MODEL=google/gemini-3.1-flash-image-preview
|
||||||
|
# OPENROUTER_BASE_URL=https://openrouter.ai/api/v1
|
||||||
|
|
||||||
# Google
|
# Google
|
||||||
GOOGLE_API_KEY=xxx
|
GOOGLE_API_KEY=xxx
|
||||||
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
||||||
@@ -967,6 +998,23 @@ GOOGLE_IMAGE_MODEL=gemini-3-pro-image-preview
|
|||||||
DASHSCOPE_API_KEY=sk-xxx
|
DASHSCOPE_API_KEY=sk-xxx
|
||||||
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
DASHSCOPE_IMAGE_MODEL=z-image-turbo
|
||||||
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
# DASHSCOPE_BASE_URL=https://dashscope.aliyuncs.com/api/v1
|
||||||
|
|
||||||
|
# Replicate
|
||||||
|
REPLICATE_API_TOKEN=r8_xxx
|
||||||
|
REPLICATE_IMAGE_MODEL=google/nano-banana-pro
|
||||||
|
# REPLICATE_BASE_URL=https://api.replicate.com
|
||||||
|
|
||||||
|
# 即梦(Jimeng)
|
||||||
|
JIMENG_ACCESS_KEY_ID=xxx
|
||||||
|
JIMENG_SECRET_ACCESS_KEY=xxx
|
||||||
|
JIMENG_IMAGE_MODEL=jimeng_t2i_v40
|
||||||
|
# JIMENG_BASE_URL=https://visual.volcengineapi.com
|
||||||
|
# JIMENG_REGION=cn-north-1
|
||||||
|
|
||||||
|
# 豆包(Seedream)
|
||||||
|
ARK_API_KEY=xxx
|
||||||
|
SEEDREAM_IMAGE_MODEL=doubao-seedream-5-0-260128
|
||||||
|
# SEEDREAM_BASE_URL=https://ark.cn-beijing.volces.com/api/v3
|
||||||
EOF
|
EOF
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -27,7 +27,9 @@ Release hooks are configured via `.releaserc.yml`. This repo does not stage a se
|
|||||||
|
|
||||||
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
`packages/` is the **only** source of truth. Never edit `skills/*/scripts/vendor/` directly.
|
||||||
|
|
||||||
Current package: `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`).
|
Current packages:
|
||||||
|
- `baoyu-chrome-cdp` (Chrome CDP utilities), consumed by 6 skills (`baoyu-danger-gemini-web`, `baoyu-danger-x-to-markdown`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`, `baoyu-post-to-x`, `baoyu-url-to-markdown`)
|
||||||
|
- `baoyu-md` (shared Markdown rendering and placeholder pipeline), consumed by 3 skills (`baoyu-markdown-to-html`, `baoyu-post-to-wechat`, `baoyu-post-to-weibo`)
|
||||||
|
|
||||||
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
**How it works**: Sync script copies packages into each consuming skill's `vendor/` directory and rewrites dependency specs to `file:./vendor/<name>`. Vendor copies are committed to git, making skills self-contained.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
# Testing Strategy
|
||||||
|
|
||||||
|
This repository has many scripts, but they do not share a single runtime or dependency graph. The lowest-risk testing strategy is to start from stable Node-based library code, then expand outward to CLI and skill-specific smoke tests.
|
||||||
|
|
||||||
|
## Current Baseline
|
||||||
|
|
||||||
|
- Root test runner: `node:test`
|
||||||
|
- Entry point: `npm test`
|
||||||
|
- Coverage command: `npm run test:coverage`
|
||||||
|
- CI trigger: GitHub Actions on `push`, `pull_request`, and manual dispatch
|
||||||
|
|
||||||
|
This avoids introducing Jest/Vitest across a repo that already mixes plain Node scripts, Bun-based skill packages, vendored code, and browser automation.
|
||||||
|
|
||||||
|
## Rollout Plan
|
||||||
|
|
||||||
|
### Phase 1: Stable library coverage
|
||||||
|
|
||||||
|
Focus on pure functions under `scripts/lib/` first.
|
||||||
|
|
||||||
|
- `scripts/lib/release-files.mjs`
|
||||||
|
- `scripts/lib/shared-skill-packages.mjs`
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
|
||||||
|
- Validate file filtering and release packaging rules
|
||||||
|
- Catch regressions in package vendoring and dependency rewriting
|
||||||
|
- Keep tests deterministic and free of network, Bun, or browser requirements
|
||||||
|
|
||||||
|
### Phase 2: Root CLI integration tests
|
||||||
|
|
||||||
|
Add temp-directory integration tests for root CLIs that already support dry-run or local-only flows.
|
||||||
|
|
||||||
|
- `scripts/sync-shared-skill-packages.mjs`
|
||||||
|
- `scripts/publish-skill.mjs --dry-run`
|
||||||
|
- `scripts/sync-clawhub.mjs` argument handling and local skill discovery
|
||||||
|
|
||||||
|
Goals:
|
||||||
|
|
||||||
|
- Assert exit codes and stdout for common flows
|
||||||
|
- Cover CLI argument parsing without hitting external services
|
||||||
|
|
||||||
|
### Phase 3: Skill script smoke tests
|
||||||
|
|
||||||
|
Add opt-in smoke tests for selected `skills/*/scripts/` packages, starting with those that:
|
||||||
|
|
||||||
|
- accept local input files
|
||||||
|
- have deterministic output
|
||||||
|
- do not require authenticated browser sessions
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
|
||||||
|
- markdown transforms
|
||||||
|
- file conversion helpers
|
||||||
|
- local content analyzers
|
||||||
|
|
||||||
|
Keep browser automation, login flows, and live API publishing scripts outside the default CI path unless they are explicitly mocked.
|
||||||
|
|
||||||
|
### Phase 4: Coverage gates
|
||||||
|
|
||||||
|
After the stable Node path has enough breadth, add coverage thresholds in CI for the tested root modules.
|
||||||
|
|
||||||
|
Recommended order:
|
||||||
|
|
||||||
|
1. Start with reporting only
|
||||||
|
2. Add line/function thresholds for `scripts/lib/**`
|
||||||
|
3. Expand include patterns once skill-level smoke tests are reliable
|
||||||
|
|
||||||
|
## Conventions For New Tests
|
||||||
|
|
||||||
|
- Prefer temp directories over committed fixtures unless the fixture is reused heavily
|
||||||
|
- Test exported functions before testing CLI wrappers
|
||||||
|
- Avoid network, browser, and credential dependencies in default CI
|
||||||
|
- Keep tests isolated so they can run with plain `node --test`
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"name": "baoyu-skills",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"test": "node --test",
|
||||||
|
"test:coverage": "node --experimental-test-coverage --test"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,11 @@
|
|||||||
{
|
{
|
||||||
|
"name": "baoyu-md",
|
||||||
|
"private": true,
|
||||||
|
"version": "0.1.0",
|
||||||
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"fflate": "^0.8.2",
|
"fflate": "^0.8.2",
|
||||||
"front-matter": "^4.0.2",
|
"front-matter": "^4.0.2",
|
||||||
@@ -12,7 +12,7 @@ export function printUsage(): void {
|
|||||||
console.error(
|
console.error(
|
||||||
[
|
[
|
||||||
"Usage:",
|
"Usage:",
|
||||||
" npx tsx src/md/render.ts <markdown_file> [options]",
|
" npx tsx render.ts <markdown_file> [options]",
|
||||||
"",
|
"",
|
||||||
"Options:",
|
"Options:",
|
||||||
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
|
` --theme <name> Theme (${THEME_NAMES.join(", ")})`,
|
||||||
@@ -0,0 +1,105 @@
|
|||||||
|
import { Lexer } from "marked";
|
||||||
|
|
||||||
|
export type FrontmatterFields = Record<string, string>;
|
||||||
|
|
||||||
|
export function parseFrontmatter(content: string): {
|
||||||
|
frontmatter: FrontmatterFields;
|
||||||
|
body: string;
|
||||||
|
} {
|
||||||
|
const match = content.match(/^\s*---\r?\n([\s\S]*?)\r?\n---\r?\n?([\s\S]*)$/);
|
||||||
|
if (!match) {
|
||||||
|
return { frontmatter: {}, body: content };
|
||||||
|
}
|
||||||
|
|
||||||
|
const frontmatter: FrontmatterFields = {};
|
||||||
|
const lines = match[1]!.split("\n");
|
||||||
|
for (const line of lines) {
|
||||||
|
const colonIdx = line.indexOf(":");
|
||||||
|
if (colonIdx <= 0) continue;
|
||||||
|
|
||||||
|
const key = line.slice(0, colonIdx).trim();
|
||||||
|
const value = line.slice(colonIdx + 1).trim();
|
||||||
|
frontmatter[key] = stripWrappingQuotes(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { frontmatter, body: match[2]! };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serializeFrontmatter(frontmatter: FrontmatterFields): string {
|
||||||
|
const entries = Object.entries(frontmatter);
|
||||||
|
if (entries.length === 0) return "";
|
||||||
|
return `---\n${entries.map(([key, value]) => `${key}: ${value}`).join("\n")}\n---\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function stripWrappingQuotes(value: string): string {
|
||||||
|
if (!value) return value;
|
||||||
|
|
||||||
|
const doubleQuoted = value.startsWith('"') && value.endsWith('"');
|
||||||
|
const singleQuoted = value.startsWith("'") && value.endsWith("'");
|
||||||
|
const cjkDoubleQuoted = value.startsWith("\u201c") && value.endsWith("\u201d");
|
||||||
|
const cjkSingleQuoted = value.startsWith("\u2018") && value.endsWith("\u2019");
|
||||||
|
|
||||||
|
if (doubleQuoted || singleQuoted || cjkDoubleQuoted || cjkSingleQuoted) {
|
||||||
|
return value.slice(1, -1).trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
return value.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
export function toFrontmatterString(value: unknown): string | undefined {
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return stripWrappingQuotes(value);
|
||||||
|
}
|
||||||
|
if (typeof value === "number" || typeof value === "boolean") {
|
||||||
|
return String(value);
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function pickFirstString(
|
||||||
|
frontmatter: Record<string, unknown>,
|
||||||
|
keys: string[],
|
||||||
|
): string | undefined {
|
||||||
|
for (const key of keys) {
|
||||||
|
const value = toFrontmatterString(frontmatter[key]);
|
||||||
|
if (value) return value;
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractTitleFromMarkdown(markdown: string): string {
|
||||||
|
const tokens = Lexer.lex(markdown, { gfm: true, breaks: true });
|
||||||
|
for (const token of tokens) {
|
||||||
|
if (token.type !== "heading" || (token.depth !== 1 && token.depth !== 2)) continue;
|
||||||
|
return stripWrappingQuotes(token.text);
|
||||||
|
}
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractSummaryFromBody(body: string, maxLen: number): string {
|
||||||
|
const lines = body.split("\n");
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) continue;
|
||||||
|
if (trimmed.startsWith("#")) continue;
|
||||||
|
if (trimmed.startsWith("![")) continue;
|
||||||
|
if (trimmed.startsWith(">")) continue;
|
||||||
|
if (trimmed.startsWith("-") || trimmed.startsWith("*")) continue;
|
||||||
|
if (/^\d+\./.test(trimmed)) continue;
|
||||||
|
if (trimmed.startsWith("```")) continue;
|
||||||
|
|
||||||
|
const cleanText = trimmed
|
||||||
|
.replace(/\*\*(.+?)\*\*/g, "$1")
|
||||||
|
.replace(/\*(.+?)\*/g, "$1")
|
||||||
|
.replace(/\[([^\]]+)\]\([^)]+\)/g, "$1")
|
||||||
|
.replace(/`([^`]+)`/g, "$1");
|
||||||
|
|
||||||
|
if (cleanText.length > 20) {
|
||||||
|
if (cleanText.length <= maxLen) return cleanText;
|
||||||
|
return `${cleanText.slice(0, maxLen - 3)}...`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "";
|
||||||
|
}
|
||||||
@@ -0,0 +1,206 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
|
||||||
|
import type { ReadTimeResults } from "reading-time";
|
||||||
|
|
||||||
|
import {
|
||||||
|
COLOR_PRESETS,
|
||||||
|
DEFAULT_STYLE,
|
||||||
|
FONT_FAMILY_MAP,
|
||||||
|
THEME_STYLE_DEFAULTS,
|
||||||
|
} from "./constants.js";
|
||||||
|
import {
|
||||||
|
extractSummaryFromBody,
|
||||||
|
extractTitleFromMarkdown,
|
||||||
|
pickFirstString,
|
||||||
|
stripWrappingQuotes,
|
||||||
|
} from "./content.js";
|
||||||
|
import { loadExtendConfig } from "./extend-config.js";
|
||||||
|
import {
|
||||||
|
buildCss,
|
||||||
|
buildHtmlDocument,
|
||||||
|
inlineCss,
|
||||||
|
loadCodeThemeCss,
|
||||||
|
modifyHtmlStructure,
|
||||||
|
normalizeInlineCss,
|
||||||
|
removeFirstHeading,
|
||||||
|
} from "./html-builder.js";
|
||||||
|
import { initRenderer, postProcessHtml, renderMarkdown } from "./renderer.js";
|
||||||
|
import { loadThemeCss, normalizeThemeCss } from "./themes.js";
|
||||||
|
import type { HtmlDocumentMeta, IOpts, StyleConfig, ThemeName } from "./types.js";
|
||||||
|
|
||||||
|
export interface RenderMarkdownDocumentOptions {
|
||||||
|
codeTheme?: string;
|
||||||
|
countStatus?: boolean;
|
||||||
|
citeStatus?: boolean;
|
||||||
|
defaultTitle?: string;
|
||||||
|
fontFamily?: string;
|
||||||
|
fontSize?: string;
|
||||||
|
isMacCodeBlock?: boolean;
|
||||||
|
isShowLineNumber?: boolean;
|
||||||
|
keepTitle?: boolean;
|
||||||
|
legend?: string;
|
||||||
|
primaryColor?: string;
|
||||||
|
theme?: ThemeName;
|
||||||
|
themeMode?: IOpts["themeMode"];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface RenderMarkdownDocumentResult {
|
||||||
|
contentHtml: string;
|
||||||
|
html: string;
|
||||||
|
meta: HtmlDocumentMeta;
|
||||||
|
readingTime: ReadTimeResults;
|
||||||
|
style: StyleConfig;
|
||||||
|
yamlData: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveColorToken(value?: string): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return COLOR_PRESETS[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveFontFamilyToken(value?: string): string | undefined {
|
||||||
|
if (!value) return undefined;
|
||||||
|
return FONT_FAMILY_MAP[value] ?? value;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function formatTimestamp(date = new Date()): string {
|
||||||
|
const pad = (value: number) => String(value).padStart(2, "0");
|
||||||
|
return `${date.getFullYear()}${pad(date.getMonth() + 1)}${pad(
|
||||||
|
date.getDate(),
|
||||||
|
)}${pad(date.getHours())}${pad(date.getMinutes())}${pad(date.getSeconds())}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function buildMarkdownDocumentMeta(
|
||||||
|
markdown: string,
|
||||||
|
yamlData: Record<string, unknown>,
|
||||||
|
defaultTitle = "document",
|
||||||
|
): HtmlDocumentMeta {
|
||||||
|
const title = pickFirstString(yamlData, ["title"])
|
||||||
|
|| extractTitleFromMarkdown(markdown)
|
||||||
|
|| defaultTitle;
|
||||||
|
const author = pickFirstString(yamlData, ["author"]);
|
||||||
|
const description = pickFirstString(yamlData, ["description", "summary"])
|
||||||
|
|| extractSummaryFromBody(markdown, 120);
|
||||||
|
|
||||||
|
return {
|
||||||
|
title: stripWrappingQuotes(title),
|
||||||
|
author: author ? stripWrappingQuotes(author) : undefined,
|
||||||
|
description: description ? stripWrappingQuotes(description) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveMarkdownStyle(options: RenderMarkdownDocumentOptions = {}): StyleConfig {
|
||||||
|
const theme = options.theme ?? "default";
|
||||||
|
const themeDefaults = THEME_STYLE_DEFAULTS[theme] ?? {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
...DEFAULT_STYLE,
|
||||||
|
...themeDefaults,
|
||||||
|
...(options.primaryColor !== undefined ? { primaryColor: options.primaryColor } : {}),
|
||||||
|
...(options.fontFamily !== undefined ? { fontFamily: options.fontFamily } : {}),
|
||||||
|
...(options.fontSize !== undefined ? { fontSize: options.fontSize } : {}),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveRenderOptions(
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): RenderMarkdownDocumentOptions {
|
||||||
|
const extendConfig = loadExtendConfig();
|
||||||
|
|
||||||
|
return {
|
||||||
|
codeTheme: options.codeTheme ?? extendConfig.default_code_theme ?? "github",
|
||||||
|
countStatus: options.countStatus ?? extendConfig.count ?? false,
|
||||||
|
citeStatus: options.citeStatus ?? extendConfig.cite ?? false,
|
||||||
|
defaultTitle: options.defaultTitle,
|
||||||
|
fontFamily: options.fontFamily ?? resolveFontFamilyToken(extendConfig.default_font_family ?? undefined),
|
||||||
|
fontSize: options.fontSize ?? extendConfig.default_font_size ?? undefined,
|
||||||
|
isMacCodeBlock: options.isMacCodeBlock ?? extendConfig.mac_code_block ?? true,
|
||||||
|
isShowLineNumber: options.isShowLineNumber ?? extendConfig.show_line_number ?? false,
|
||||||
|
keepTitle: options.keepTitle ?? extendConfig.keep_title ?? false,
|
||||||
|
legend: options.legend ?? extendConfig.legend ?? "alt",
|
||||||
|
primaryColor: options.primaryColor ?? resolveColorToken(extendConfig.default_color ?? undefined),
|
||||||
|
theme: options.theme ?? extendConfig.default_theme ?? "default",
|
||||||
|
themeMode: options.themeMode,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMarkdownDocument(
|
||||||
|
markdown: string,
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): Promise<RenderMarkdownDocumentResult> {
|
||||||
|
const resolvedOptions = resolveRenderOptions(options);
|
||||||
|
const theme = resolvedOptions.theme ?? "default";
|
||||||
|
const codeTheme = resolvedOptions.codeTheme ?? "github";
|
||||||
|
const style = resolveMarkdownStyle(resolvedOptions);
|
||||||
|
|
||||||
|
const { baseCss, themeCss } = loadThemeCss(theme);
|
||||||
|
const css = normalizeThemeCss(buildCss(baseCss, themeCss, style));
|
||||||
|
const codeThemeCss = loadCodeThemeCss(codeTheme);
|
||||||
|
|
||||||
|
const renderer = initRenderer({
|
||||||
|
citeStatus: resolvedOptions.citeStatus ?? false,
|
||||||
|
countStatus: resolvedOptions.countStatus ?? false,
|
||||||
|
isMacCodeBlock: resolvedOptions.isMacCodeBlock ?? true,
|
||||||
|
isShowLineNumber: resolvedOptions.isShowLineNumber ?? false,
|
||||||
|
legend: resolvedOptions.legend ?? "alt",
|
||||||
|
themeMode: resolvedOptions.themeMode,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { yamlData, markdownContent, readingTime } = renderer.parseFrontMatterAndContent(markdown);
|
||||||
|
const { html: baseHtml, readingTime: readingTimeResult } = renderMarkdown(markdown, renderer);
|
||||||
|
|
||||||
|
let contentHtml = postProcessHtml(baseHtml, readingTimeResult, renderer);
|
||||||
|
if (!(resolvedOptions.keepTitle ?? false)) {
|
||||||
|
contentHtml = removeFirstHeading(contentHtml);
|
||||||
|
}
|
||||||
|
|
||||||
|
const meta = buildMarkdownDocumentMeta(
|
||||||
|
markdownContent,
|
||||||
|
yamlData as Record<string, unknown>,
|
||||||
|
resolvedOptions.defaultTitle,
|
||||||
|
);
|
||||||
|
const html = buildHtmlDocument(meta, css, contentHtml, codeThemeCss);
|
||||||
|
const inlinedHtml = normalizeInlineCss(await inlineCss(html), style);
|
||||||
|
|
||||||
|
return {
|
||||||
|
contentHtml,
|
||||||
|
html: modifyHtmlStructure(inlinedHtml),
|
||||||
|
meta,
|
||||||
|
readingTime,
|
||||||
|
style,
|
||||||
|
yamlData: yamlData as Record<string, unknown>,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function renderMarkdownFileToHtml(
|
||||||
|
inputPath: string,
|
||||||
|
options: RenderMarkdownDocumentOptions = {},
|
||||||
|
): Promise<RenderMarkdownDocumentResult & {
|
||||||
|
backupPath?: string;
|
||||||
|
outputPath: string;
|
||||||
|
}> {
|
||||||
|
const markdown = fs.readFileSync(inputPath, "utf-8");
|
||||||
|
const outputPath = path.resolve(
|
||||||
|
path.dirname(inputPath),
|
||||||
|
`${path.basename(inputPath, path.extname(inputPath))}.html`,
|
||||||
|
);
|
||||||
|
const result = await renderMarkdownDocument(markdown, {
|
||||||
|
...options,
|
||||||
|
defaultTitle: options.defaultTitle ?? path.basename(outputPath, ".html"),
|
||||||
|
});
|
||||||
|
|
||||||
|
let backupPath: string | undefined;
|
||||||
|
if (fs.existsSync(outputPath)) {
|
||||||
|
backupPath = `${outputPath}.bak-${formatTimestamp()}`;
|
||||||
|
fs.renameSync(outputPath, backupPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
fs.writeFileSync(outputPath, result.html, "utf-8");
|
||||||
|
|
||||||
|
return {
|
||||||
|
...result,
|
||||||
|
backupPath,
|
||||||
|
outputPath,
|
||||||
|
};
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user