mirror of
https://github.com/comeonzhj/Auto-Redbook-Skills.git
synced 2026-03-27 12:49:27 +08:00
feat: v2.0 智能分页渲染 + 7种可选样式
新增功能: - 智能分页渲染脚本(Python/Node.js),自动解决内容溢出问题 - 7种可选样式主题:purple, xiaohongshu, mint, sunset, ocean, elegant, dark - 新增 STYLES.md 样式选择指南 - 更新 README,添加 v2.0 更新说明 - 保留 V1 旧版脚本保证兼容性 使用方式: python scripts/render_xhs_v2.py note.md --style sunset
This commit is contained in:
109
README.md
109
README.md
@@ -6,6 +6,56 @@
|
|||||||
[](https://nodejs.org/)
|
[](https://nodejs.org/)
|
||||||
[](LICENSE)
|
[](LICENSE)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 🆕 v2.0 版本更新
|
||||||
|
|
||||||
|
### ✨ 新增功能
|
||||||
|
|
||||||
|
- **🚀 智能分页渲染** - 自动检测内容高度,超出时自动拆分到多张卡片,彻底解决文字溢出问题
|
||||||
|
- **🎨 7种可选样式** - 新增多种主题风格,一键切换不同视觉效果
|
||||||
|
- **⚡ V2 渲染脚本** - 全新 `render_xhs_v2.py` / `render_xhs_v2.js`,推荐升级使用
|
||||||
|
|
||||||
|
### 📋 可用样式列表
|
||||||
|
|
||||||
|
| 样式 | 名称 | 预览 |
|
||||||
|
|------|------|------|
|
||||||
|
| `purple` | 紫韵(默认)| 蓝紫色渐变 |
|
||||||
|
| `xiaohongshu` | 小红书红 | 品牌红色系 |
|
||||||
|
| `mint` | 清新薄荷 | 绿色自然调 |
|
||||||
|
| `sunset` | 日落橙 | 粉橙浪漫调 |
|
||||||
|
| `ocean` | 深海蓝 | 海洋蓝色调 |
|
||||||
|
| `elegant` | 优雅白 | 灰白简约调 |
|
||||||
|
| `dark` | 暗黑模式 | 深色高对比 |
|
||||||
|
|
||||||
|
### 🎯 使用 V2 脚本
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 版本
|
||||||
|
python scripts/render_xhs_v2.py note.md --style sunset
|
||||||
|
|
||||||
|
# Node.js 版本
|
||||||
|
node scripts/render_xhs_v2.js note.md --style ocean
|
||||||
|
|
||||||
|
# 查看所有样式
|
||||||
|
python scripts/render_xhs_v2.py --list-styles
|
||||||
|
```
|
||||||
|
|
||||||
|
### 📁 v2.0 新增文件
|
||||||
|
|
||||||
|
```
|
||||||
|
scripts/
|
||||||
|
├── render_xhs_v2.py # 新增:Python 智能分页版(推荐)
|
||||||
|
├── render_xhs_v2.js # 新增:Node.js 智能分页版(推荐)
|
||||||
|
├── render_xhs.py # 旧版:保留兼容
|
||||||
|
└── render_xhs.js # 旧版:保留兼容
|
||||||
|
STYLES.md # 新增:样式选择指南
|
||||||
|
```
|
||||||
|
|
||||||
|
**注:V1 旧版脚本保留兼容,V2 版本完全向下兼容 Markdown 格式。**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## ✨ 功能特性
|
## ✨ 功能特性
|
||||||
|
|
||||||
- 📝 **撰写笔记** - 根据既定主题,撰写小红书笔记(提示词自己调整,在 `SKILL.md`里)
|
- 📝 **撰写笔记** - 根据既定主题,撰写小红书笔记(提示词自己调整,在 `SKILL.md`里)
|
||||||
@@ -22,7 +72,7 @@ Clone 项目到本地
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
|
||||||
git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git
|
git clone https://github.com/comeonzhj/Auto-Redbook-Skills.git
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -49,6 +99,41 @@ npm install
|
|||||||
npx playwright install chromium
|
npx playwright install chromium
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## 🎨 渲染图片
|
||||||
|
|
||||||
|
### V2 渲染(推荐)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 使用默认样式
|
||||||
|
python scripts/render_xhs_v2.py note.md
|
||||||
|
|
||||||
|
# 指定样式主题
|
||||||
|
python scripts/render_xhs_v2.py note.md --style sunset
|
||||||
|
|
||||||
|
# 指定输出目录
|
||||||
|
python scripts/render_xhs_v2.py note.md -o ./output --style xiaohongshu
|
||||||
|
|
||||||
|
# 查看所有样式
|
||||||
|
python scripts/render_xhs_v2.py --list-styles
|
||||||
|
```
|
||||||
|
|
||||||
|
**V2 特性:**
|
||||||
|
- 智能分页:自动检测内容高度,自动拆分卡片
|
||||||
|
- 固定尺寸:所有卡片固定 1080×1440px
|
||||||
|
- 多种样式:7种预设主题风格
|
||||||
|
|
||||||
|
### V1 渲染(旧版)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 版本
|
||||||
|
python scripts/render_xhs.py note.md
|
||||||
|
|
||||||
|
# Node.js 版本
|
||||||
|
node scripts/render_xhs.js note.md
|
||||||
|
```
|
||||||
|
|
||||||
|
**注意:** V1 版本当内容过长时可能出现溢出,建议手动使用 `---` 分隔内容。
|
||||||
|
|
||||||
## 📤 发布到小红书
|
## 📤 发布到小红书
|
||||||
|
|
||||||
### 1. 配置 Cookie
|
### 1. 配置 Cookie
|
||||||
@@ -93,7 +178,7 @@ python scripts/publish_xhs.py \
|
|||||||
|
|
||||||
## 🎨 自定义样式
|
## 🎨 自定义样式
|
||||||
|
|
||||||
### 修改背景渐变
|
### 修改背景渐变(V1)
|
||||||
|
|
||||||
编辑 `assets/card.html` 中的 `.card-container`:
|
编辑 `assets/card.html` 中的 `.card-container`:
|
||||||
|
|
||||||
@@ -113,9 +198,16 @@ python scripts/publish_xhs.py \
|
|||||||
| 绿色 | `#43e97b → #38f9d7` |
|
| 绿色 | `#43e97b → #38f9d7` |
|
||||||
| 橙黄 | `#fa709a → #fee140` |
|
| 橙黄 | `#fa709a → #fee140` |
|
||||||
|
|
||||||
### 修改封面样式
|
### 更多样式选择(V2)
|
||||||
|
|
||||||
编辑 `assets/cover.html` 中的样式。
|
V2 版本提供 7 种内置样式,通过 `--style` 参数快速切换:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style dark # 暗黑模式
|
||||||
|
python scripts/render_xhs_v2.py note.md --style mint # 清新薄荷
|
||||||
|
```
|
||||||
|
|
||||||
|
详见 [STYLES.md](./STYLES.md)
|
||||||
|
|
||||||
## 📁 项目结构
|
## 📁 项目结构
|
||||||
|
|
||||||
@@ -123,6 +215,7 @@ python scripts/publish_xhs.py \
|
|||||||
md2Redbook/
|
md2Redbook/
|
||||||
├── SKILL.md # 技能描述(AI Agent 使用)
|
├── SKILL.md # 技能描述(AI Agent 使用)
|
||||||
├── README.md # 项目文档
|
├── README.md # 项目文档
|
||||||
|
├── STYLES.md # 样式选择指南
|
||||||
├── requirements.txt # Python 依赖
|
├── requirements.txt # Python 依赖
|
||||||
├── package.json # Node.js 依赖
|
├── package.json # Node.js 依赖
|
||||||
├── env.example.txt # Cookie 配置示例
|
├── env.example.txt # Cookie 配置示例
|
||||||
@@ -132,8 +225,10 @@ md2Redbook/
|
|||||||
│ ├── styles.css # 共用样式表
|
│ ├── styles.css # 共用样式表
|
||||||
│ └── example.md # 示例 Markdown
|
│ └── example.md # 示例 Markdown
|
||||||
└── scripts/
|
└── scripts/
|
||||||
├── render_xhs.py # Python 渲染脚本
|
├── render_xhs_v2.py # Python 渲染脚本 V2(推荐)
|
||||||
├── render_xhs.js # Node.js 渲染脚本
|
├── render_xhs_v2.js # Node.js 渲染脚本 V2(推荐)
|
||||||
|
├── render_xhs.py # Python 渲染脚本 V1
|
||||||
|
├── render_xhs.js # Node.js 渲染脚本 V1
|
||||||
└── publish_xhs.py # 小红书发布脚本
|
└── publish_xhs.py # 小红书发布脚本
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -149,7 +244,7 @@ md2Redbook/
|
|||||||
|
|
||||||
- [Playwright](https://playwright.dev/) - 浏览器自动化渲染
|
- [Playwright](https://playwright.dev/) - 浏览器自动化渲染
|
||||||
- [Marked](https://marked.js.org/) - Markdown 解析
|
- [Marked](https://marked.js.org/) - Markdown 解析
|
||||||
- [Madopic](https://github.com/xiaolinbaba/Madopic) - Markdown 渲染
|
- [Madopic](https://github.com/xiaolinbaba/Madopic) - Markdown 渲染
|
||||||
- [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端
|
- [xhs](https://github.com/ReaJason/xhs) - 小红书 API 客户端
|
||||||
|
|
||||||
## 📄 License
|
## 📄 License
|
||||||
|
|||||||
113
SKILL.md
113
SKILL.md
@@ -1,17 +1,18 @@
|
|||||||
---
|
---
|
||||||
name: xhs-note-creator
|
name: Auto-Redbook
|
||||||
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片),以及发布小红书笔记。
|
description: 小红书笔记素材创作技能。当用户需要创建小红书笔记素材时使用这个技能。技能包含:根据用户的需求和提供的资料,撰写小红书笔记内容(标题+正文),生成图片卡片(封面+正文卡片,支持多种样式主题)。
|
||||||
---
|
---
|
||||||
|
|
||||||
# 小红书笔记创作技能
|
# 小红书笔记创作技能
|
||||||
|
|
||||||
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成和笔记发布。
|
这个技能用于创建专业的小红书笔记素材,包括内容撰写、图片卡片生成(支持7种样式主题)和智能分页渲染。
|
||||||
|
|
||||||
## 使用场景
|
## 使用场景
|
||||||
|
|
||||||
- 用户需要创建小红书笔记时
|
- 用户需要创建小红书笔记时
|
||||||
- 用户提供资料需要转化为小红书风格内容时
|
- 用户提供资料需要转化为小红书风格内容时
|
||||||
- 用户需要生成精美的图片卡片用于发布时
|
- 用户需要生成精美的图片卡片用于发布时
|
||||||
|
- 用户需要多种风格样式选择时
|
||||||
|
|
||||||
## 工作流程
|
## 工作流程
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ subtitle: "副标题文案" # 封面副标题(不超过15字)
|
|||||||
2. 用于渲染卡片的 Markdown 文本内容:
|
2. 用于渲染卡片的 Markdown 文本内容:
|
||||||
- 使用 `---` 分割线将正文分隔为多个卡片段落
|
- 使用 `---` 分割线将正文分隔为多个卡片段落
|
||||||
- 每个分段的文字控制在 200 字左右
|
- 每个分段的文字控制在 200 字左右
|
||||||
- 后续会将每个卡片段落渲染为一张图片
|
- 脚本会自动检测内容高度并智能分页
|
||||||
|
|
||||||
完整示例:
|
完整示例:
|
||||||
```markdown
|
```markdown
|
||||||
@@ -72,9 +73,7 @@ subtitle: "对着抄作业就好了,一起变高效"
|
|||||||
|
|
||||||
# 神器二:Raycast ⚡
|
# 神器二:Raycast ⚡
|
||||||
|
|
||||||
\`\`\`
|
|
||||||
可使用代码块来增加渲染后图片的视觉丰富度
|
可使用代码块来增加渲染后图片的视觉丰富度
|
||||||
\`\`\`
|
|
||||||
|
|
||||||
## 推荐原因
|
## 推荐原因
|
||||||
|
|
||||||
@@ -90,28 +89,74 @@ subtitle: "对着抄作业就好了,一起变高效"
|
|||||||
|
|
||||||
...
|
...
|
||||||
|
|
||||||
|
#效率工具 #生产力 #Mac软件
|
||||||
```
|
```
|
||||||
|
|
||||||
### 第三步:渲染图片卡片
|
### 第三步:渲染图片卡片
|
||||||
|
|
||||||
将 Markdown 文档渲染为图片卡片。提供两种渲染脚本:
|
将 Markdown 文档渲染为图片卡片。**推荐使用 V2 版本脚本**,支持智能分页和多种样式。
|
||||||
|
|
||||||
#### Python 渲染脚本
|
#### V2 渲染脚本(推荐)
|
||||||
|
|
||||||
|
V2 版本新增特性:
|
||||||
|
- ✅ **智能分页**:自动检测内容高度,超出时自动拆分到多张卡片
|
||||||
|
- ✅ **多种样式**:支持 7 种预设样式主题
|
||||||
|
- ✅ **字数预估**:基于字数预分配内容,减少渲染次数
|
||||||
|
|
||||||
|
**Python 版本:**
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
python scripts/render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
# 基本用法
|
||||||
|
python scripts/render_xhs_v2.py <markdown_file>
|
||||||
|
|
||||||
|
# 指定输出目录
|
||||||
|
python scripts/render_xhs_v2.py <markdown_file> -o <output_directory>
|
||||||
|
|
||||||
|
# 指定样式主题
|
||||||
|
python scripts/render_xhs_v2.py <markdown_file> --style xiaohongshu
|
||||||
|
|
||||||
|
# 查看所有可用样式
|
||||||
|
python scripts/render_xhs_v2.py --list-styles
|
||||||
```
|
```
|
||||||
|
|
||||||
- 默认输出目录为当前工作目录
|
**Node.js 版本:**
|
||||||
- 生成的图片包括:封面(cover.png)和正文卡片(card_1.png, card_2.png, ...)
|
|
||||||
|
|
||||||
#### Node.js 渲染脚本
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
|
# 基本用法
|
||||||
|
node scripts/render_xhs_v2.js <markdown_file>
|
||||||
|
|
||||||
|
# 指定输出目录和样式
|
||||||
|
node scripts/render_xhs_v2.js <markdown_file> -o ./output --style mint
|
||||||
|
|
||||||
|
# 查看所有可用样式
|
||||||
|
node scripts/render_xhs_v2.js --list-styles
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 可用样式主题
|
||||||
|
|
||||||
|
| 样式键 | 名称 | 描述 |
|
||||||
|
|--------|------|------|
|
||||||
|
| `purple` | 紫韵 | 默认样式,紫蓝色渐变 |
|
||||||
|
| `xiaohongshu` | 小红书红 | 小红书品牌色系 |
|
||||||
|
| `mint` | 清新薄荷 | 绿色/自然调 |
|
||||||
|
| `sunset` | 日落橙 | 粉色/日落渐变 |
|
||||||
|
| `ocean` | 深海蓝 | 蓝绿色海洋调 |
|
||||||
|
| `elegant` | 优雅白 | 简约灰白调 |
|
||||||
|
| `dark` | 暗黑模式 | 深色背景,高对比度 |
|
||||||
|
|
||||||
|
#### 旧版渲染脚本(保留)
|
||||||
|
|
||||||
|
如需使用旧版(不支持自动分页):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 版本
|
||||||
|
python scripts/render_xhs.py <markdown_file> [--output-dir <output_directory>]
|
||||||
|
|
||||||
|
# Node.js 版本
|
||||||
node scripts/render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
node scripts/render_xhs.js <markdown_file> [--output-dir <output_directory>]
|
||||||
```
|
```
|
||||||
|
|
||||||
功能与 Python 版本相同。
|
**旧版已知问题**:单张卡片内容过多时可能出现文字溢出,需手动用 `---` 分隔。
|
||||||
|
|
||||||
### 第四步:发布小红书笔记(可选)
|
### 第四步:发布小红书笔记(可选)
|
||||||
|
|
||||||
@@ -139,30 +184,46 @@ XHS_COOKIE=your_cookie_string_here
|
|||||||
- 尺寸比例:3:4(小红书推荐比例)
|
- 尺寸比例:3:4(小红书推荐比例)
|
||||||
- 基准尺寸:1080×1440px
|
- 基准尺寸:1080×1440px
|
||||||
- 包含:Emoji 装饰、大标题、副标题
|
- 包含:Emoji 装饰、大标题、副标题
|
||||||
- 样式:渐变背景 + 圆角内容区
|
- 样式:渐变背景 + 圆角内容区(根据所选主题变化)
|
||||||
|
|
||||||
### 正文卡片
|
### 正文卡片
|
||||||
- 尺寸比例:3:4
|
- 尺寸比例:3:4
|
||||||
- 基准尺寸:1080×1440px
|
- 基准尺寸:1080×1440px
|
||||||
- 支持:标题、段落、列表、引用、代码块、图片
|
- 支持:标题、段落、列表、引用、代码块、图片
|
||||||
- 样式:白色卡片 + 渐变背景边框
|
- 样式:白色卡片 + 渐变背景边框(根据所选主题变化)
|
||||||
|
- V2 版本:自动分页,单张卡片内容不会溢出
|
||||||
|
|
||||||
## 技能资源
|
## 技能资源
|
||||||
|
|
||||||
### 脚本文件
|
### 脚本文件
|
||||||
- `scripts/render_xhs.py` - Python 渲染脚本
|
- `scripts/render_xhs.py` - Python V1 渲染脚本(旧版)
|
||||||
- `scripts/render_xhs.js` - Node.js 渲染脚本
|
- `scripts/render_xhs.js` - Node.js V1 渲染脚本(旧版)
|
||||||
|
- `scripts/render_xhs_v2.py` - Python V2 渲染脚本(推荐 ✅)
|
||||||
|
- `scripts/render_xhs_v2.js` - Node.js V2 渲染脚本(推荐 ✅)
|
||||||
- `scripts/publish_xhs.py` - 小红书发布脚本
|
- `scripts/publish_xhs.py` - 小红书发布脚本
|
||||||
|
|
||||||
### 资源文件
|
### 资源文件
|
||||||
- `assets/cover.html` - 封面 HTML 模板
|
- `assets/cover.html` - 封面 HTML 模板(旧版)
|
||||||
- `assets/card.html` - 正文卡片 HTML 模板
|
- `assets/card.html` - 正文卡片 HTML 模板(旧版)
|
||||||
- `assets/styles.css` - 共用样式表
|
- `assets/styles.css` - 共用样式表(旧版)
|
||||||
|
- `assets/example.md` - 示例 Markdown 文件
|
||||||
|
|
||||||
## 注意事项
|
## 注意事项
|
||||||
|
|
||||||
1. Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
|
1. **V2 版本推荐**:V2 版本支持智能分页,可自动处理内容溢出问题
|
||||||
2. 技能目录 (`md2Redbook/`) 仅存放脚本和模板,不存放用户数据
|
2. **样式选择**:根据内容风格选择合适的样式主题
|
||||||
3. 图片尺寸会根据内容自动调整,但保持 3:4 比例
|
3. **Markdown 位置**:Markdown 文件应保存在工作目录,渲染后的图片也保存在工作目录
|
||||||
4. Cookie 有有效期限制,过期后需要重新获取
|
4. **内容长度**:建议每个 `---` 分隔的内容块控制在 200 字以内
|
||||||
5. 发布功能依赖 xhs 库,需要安装:`pip install xhs`
|
5. **Cookie 有效期**:发布功能的 Cookie 有过期限制,过期后需要重新获取
|
||||||
|
6. **发布依赖**:发布功能依赖 xhs 库,需要安装:`pip install xhs`
|
||||||
|
|
||||||
|
## 智能分页说明
|
||||||
|
|
||||||
|
V2 版本的智能分页机制:
|
||||||
|
|
||||||
|
1. **预估阶段**:基于字数、元素类型预估内容高度
|
||||||
|
2. **预渲染阶段**:使用 Playwright 预渲染并测量实际高度
|
||||||
|
3. **拆分阶段**:如果内容超出,按段落/行智能拆分内容
|
||||||
|
4. **固定输出**:每张卡片固定为 1080×1440px,确保一致性
|
||||||
|
|
||||||
|
这种机制确保无论内容多长,都不会出现文字溢出问题。
|
||||||
|
|||||||
98
STYLES.md
Normal file
98
STYLES.md
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
# 小红书笔记样式预览
|
||||||
|
|
||||||
|
本文档展示所有可用的样式主题,方便用户选择合适的设计风格。
|
||||||
|
|
||||||
|
## 样式列表
|
||||||
|
|
||||||
|
### 1. purple(紫韵)- 默认
|
||||||
|
- **封面背景**:蓝紫色渐变
|
||||||
|
- **卡片背景**:紫蓝渐变
|
||||||
|
- **强调色**:#6366f1
|
||||||
|
- **适用场景**:通用、科技感、创意设计
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style purple
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. xiaohongshu(小红书红)
|
||||||
|
- **封面背景**:小红书品牌红渐变
|
||||||
|
- **卡片背景**:粉红渐变
|
||||||
|
- **强调色**:#FF2442
|
||||||
|
- **适用场景**:小红书原生风格、时尚、生活方式
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style xiaohongshu
|
||||||
|
```
|
||||||
|
|
||||||
|
### 3. mint(清新薄荷)
|
||||||
|
- **封面背景**:绿色渐变
|
||||||
|
- **卡片背景**:薄荷绿渐变
|
||||||
|
- **强调色**:#43e97b
|
||||||
|
- **适用场景**:健康、环保、自然、春季主题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style mint
|
||||||
|
```
|
||||||
|
|
||||||
|
### 4. sunset(日落橙)
|
||||||
|
- **封面背景**:粉橙渐变
|
||||||
|
- **卡片背景**:日落渐变
|
||||||
|
- **强调色**:#fa709a
|
||||||
|
- **适用场景**:温暖、浪漫、傍晚、秋季主题
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style sunset
|
||||||
|
```
|
||||||
|
|
||||||
|
### 5. ocean(深海蓝)
|
||||||
|
- **封面背景**:天蓝渐变
|
||||||
|
- **卡片背景**:海洋蓝渐变
|
||||||
|
- **强调色**:#4facfe
|
||||||
|
- **适用场景**:清新、水、夏季主题、专业商务
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style ocean
|
||||||
|
```
|
||||||
|
|
||||||
|
### 6. elegant(优雅白)
|
||||||
|
- **封面背景**:灰白渐变
|
||||||
|
- **卡片背景**:浅灰渐变
|
||||||
|
- **强调色**:#333333
|
||||||
|
- **适用场景**:极简、商务、正式、高级
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style elegant
|
||||||
|
```
|
||||||
|
|
||||||
|
### 7. dark(暗黑模式)
|
||||||
|
- **封面背景**:深蓝黑色渐变
|
||||||
|
- **卡片背景**:深色渐变
|
||||||
|
- **强调色**:#e94560
|
||||||
|
- **适用场景**:夜间阅读、科技、编程、游戏
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python scripts/render_xhs_v2.py note.md --style dark
|
||||||
|
```
|
||||||
|
|
||||||
|
## 样式选择建议
|
||||||
|
|
||||||
|
| 内容类型 | 推荐样式 |
|
||||||
|
|----------|----------|
|
||||||
|
| 科技/编程 | dark, purple |
|
||||||
|
| 时尚/美妆 | xiaohongshu, sunset |
|
||||||
|
| 健康/自然 | mint, ocean |
|
||||||
|
| 商务/职场 | ocean, elegant |
|
||||||
|
| 生活/情感 | sunset, xiaohongshu |
|
||||||
|
| 创意/设计 | purple, dark |
|
||||||
|
|
||||||
|
## 查看所有样式
|
||||||
|
|
||||||
|
运行以下命令列出所有可用样式:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Python 版本
|
||||||
|
python scripts/render_xhs_v2.py --list-styles
|
||||||
|
|
||||||
|
# Node.js 版本
|
||||||
|
node scripts/render_xhs_v2.js --list-styles
|
||||||
|
```
|
||||||
@@ -4,7 +4,7 @@ title: "5个效率神器"
|
|||||||
subtitle: "让工作效率翻倍"
|
subtitle: "让工作效率翻倍"
|
||||||
---
|
---
|
||||||
|
|
||||||
## 神器一:Notion 📝
|
# 神器一:Notion 📝
|
||||||
|
|
||||||
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
全能型笔记工具,支持数据库、看板、日历等多种视图。
|
||||||
|
|
||||||
@@ -18,33 +18,60 @@ subtitle: "让工作效率翻倍"
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 神器二:Raycast ⚡
|
# 神器二:Raycast ⚡
|
||||||
|
|
||||||
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# 快捷命令示例
|
||||||
|
raycast://extensions/raycast/clipboard/clipboard-history
|
||||||
|
```
|
||||||
|
|
||||||
**必装插件推荐:**
|
**必装插件推荐:**
|
||||||
- 剪贴板历史
|
- 剪贴板历史
|
||||||
- 窗口管理
|
- 窗口管理
|
||||||
- 快捷短语
|
- 快捷短语
|
||||||
- API 调试工具
|
- API 调试工具
|
||||||
|
|
||||||
一键搜索、快速启动,让你的 Mac 飞起来 ✈️
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 神器三:Arc 浏览器 🌈
|
# 神器三:Arc 浏览器 🌈
|
||||||
|
|
||||||
重新定义浏览器体验:
|
全新理念的浏览器体验:
|
||||||
- 侧边栏标签管理
|
- 侧边栏标签管理
|
||||||
- 空间分组功能
|
- 空间分组功能
|
||||||
- 内置笔记和画板
|
- 内置笔记和画板
|
||||||
- 极简无干扰模式
|
- 极简无干扰模式
|
||||||
|
|
||||||
告别标签栏焦虑,专注当下任务!
|
---
|
||||||
|
|
||||||
|
# 神器四:Warp 终端 🖥️
|
||||||
|
|
||||||
|
基于 Rust 的现代化终端:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 支持 AI 智能补全
|
||||||
|
def example():
|
||||||
|
print("Hello Warp!")
|
||||||
|
```
|
||||||
|
|
||||||
|
- ⚡ 极速性能
|
||||||
|
- 🤖 AI 智能提示
|
||||||
|
- 📋 自动补全
|
||||||
|
- 🎯 分组工作区
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 总结 🎯
|
# 神器五:Fig 自动补全 🔮
|
||||||
|
|
||||||
|
终端命令自动补全神器:
|
||||||
|
- 数百种 CLI 工具支持
|
||||||
|
- 可视化参数提示
|
||||||
|
- 团队协作分享
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# 总结 🎯
|
||||||
|
|
||||||
效率提升不在于工具多少,而在于是否**真正用起来**。
|
效率提升不在于工具多少,而在于是否**真正用起来**。
|
||||||
|
|
||||||
@@ -54,4 +81,4 @@ Mac 上的效率启动器,比 Spotlight 强大 100 倍!
|
|||||||
✅ 减少 80% 的焦虑
|
✅ 减少 80% 的焦虑
|
||||||
✅ 提升 100% 的专注力
|
✅ 提升 100% 的专注力
|
||||||
|
|
||||||
#效率工具 #生产力 #Mac软件 #工作技巧 #神器推荐 #Notion #Raycast #Arc浏览器
|
#效率工具 #生产力 #Mac软件 #工作技巧 #神器推荐 #Notion #Raycast #Arc浏览器 #Warp #Fig
|
||||||
|
|||||||
723
scripts/render_xhs_v2.js
Normal file
723
scripts/render_xhs_v2.js
Normal file
@@ -0,0 +1,723 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
/**
|
||||||
|
* 小红书卡片渲染脚本 V2 - Node.js 智能分页版
|
||||||
|
* 将 Markdown 文件渲染为小红书风格的图片卡片
|
||||||
|
*
|
||||||
|
* 新特性:
|
||||||
|
* 1. 智能分页:自动检测内容高度,超出时自动拆分到多张卡片
|
||||||
|
* 2. 多种样式:支持多种预设样式主题
|
||||||
|
*
|
||||||
|
* 使用方法:
|
||||||
|
* node render_xhs_v2.js <markdown_file> [options]
|
||||||
|
*
|
||||||
|
* 依赖安装:
|
||||||
|
* npm install marked js-yaml playwright
|
||||||
|
* npx playwright install chromium
|
||||||
|
*/
|
||||||
|
|
||||||
|
const fs = require('fs');
|
||||||
|
const path = require('path');
|
||||||
|
const { chromium } = require('playwright');
|
||||||
|
const { marked } = require('marked');
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
|
// 获取脚本所在目录
|
||||||
|
const SCRIPT_DIR = path.dirname(__dirname);
|
||||||
|
const ASSETS_DIR = path.join(SCRIPT_DIR, 'assets');
|
||||||
|
|
||||||
|
// 卡片尺寸配置 (3:4 比例)
|
||||||
|
const CARD_WIDTH = 1080;
|
||||||
|
const CARD_HEIGHT = 1440;
|
||||||
|
|
||||||
|
// 内容区域安全高度
|
||||||
|
const SAFE_HEIGHT = CARD_HEIGHT - 120 - 100 - 80 - 40; // ~1100px
|
||||||
|
|
||||||
|
// 样式配置
|
||||||
|
const STYLES = {
|
||||||
|
purple: {
|
||||||
|
name: "紫韵",
|
||||||
|
cover_bg: "linear-gradient(180deg, #3450E4 0%, #D266DA 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
|
accent_color: "#6366f1",
|
||||||
|
},
|
||||||
|
xiaohongshu: {
|
||||||
|
name: "小红书红",
|
||||||
|
cover_bg: "linear-gradient(180deg, #FF2442 0%, #FF6B81 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #FF2442 0%, #FF6B81 100%)",
|
||||||
|
accent_color: "#FF2442",
|
||||||
|
},
|
||||||
|
mint: {
|
||||||
|
name: "清新薄荷",
|
||||||
|
cover_bg: "linear-gradient(180deg, #43e97b 0%, #38f9d7 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)",
|
||||||
|
accent_color: "#43e97b",
|
||||||
|
},
|
||||||
|
sunset: {
|
||||||
|
name: "日落橙",
|
||||||
|
cover_bg: "linear-gradient(180deg, #fa709a 0%, #fee140 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #fa709a 0%, #fee140 100%)",
|
||||||
|
accent_color: "#fa709a",
|
||||||
|
},
|
||||||
|
ocean: {
|
||||||
|
name: "深海蓝",
|
||||||
|
cover_bg: "linear-gradient(180deg, #4facfe 0%, #00f2fe 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
|
||||||
|
accent_color: "#4facfe",
|
||||||
|
},
|
||||||
|
elegant: {
|
||||||
|
name: "优雅白",
|
||||||
|
cover_bg: "linear-gradient(180deg, #f5f5f5 0%, #e0e0e0 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)",
|
||||||
|
accent_color: "#333333",
|
||||||
|
},
|
||||||
|
dark: {
|
||||||
|
name: "暗黑模式",
|
||||||
|
cover_bg: "linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)",
|
||||||
|
card_bg: "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
|
||||||
|
accent_color: "#e94560",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析 Markdown 文件,提取 YAML 头部和正文内容
|
||||||
|
*/
|
||||||
|
function parseMarkdownFile(filePath) {
|
||||||
|
const content = fs.readFileSync(filePath, 'utf-8');
|
||||||
|
|
||||||
|
const yamlPattern = /^---\s*\n([\s\S]*?)\n---\s*\n/;
|
||||||
|
const yamlMatch = content.match(yamlPattern);
|
||||||
|
|
||||||
|
let metadata = {};
|
||||||
|
let body = content;
|
||||||
|
|
||||||
|
if (yamlMatch) {
|
||||||
|
try {
|
||||||
|
metadata = yaml.load(yamlMatch[1]) || {};
|
||||||
|
} catch (e) {
|
||||||
|
metadata = {};
|
||||||
|
}
|
||||||
|
body = content.slice(yamlMatch[0].length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { metadata, body: body.trim() };
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 按照 --- 分隔符拆分正文为多张卡片内容
|
||||||
|
*/
|
||||||
|
function splitContentBySeparator(body) {
|
||||||
|
const parts = body.split(/\n---+\n/);
|
||||||
|
return parts.filter(part => part.trim()).map(part => part.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 预估内容高度
|
||||||
|
*/
|
||||||
|
function estimateContentHeight(content) {
|
||||||
|
const lines = content.split('\n');
|
||||||
|
let totalHeight = 0;
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const trimmed = line.trim();
|
||||||
|
if (!trimmed) {
|
||||||
|
totalHeight += 20;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (trimmed.startsWith('# ')) {
|
||||||
|
totalHeight += 130;
|
||||||
|
} else if (trimmed.startsWith('## ')) {
|
||||||
|
totalHeight += 110;
|
||||||
|
} else if (trimmed.startsWith('### ')) {
|
||||||
|
totalHeight += 90;
|
||||||
|
} else if (trimmed.startsWith('```')) {
|
||||||
|
totalHeight += 80;
|
||||||
|
} else if (trimmed.match(/^[-*+]\s/)) {
|
||||||
|
totalHeight += 85;
|
||||||
|
} else if (trimmed.startsWith('>')) {
|
||||||
|
totalHeight += 100;
|
||||||
|
} else if (trimmed.startsWith('![')) {
|
||||||
|
totalHeight += 300;
|
||||||
|
} else {
|
||||||
|
const charCount = trimmed.length;
|
||||||
|
const linesNeeded = Math.max(1, charCount / 28);
|
||||||
|
totalHeight += Math.floor(linesNeeded * 42 * 1.7) + 35;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return totalHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 智能拆分内容
|
||||||
|
*/
|
||||||
|
function smartSplitContent(content, maxHeight = SAFE_HEIGHT) {
|
||||||
|
const blocks = [];
|
||||||
|
let currentBlock = [];
|
||||||
|
|
||||||
|
const lines = content.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (line.trim().startsWith('#') && currentBlock.length > 0) {
|
||||||
|
blocks.push(currentBlock.join('\n'));
|
||||||
|
currentBlock = [line];
|
||||||
|
} else if (line.trim() === '---') {
|
||||||
|
if (currentBlock.length > 0) {
|
||||||
|
blocks.push(currentBlock.join('\n'));
|
||||||
|
currentBlock = [];
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentBlock.push(line);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentBlock.length > 0) {
|
||||||
|
blocks.push(currentBlock.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (blocks.length <= 1) {
|
||||||
|
const paragraphs = content.split('\n\n').filter(b => b.trim());
|
||||||
|
blocks.length = 0;
|
||||||
|
blocks.push(...paragraphs);
|
||||||
|
}
|
||||||
|
|
||||||
|
const cards = [];
|
||||||
|
let currentCard = [];
|
||||||
|
let currentHeight = 0;
|
||||||
|
|
||||||
|
for (const block of blocks) {
|
||||||
|
const blockHeight = estimateContentHeight(block);
|
||||||
|
|
||||||
|
if (blockHeight > maxHeight) {
|
||||||
|
if (currentCard.length > 0) {
|
||||||
|
cards.push(currentCard.join('\n\n'));
|
||||||
|
currentCard = [];
|
||||||
|
currentHeight = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const blockLines = block.split('\n');
|
||||||
|
let subBlock = [];
|
||||||
|
let subHeight = 0;
|
||||||
|
|
||||||
|
for (const line of blockLines) {
|
||||||
|
const lineHeight = estimateContentHeight(line);
|
||||||
|
|
||||||
|
if (subHeight + lineHeight > maxHeight && subBlock.length > 0) {
|
||||||
|
cards.push(subBlock.join('\n'));
|
||||||
|
subBlock = [line];
|
||||||
|
subHeight = lineHeight;
|
||||||
|
} else {
|
||||||
|
subBlock.push(line);
|
||||||
|
subHeight += lineHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subBlock.length > 0) {
|
||||||
|
cards.push(subBlock.join('\n'));
|
||||||
|
}
|
||||||
|
} else if (currentHeight + blockHeight > maxHeight && currentCard.length > 0) {
|
||||||
|
cards.push(currentCard.join('\n\n'));
|
||||||
|
currentCard = [block];
|
||||||
|
currentHeight = blockHeight;
|
||||||
|
} else {
|
||||||
|
currentCard.push(block);
|
||||||
|
currentHeight += blockHeight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCard.length > 0) {
|
||||||
|
cards.push(currentCard.join('\n\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
return cards.length > 0 ? cards : [content];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 将 Markdown 转换为 HTML
|
||||||
|
*/
|
||||||
|
function convertMarkdownToHtml(mdContent, style = STYLES.purple) {
|
||||||
|
const tagsPattern = /((?:#[\w\u4e00-\u9fa5]+\s*)+)$/m;
|
||||||
|
const tagsMatch = mdContent.match(tagsPattern);
|
||||||
|
let tagsHtml = "";
|
||||||
|
|
||||||
|
if (tagsMatch) {
|
||||||
|
const tagsStr = tagsMatch[1];
|
||||||
|
mdContent = mdContent.slice(0, tagsMatch.index).trim();
|
||||||
|
const tags = tagsStr.match(/#([\w\u4e00-\u9fa5]+)/g);
|
||||||
|
if (tags) {
|
||||||
|
const accent = style.accent_color;
|
||||||
|
tagsHtml = '<div class="tags-container">';
|
||||||
|
for (const tag of tags) {
|
||||||
|
tagsHtml += `<span class="tag" style="background: ${accent};">${tag}</span>`;
|
||||||
|
}
|
||||||
|
tagsHtml += '</div>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const html = marked.parse(mdContent, { breaks: true, gfm: true });
|
||||||
|
return html + tagsHtml;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成封面 HTML
|
||||||
|
*/
|
||||||
|
function generateCoverHtml(metadata, styleKey = 'purple') {
|
||||||
|
const style = STYLES[styleKey] || STYLES.purple;
|
||||||
|
|
||||||
|
const emoji = metadata.emoji || '📝';
|
||||||
|
let title = metadata.title || '标题';
|
||||||
|
let subtitle = metadata.subtitle || '';
|
||||||
|
|
||||||
|
if (title.length > 15) title = title.slice(0, 15);
|
||||||
|
if (subtitle.length > 15) subtitle = subtitle.slice(0, 15);
|
||||||
|
|
||||||
|
const isDark = styleKey === 'dark';
|
||||||
|
const textColor = isDark ? '#ffffff' : '#000000';
|
||||||
|
const titleGradient = isDark
|
||||||
|
? 'linear-gradient(180deg, #ffffff 0%, #cccccc 100%)'
|
||||||
|
: 'linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%)';
|
||||||
|
const innerBg = isDark ? '#1a1a2e' : '#F3F3F3';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1440">
|
||||||
|
<title>小红书封面</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px; height: 1440px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.cover-container {
|
||||||
|
width: 1080px; height: 1440px;
|
||||||
|
background: ${style.cover_bg};
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}
|
||||||
|
.cover-inner {
|
||||||
|
position: absolute; width: 950px; height: 1310px;
|
||||||
|
left: 65px; top: 65px;
|
||||||
|
background: ${innerBg};
|
||||||
|
border-radius: 25px;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
padding: 80px 85px;
|
||||||
|
}
|
||||||
|
.cover-emoji { font-size: 180px; line-height: 1.2; margin-bottom: 50px; }
|
||||||
|
.cover-title {
|
||||||
|
font-weight: 900; font-size: 130px; line-height: 1.4;
|
||||||
|
background: ${titleGradient};
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
flex: 1;
|
||||||
|
display: flex; align-items: flex-start;
|
||||||
|
word-break: break-all;
|
||||||
|
}
|
||||||
|
.cover-subtitle {
|
||||||
|
font-weight: 350; font-size: 72px; line-height: 1.4;
|
||||||
|
color: ${textColor};
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-container">
|
||||||
|
<div class="cover-inner">
|
||||||
|
<div class="cover-emoji">${emoji}</div>
|
||||||
|
<div class="cover-title">${title}</div>
|
||||||
|
<div class="cover-subtitle">${subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 生成正文卡片 HTML
|
||||||
|
*/
|
||||||
|
function generateCardHtml(content, pageNumber = 1, totalPages = 1, styleKey = 'purple') {
|
||||||
|
const style = STYLES[styleKey] || STYLES.purple;
|
||||||
|
const htmlContent = convertMarkdownToHtml(content, style);
|
||||||
|
const pageText = totalPages > 1 ? `${pageNumber}/${totalPages}` : '';
|
||||||
|
|
||||||
|
const isDark = styleKey === 'dark';
|
||||||
|
const cardBg = isDark ? 'rgba(30, 30, 46, 0.95)' : 'rgba(255, 255, 255, 0.95)';
|
||||||
|
const textColor = isDark ? '#e0e0e0' : '#475569';
|
||||||
|
const headingColor = isDark ? '#ffffff' : '#1e293b';
|
||||||
|
const h2Color = isDark ? '#e0e0e0' : '#334155';
|
||||||
|
const h3Color = isDark ? '#c0c0c0' : '#475569';
|
||||||
|
const codeBg = isDark ? '#252540' : '#f1f5f9';
|
||||||
|
const preBg = isDark ? '#0f0f23' : '#1e293b';
|
||||||
|
const blockquoteBg = isDark ? '#252540' : '#f1f5f9';
|
||||||
|
const blockquoteColor = isDark ? '#a0a0a0' : '#64748b';
|
||||||
|
const hrBg = isDark ? '#333355' : '#e2e8f0';
|
||||||
|
|
||||||
|
return `<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080">
|
||||||
|
<title>小红书卡片</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px; min-height: 1440px; overflow: hidden; background: transparent;
|
||||||
|
}
|
||||||
|
.card-container {
|
||||||
|
width: 1080px; min-height: 1440px;
|
||||||
|
background: ${style.card_bg};
|
||||||
|
position: relative; padding: 50px; overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-inner {
|
||||||
|
background: ${cardBg};
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px;
|
||||||
|
min-height: calc(1440px - 100px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
color: ${textColor};
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}
|
||||||
|
.card-content h1 {
|
||||||
|
font-size: 72px; font-weight: 700; color: ${headingColor};
|
||||||
|
margin-bottom: 40px; line-height: 1.3;
|
||||||
|
}
|
||||||
|
.card-content h2 {
|
||||||
|
font-size: 56px; font-weight: 600; color: ${h2Color};
|
||||||
|
margin: 50px 0 25px 0; line-height: 1.4;
|
||||||
|
}
|
||||||
|
.card-content h3 {
|
||||||
|
font-size: 48px; font-weight: 600; color: ${h3Color};
|
||||||
|
margin: 40px 0 20px 0;
|
||||||
|
}
|
||||||
|
.card-content p { margin-bottom: 35px; }
|
||||||
|
.card-content strong { font-weight: 700; color: ${headingColor}; }
|
||||||
|
.card-content em { font-style: italic; color: ${style.accent_color}; }
|
||||||
|
.card-content a {
|
||||||
|
color: ${style.accent_color}; text-decoration: none;
|
||||||
|
border-bottom: 2px solid ${style.accent_color};
|
||||||
|
}
|
||||||
|
.card-content ul, .card-content ol {
|
||||||
|
margin: 30px 0; padding-left: 60px;
|
||||||
|
}
|
||||||
|
.card-content li { margin-bottom: 20px; line-height: 1.6; }
|
||||||
|
.card-content blockquote {
|
||||||
|
border-left: 8px solid ${style.accent_color};
|
||||||
|
padding-left: 40px;
|
||||||
|
background: ${blockquoteBg};
|
||||||
|
padding-top: 25px; padding-bottom: 25px; padding-right: 30px;
|
||||||
|
margin: 35px 0;
|
||||||
|
color: ${blockquoteColor};
|
||||||
|
font-style: italic;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}
|
||||||
|
.card-content blockquote p { margin: 0; }
|
||||||
|
.card-content code {
|
||||||
|
background: ${codeBg};
|
||||||
|
padding: 6px 16px; border-radius: 8px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 38px;
|
||||||
|
color: ${style.accent_color};
|
||||||
|
}
|
||||||
|
.card-content pre {
|
||||||
|
background: ${preBg};
|
||||||
|
color: ${isDark ? '#e0e0e0' : '#e2e8f0'};
|
||||||
|
padding: 40px; border-radius: 16px;
|
||||||
|
margin: 35px 0;
|
||||||
|
overflow-x: visible;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 36px; line-height: 1.5;
|
||||||
|
}
|
||||||
|
.card-content pre code {
|
||||||
|
background: transparent; color: inherit; padding: 0; font-size: inherit;
|
||||||
|
}
|
||||||
|
.card-content img {
|
||||||
|
max-width: 100%; height: auto; border-radius: 16px;
|
||||||
|
margin: 35px auto; display: block;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
.card-content hr {
|
||||||
|
border: none; height: 2px;
|
||||||
|
background: ${hrBg};
|
||||||
|
margin: 50px 0;
|
||||||
|
}
|
||||||
|
.tags-container {
|
||||||
|
margin-top: 50px; padding-top: 30px;
|
||||||
|
border-top: 2px solid ${hrBg};
|
||||||
|
}
|
||||||
|
.tag {
|
||||||
|
display: inline-block;
|
||||||
|
background: ${style.accent_color};
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px; border-radius: 30px;
|
||||||
|
font-size: 34px;
|
||||||
|
margin: 10px 15px 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
.page-number {
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px; right: 80px;
|
||||||
|
font-size: 36px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-content">
|
||||||
|
${htmlContent}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-number">${pageText}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 测量内容高度
|
||||||
|
*/
|
||||||
|
async function measureContentHeight(page, htmlContent) {
|
||||||
|
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
return await page.evaluate(() => {
|
||||||
|
const inner = document.querySelector('.card-inner');
|
||||||
|
if (inner) return inner.scrollHeight;
|
||||||
|
const container = document.querySelector('.card-container');
|
||||||
|
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 处理和渲染卡片
|
||||||
|
*/
|
||||||
|
async function processAndRenderCards(cardContents, outputDir, styleKey) {
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: CARD_WIDTH, height: CARD_HEIGHT } });
|
||||||
|
|
||||||
|
const allCards = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (const content of cardContents) {
|
||||||
|
const estimatedHeight = estimateContentHeight(content);
|
||||||
|
|
||||||
|
let splitContents;
|
||||||
|
if (estimatedHeight > SAFE_HEIGHT) {
|
||||||
|
splitContents = smartSplitContent(content, SAFE_HEIGHT);
|
||||||
|
} else {
|
||||||
|
splitContents = [content];
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const splitContent of splitContents) {
|
||||||
|
const tempHtml = generateCardHtml(splitContent, 1, 1, styleKey);
|
||||||
|
const actualHeight = await measureContentHeight(page, tempHtml);
|
||||||
|
|
||||||
|
if (actualHeight > CARD_HEIGHT - 100) {
|
||||||
|
const lines = splitContent.split('\n');
|
||||||
|
const subContents = [];
|
||||||
|
let subLines = [];
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
const testLines = [...subLines, line];
|
||||||
|
const testHtml = generateCardHtml(testLines.join('\n'), 1, 1, styleKey);
|
||||||
|
const testHeight = await measureContentHeight(page, testHtml);
|
||||||
|
|
||||||
|
if (testHeight > CARD_HEIGHT - 100 && subLines.length > 0) {
|
||||||
|
subContents.push(subLines.join('\n'));
|
||||||
|
subLines = [line];
|
||||||
|
} else {
|
||||||
|
subLines = testLines;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (subLines.length > 0) {
|
||||||
|
subContents.push(subLines.join('\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
allCards.push(...subContents);
|
||||||
|
} else {
|
||||||
|
allCards.push(splitContent);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
return allCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 渲染 HTML 到图片
|
||||||
|
*/
|
||||||
|
async function renderHtmlToImage(page, htmlContent, outputPath) {
|
||||||
|
await page.setContent(htmlContent, { waitUntil: 'networkidle' });
|
||||||
|
await page.waitForTimeout(300);
|
||||||
|
|
||||||
|
await page.screenshot({
|
||||||
|
path: outputPath,
|
||||||
|
clip: { x: 0, y: 0, width: CARD_WIDTH, height: CARD_HEIGHT },
|
||||||
|
type: 'png'
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log(` ✅ 已生成: ${outputPath}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 主渲染函数
|
||||||
|
*/
|
||||||
|
async function renderMarkdownToCards(mdFile, outputDir, styleKey = 'purple') {
|
||||||
|
console.log(`\n🎨 开始渲染: ${mdFile}`);
|
||||||
|
console.log(`🎨 使用样式: ${STYLES[styleKey].name}`);
|
||||||
|
|
||||||
|
if (!fs.existsSync(outputDir)) {
|
||||||
|
fs.mkdirSync(outputDir, { recursive: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const data = parseMarkdownFile(mdFile);
|
||||||
|
const { metadata, body } = data;
|
||||||
|
|
||||||
|
const cardContents = splitContentBySeparator(body);
|
||||||
|
console.log(` 📄 检测到 ${cardContents.length} 个内容块`);
|
||||||
|
|
||||||
|
console.log(' 🔍 分析内容高度并智能分页...');
|
||||||
|
const processedCards = await processAndRenderCards(cardContents, outputDir, styleKey);
|
||||||
|
const totalCards = processedCards.length;
|
||||||
|
console.log(` 📄 将生成 ${totalCards} 张卡片`);
|
||||||
|
|
||||||
|
if (metadata.emoji || metadata.title) {
|
||||||
|
console.log(' 📷 生成封面...');
|
||||||
|
const coverHtml = generateCoverHtml(metadata, styleKey);
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: CARD_WIDTH, height: CARD_HEIGHT } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
await renderHtmlToImage(page, coverHtml, path.join(outputDir, 'cover.png'));
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const browser = await chromium.launch();
|
||||||
|
const page = await browser.newPage({ viewport: { width: CARD_WIDTH, height: CARD_HEIGHT } });
|
||||||
|
|
||||||
|
try {
|
||||||
|
for (let i = 0; i < processedCards.length; i++) {
|
||||||
|
const pageNum = i + 1;
|
||||||
|
console.log(` 📷 生成卡片 ${pageNum}/${totalCards}...`);
|
||||||
|
|
||||||
|
const cardHtml = generateCardHtml(processedCards[i], pageNum, totalCards, styleKey);
|
||||||
|
const cardPath = path.join(outputDir, `card_${pageNum}.png`);
|
||||||
|
|
||||||
|
await renderHtmlToImage(page, cardHtml, cardPath);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
await browser.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`\n✨ 渲染完成!共生成 ${totalCards} 张卡片,保存到: ${outputDir}`);
|
||||||
|
return totalCards;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 列出所有样式
|
||||||
|
*/
|
||||||
|
function listStyles() {
|
||||||
|
console.log('\n📋 可用样式列表:');
|
||||||
|
console.log('-'.repeat(40));
|
||||||
|
for (const [key, style] of Object.entries(STYLES)) {
|
||||||
|
console.log(` ${key.padEnd(12)} - ${style.name}`);
|
||||||
|
}
|
||||||
|
console.log('-'.repeat(40));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 解析命令行参数
|
||||||
|
*/
|
||||||
|
function parseArgs() {
|
||||||
|
const args = process.argv.slice(2);
|
||||||
|
|
||||||
|
if (args.length === 0 || args.includes('--help')) {
|
||||||
|
console.log(`
|
||||||
|
使用方法: node render_xhs_v2.js <markdown_file> [options]
|
||||||
|
|
||||||
|
选项:
|
||||||
|
-o, --output-dir <dir> 输出目录(默认为当前工作目录)
|
||||||
|
-s, --style <style> 样式主题(默认: purple)
|
||||||
|
--list-styles 列出所有可用样式
|
||||||
|
--help 显示帮助信息
|
||||||
|
|
||||||
|
可用样式:
|
||||||
|
purple, xiaohongshu, mint, sunset, ocean, elegant, dark
|
||||||
|
|
||||||
|
示例:
|
||||||
|
node render_xhs_v2.js note.md
|
||||||
|
node render_xhs_v2.js note.md -o ./output --style xiaohongshu
|
||||||
|
`);
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (args.includes('--list-styles')) {
|
||||||
|
listStyles();
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
|
let markdownFile = null;
|
||||||
|
let outputDir = process.cwd();
|
||||||
|
let style = 'purple';
|
||||||
|
|
||||||
|
for (let i = 0; i < args.length; i++) {
|
||||||
|
if (args[i] === '--output-dir' || args[i] === '-o') {
|
||||||
|
outputDir = args[i + 1];
|
||||||
|
i++;
|
||||||
|
} else if (args[i] === '--style' || args[i] === '-s') {
|
||||||
|
if (STYLES[args[i + 1]]) {
|
||||||
|
style = args[i + 1];
|
||||||
|
} else {
|
||||||
|
console.error(`❌ 无效样式: ${args[i + 1]}`);
|
||||||
|
console.log('可用样式:', Object.keys(STYLES).join(', '));
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
i++;
|
||||||
|
} else if (!args[i].startsWith('-')) {
|
||||||
|
markdownFile = args[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!markdownFile) {
|
||||||
|
console.error('❌ 错误: 请指定 Markdown 文件');
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!fs.existsSync(markdownFile)) {
|
||||||
|
console.error(`❌ 错误: 文件不存在 - ${markdownFile}`);
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return { markdownFile, outputDir, style };
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主函数
|
||||||
|
async function main() {
|
||||||
|
const { markdownFile, outputDir, style } = parseArgs();
|
||||||
|
await renderMarkdownToCards(markdownFile, outputDir, style);
|
||||||
|
}
|
||||||
|
|
||||||
|
main().catch(error => {
|
||||||
|
console.error('❌ 渲染失败:', error.message);
|
||||||
|
process.exit(1);
|
||||||
|
});
|
||||||
737
scripts/render_xhs_v2.py
Normal file
737
scripts/render_xhs_v2.py
Normal file
@@ -0,0 +1,737 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""
|
||||||
|
小红书卡片渲染脚本 V2 - 智能分页版
|
||||||
|
将 Markdown 文件渲染为小红书风格的图片卡片
|
||||||
|
|
||||||
|
新特性:
|
||||||
|
1. 智能分页:自动检测内容高度,超出时自动拆分到多张卡片
|
||||||
|
2. 多种样式:支持多种预设样式主题
|
||||||
|
3. 字数预估:基于字数预分配内容,减少渲染次数
|
||||||
|
|
||||||
|
使用方法:
|
||||||
|
python render_xhs_v2.py <markdown_file> [options]
|
||||||
|
|
||||||
|
依赖安装:
|
||||||
|
pip install markdown pyyaml playwright
|
||||||
|
playwright install chromium
|
||||||
|
"""
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import asyncio
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import sys
|
||||||
|
import tempfile
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import List, Dict, Tuple
|
||||||
|
|
||||||
|
try:
|
||||||
|
import markdown
|
||||||
|
import yaml
|
||||||
|
from playwright.async_api import async_playwright, Page
|
||||||
|
except ImportError as e:
|
||||||
|
print(f"缺少依赖: {e}")
|
||||||
|
print("请运行: pip install markdown pyyaml playwright && playwright install chromium")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
|
# 获取脚本所在目录
|
||||||
|
SCRIPT_DIR = Path(__file__).parent.parent
|
||||||
|
ASSETS_DIR = SCRIPT_DIR / "assets"
|
||||||
|
|
||||||
|
# 卡片尺寸配置 (3:4 比例)
|
||||||
|
CARD_WIDTH = 1080
|
||||||
|
CARD_HEIGHT = 1440
|
||||||
|
|
||||||
|
# 内容区域安全高度(考虑 padding 和 margin)
|
||||||
|
# card-inner padding: 60px * 2 = 120px
|
||||||
|
# card-container padding: 50px * 2 = 100px
|
||||||
|
# 页码区域: ~80px
|
||||||
|
# 安全边距: ~40px
|
||||||
|
SAFE_HEIGHT = CARD_HEIGHT - 120 - 100 - 80 - 40 # ~1100px
|
||||||
|
|
||||||
|
# 样式配置
|
||||||
|
STYLES = {
|
||||||
|
"purple": {
|
||||||
|
"name": "紫韵",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #3450E4 0%, #D266DA 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #667eea 0%, #764ba2 100%)",
|
||||||
|
"accent_color": "#6366f1",
|
||||||
|
},
|
||||||
|
"xiaohongshu": {
|
||||||
|
"name": "小红书红",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #FF2442 0%, #FF6B81 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #FF2442 0%, #FF6B81 100%)",
|
||||||
|
"accent_color": "#FF2442",
|
||||||
|
},
|
||||||
|
"mint": {
|
||||||
|
"name": "清新薄荷",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #43e97b 0%, #38f9d7 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #43e97b 0%, #38f9d7 100%)",
|
||||||
|
"accent_color": "#43e97b",
|
||||||
|
},
|
||||||
|
"sunset": {
|
||||||
|
"name": "日落橙",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #fa709a 0%, #fee140 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #fa709a 0%, #fee140 100%)",
|
||||||
|
"accent_color": "#fa709a",
|
||||||
|
},
|
||||||
|
"ocean": {
|
||||||
|
"name": "深海蓝",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #4facfe 0%, #00f2fe 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #4facfe 0%, #00f2fe 100%)",
|
||||||
|
"accent_color": "#4facfe",
|
||||||
|
},
|
||||||
|
"elegant": {
|
||||||
|
"name": "优雅白",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #f5f5f5 0%, #e0e0e0 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #f5f5f5 0%, #e8e8e8 100%)",
|
||||||
|
"accent_color": "#333333",
|
||||||
|
"text_light": "#555555",
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"name": "暗黑模式",
|
||||||
|
"cover_bg": "linear-gradient(180deg, #1a1a2e 0%, #16213e 100%)",
|
||||||
|
"card_bg": "linear-gradient(135deg, #1a1a2e 0%, #16213e 100%)",
|
||||||
|
"accent_color": "#e94560",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_markdown_file(file_path: str) -> dict:
|
||||||
|
"""解析 Markdown 文件,提取 YAML 头部和正文内容"""
|
||||||
|
with open(file_path, 'r', encoding='utf-8') as f:
|
||||||
|
content = f.read()
|
||||||
|
|
||||||
|
# 解析 YAML 头部
|
||||||
|
yaml_pattern = r'^---\s*\n(.*?)\n---\s*\n'
|
||||||
|
yaml_match = re.match(yaml_pattern, content, re.DOTALL)
|
||||||
|
|
||||||
|
metadata = {}
|
||||||
|
body = content
|
||||||
|
|
||||||
|
if yaml_match:
|
||||||
|
try:
|
||||||
|
metadata = yaml.safe_load(yaml_match.group(1)) or {}
|
||||||
|
except yaml.YAMLError:
|
||||||
|
metadata = {}
|
||||||
|
body = content[yaml_match.end():]
|
||||||
|
|
||||||
|
return {
|
||||||
|
'metadata': metadata,
|
||||||
|
'body': body.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def split_content_by_separator(body: str) -> list:
|
||||||
|
"""按照 --- 分隔符拆分正文为多张卡片内容"""
|
||||||
|
parts = re.split(r'\n---+\n', body)
|
||||||
|
return [part.strip() for part in parts if part.strip()]
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_content_height(content: str) -> int:
|
||||||
|
"""预估内容高度(基于字数和元素类型)"""
|
||||||
|
lines = content.split('\n')
|
||||||
|
total_height = 0
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line = line.strip()
|
||||||
|
if not line:
|
||||||
|
total_height += 20 # 空行
|
||||||
|
continue
|
||||||
|
|
||||||
|
# 标题
|
||||||
|
if line.startswith('# '):
|
||||||
|
total_height += 130 # h1: font-size 72 + margin
|
||||||
|
elif line.startswith('## '):
|
||||||
|
total_height += 110 # h2
|
||||||
|
elif line.startswith('### '):
|
||||||
|
total_height += 90 # h3
|
||||||
|
# 代码块
|
||||||
|
elif line.startswith('```'):
|
||||||
|
total_height += 80 # 代码块起始/结束
|
||||||
|
# 列表
|
||||||
|
elif line.startswith(('- ', '* ', '+ ')):
|
||||||
|
total_height += 85 # li: line-height ~1.6, font-size 42
|
||||||
|
# 引用
|
||||||
|
elif line.startswith('>'):
|
||||||
|
total_height += 100 # blockquote padding
|
||||||
|
# 图片
|
||||||
|
elif line.startswith('!['):
|
||||||
|
total_height += 300 # 图片高度估计
|
||||||
|
# 普通段落
|
||||||
|
else:
|
||||||
|
# 估算字数
|
||||||
|
char_count = len(line)
|
||||||
|
# 一行约25-30个中文字,行高1.7,字体42px
|
||||||
|
lines_needed = max(1, char_count / 28)
|
||||||
|
total_height += int(lines_needed * 42 * 1.7) + 35 # + margin-bottom
|
||||||
|
|
||||||
|
return total_height
|
||||||
|
|
||||||
|
|
||||||
|
def smart_split_content(content: str, max_height: int = SAFE_HEIGHT) -> List[str]:
|
||||||
|
"""
|
||||||
|
智能拆分内容到多张卡片
|
||||||
|
基于预估高度进行拆分,尽量保持段落完整
|
||||||
|
"""
|
||||||
|
# 首先尝试识别内容块(以标题或空行分隔)
|
||||||
|
blocks = []
|
||||||
|
current_block = []
|
||||||
|
|
||||||
|
lines = content.split('\n')
|
||||||
|
i = 0
|
||||||
|
while i < len(lines):
|
||||||
|
line = lines[i]
|
||||||
|
|
||||||
|
# 新标题开始新块(除非是第一个)
|
||||||
|
if line.strip().startswith('#') and current_block:
|
||||||
|
blocks.append('\n'.join(current_block))
|
||||||
|
current_block = [line]
|
||||||
|
# 分隔线
|
||||||
|
elif line.strip() == '---':
|
||||||
|
if current_block:
|
||||||
|
blocks.append('\n'.join(current_block))
|
||||||
|
current_block = []
|
||||||
|
else:
|
||||||
|
current_block.append(line)
|
||||||
|
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
if current_block:
|
||||||
|
blocks.append('\n'.join(current_block))
|
||||||
|
|
||||||
|
# 如果没有明显的块边界,按段落拆分
|
||||||
|
if len(blocks) <= 1:
|
||||||
|
blocks = [b for b in content.split('\n\n') if b.strip()]
|
||||||
|
|
||||||
|
# 合并块到卡片,确保每张卡片高度不超过限制
|
||||||
|
cards = []
|
||||||
|
current_card = []
|
||||||
|
current_height = 0
|
||||||
|
|
||||||
|
for block in blocks:
|
||||||
|
block_height = estimate_content_height(block)
|
||||||
|
|
||||||
|
# 如果单个块就超过限制,需要进一步拆分
|
||||||
|
if block_height > max_height:
|
||||||
|
# 如果当前卡片有内容,先保存
|
||||||
|
if current_card:
|
||||||
|
cards.append('\n\n'.join(current_card))
|
||||||
|
current_card = []
|
||||||
|
current_height = 0
|
||||||
|
|
||||||
|
# 将大块按行拆分
|
||||||
|
lines = block.split('\n')
|
||||||
|
sub_block = []
|
||||||
|
sub_height = 0
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
line_height = estimate_content_height(line)
|
||||||
|
|
||||||
|
if sub_height + line_height > max_height and sub_block:
|
||||||
|
cards.append('\n'.join(sub_block))
|
||||||
|
sub_block = [line]
|
||||||
|
sub_height = line_height
|
||||||
|
else:
|
||||||
|
sub_block.append(line)
|
||||||
|
sub_height += line_height
|
||||||
|
|
||||||
|
if sub_block:
|
||||||
|
cards.append('\n'.join(sub_block))
|
||||||
|
|
||||||
|
# 如果当前卡片加上这个块会超,先保存当前卡片
|
||||||
|
elif current_height + block_height > max_height and current_card:
|
||||||
|
cards.append('\n\n'.join(current_card))
|
||||||
|
current_card = [block]
|
||||||
|
current_height = block_height
|
||||||
|
|
||||||
|
# 否则加入当前卡片
|
||||||
|
else:
|
||||||
|
current_card.append(block)
|
||||||
|
current_height += block_height
|
||||||
|
|
||||||
|
# 保存最后一个卡片
|
||||||
|
if current_card:
|
||||||
|
cards.append('\n\n'.join(current_card))
|
||||||
|
|
||||||
|
return cards if cards else [content]
|
||||||
|
|
||||||
|
|
||||||
|
def convert_markdown_to_html(md_content: str, style: dict = None) -> str:
|
||||||
|
"""将 Markdown 转换为 HTML"""
|
||||||
|
style = style or STYLES["purple"]
|
||||||
|
|
||||||
|
# 处理 tags(以 # 开头的标签)
|
||||||
|
tags_pattern = r'((?:#[\w\u4e00-\u9fa5]+\s*)+)$'
|
||||||
|
tags_match = re.search(tags_pattern, md_content, re.MULTILINE)
|
||||||
|
tags_html = ""
|
||||||
|
|
||||||
|
if tags_match:
|
||||||
|
tags_str = tags_match.group(1)
|
||||||
|
md_content = md_content[:tags_match.start()].strip()
|
||||||
|
tags = re.findall(r'#([\w\u4e00-\u9fa5]+)', tags_str)
|
||||||
|
if tags:
|
||||||
|
accent = style.get('accent_color', '#6366f1')
|
||||||
|
tags_html = f'<div class="tags-container">'
|
||||||
|
for tag in tags:
|
||||||
|
tags_html += f'<span class="tag" style="background: {accent};">#{tag}</span>'
|
||||||
|
tags_html += '</div>'
|
||||||
|
|
||||||
|
# 转换 Markdown 为 HTML
|
||||||
|
html = markdown.markdown(
|
||||||
|
md_content,
|
||||||
|
extensions=['extra', 'codehilite', 'tables', 'nl2br']
|
||||||
|
)
|
||||||
|
|
||||||
|
return html + tags_html
|
||||||
|
|
||||||
|
|
||||||
|
def generate_cover_html(metadata: dict, style_key: str = "purple") -> str:
|
||||||
|
"""生成封面 HTML"""
|
||||||
|
style = STYLES.get(style_key, STYLES["purple"])
|
||||||
|
|
||||||
|
emoji = metadata.get('emoji', '📝')
|
||||||
|
title = metadata.get('title', '标题')
|
||||||
|
subtitle = metadata.get('subtitle', '')
|
||||||
|
|
||||||
|
# 限制标题和副标题长度
|
||||||
|
if len(title) > 15:
|
||||||
|
title = title[:15]
|
||||||
|
if len(subtitle) > 15:
|
||||||
|
subtitle = subtitle[:15]
|
||||||
|
|
||||||
|
# 暗黑模式特殊处理
|
||||||
|
is_dark = style_key == "dark"
|
||||||
|
text_color = "#ffffff" if is_dark else "#000000"
|
||||||
|
title_gradient = "linear-gradient(180deg, #ffffff 0%, #cccccc 100%)" if is_dark else "linear-gradient(180deg, #2E67B1 0%, #4C4C4C 100%)"
|
||||||
|
inner_bg = "#1a1a2e" if is_dark else "#F3F3F3"
|
||||||
|
|
||||||
|
return f'''<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080, height=1440">
|
||||||
|
<title>小红书封面</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px; height: 1440px; overflow: hidden;
|
||||||
|
}}
|
||||||
|
.cover-container {{
|
||||||
|
width: 1080px; height: 1440px;
|
||||||
|
background: {style['cover_bg']};
|
||||||
|
position: relative; overflow: hidden;
|
||||||
|
}}
|
||||||
|
.cover-inner {{
|
||||||
|
position: absolute; width: 950px; height: 1310px;
|
||||||
|
left: 65px; top: 65px;
|
||||||
|
background: {inner_bg};
|
||||||
|
border-radius: 25px;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
padding: 80px 85px;
|
||||||
|
}}
|
||||||
|
.cover-emoji {{ font-size: 180px; line-height: 1.2; margin-bottom: 50px; }}
|
||||||
|
.cover-title {{
|
||||||
|
font-weight: 900; font-size: 130px; line-height: 1.4;
|
||||||
|
background: {title_gradient};
|
||||||
|
-webkit-background-clip: text;
|
||||||
|
-webkit-text-fill-color: transparent;
|
||||||
|
background-clip: text;
|
||||||
|
flex: 1;
|
||||||
|
display: flex; align-items: flex-start;
|
||||||
|
word-break: break-all;
|
||||||
|
}}
|
||||||
|
.cover-subtitle {{
|
||||||
|
font-weight: 350; font-size: 72px; line-height: 1.4;
|
||||||
|
color: {text_color};
|
||||||
|
margin-top: auto;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="cover-container">
|
||||||
|
<div class="cover-inner">
|
||||||
|
<div class="cover-emoji">{emoji}</div>
|
||||||
|
<div class="cover-title">{title}</div>
|
||||||
|
<div class="cover-subtitle">{subtitle}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
|
||||||
|
def generate_card_html(content: str, page_number: int = 1, total_pages: int = 1,
|
||||||
|
style_key: str = "purple") -> str:
|
||||||
|
"""生成正文卡片 HTML"""
|
||||||
|
style = STYLES.get(style_key, STYLES["purple"])
|
||||||
|
html_content = convert_markdown_to_html(content, style)
|
||||||
|
page_text = f"{page_number}/{total_pages}" if total_pages > 1 else ""
|
||||||
|
|
||||||
|
# 暗黑模式特殊处理
|
||||||
|
is_dark = style_key == "dark"
|
||||||
|
card_bg = "rgba(30, 30, 46, 0.95)" if is_dark else "rgba(255, 255, 255, 0.95)"
|
||||||
|
text_color = "#e0e0e0" if is_dark else "#475569"
|
||||||
|
heading_color = "#ffffff" if is_dark else "#1e293b"
|
||||||
|
h2_color = "#e0e0e0" if is_dark else "#334155"
|
||||||
|
h3_color = "#c0c0c0" if is_dark else "#475569"
|
||||||
|
code_bg = "#0f0f23" if is_dark else "#1e293b"
|
||||||
|
pre_bg = "#0f0f23" if is_dark else "#1e293b"
|
||||||
|
blockquote_bg = "#252540" if is_dark else "#f1f5f9"
|
||||||
|
blockquote_border = style['accent_color']
|
||||||
|
blockquote_color = "#a0a0a0" if is_dark else "#64748b"
|
||||||
|
|
||||||
|
return f'''<!DOCTYPE html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=1080">
|
||||||
|
<title>小红书卡片</title>
|
||||||
|
<style>
|
||||||
|
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700;900&display=swap');
|
||||||
|
* {{ margin: 0; padding: 0; box-sizing: border-box; }}
|
||||||
|
body {{
|
||||||
|
font-family: 'Noto Sans SC', 'Source Han Sans CN', 'PingFang SC', 'Microsoft YaHei', sans-serif;
|
||||||
|
width: 1080px; min-height: 1440px; overflow: hidden; background: transparent;
|
||||||
|
}}
|
||||||
|
.card-container {{
|
||||||
|
width: 1080px; min-height: 1440px;
|
||||||
|
background: {style['card_bg']};
|
||||||
|
position: relative; padding: 50px; overflow: hidden;
|
||||||
|
}}
|
||||||
|
.card-inner {{
|
||||||
|
background: {card_bg};
|
||||||
|
border-radius: 20px;
|
||||||
|
padding: 60px;
|
||||||
|
min-height: calc(1440px - 100px);
|
||||||
|
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(10px);
|
||||||
|
}}
|
||||||
|
.card-content {{
|
||||||
|
color: {text_color};
|
||||||
|
font-size: 42px;
|
||||||
|
line-height: 1.7;
|
||||||
|
}}
|
||||||
|
.card-content h1 {{
|
||||||
|
font-size: 72px; font-weight: 700; color: {heading_color};
|
||||||
|
margin-bottom: 40px; line-height: 1.3;
|
||||||
|
}}
|
||||||
|
.card-content h2 {{
|
||||||
|
font-size: 56px; font-weight: 600; color: {h2_color};
|
||||||
|
margin: 50px 0 25px 0; line-height: 1.4;
|
||||||
|
}}
|
||||||
|
.card-content h3 {{
|
||||||
|
font-size: 48px; font-weight: 600; color: {h3_color};
|
||||||
|
margin: 40px 0 20px 0;
|
||||||
|
}}
|
||||||
|
.card-content p {{ margin-bottom: 35px; }}
|
||||||
|
.card-content strong {{ font-weight: 700; color: {heading_color}; }}
|
||||||
|
.card-content em {{ font-style: italic; color: {style['accent_color']}; }}
|
||||||
|
.card-content a {{
|
||||||
|
color: {style['accent_color']}; text-decoration: none;
|
||||||
|
border-bottom: 2px solid {style['accent_color']};
|
||||||
|
}}
|
||||||
|
.card-content ul, .card-content ol {{
|
||||||
|
margin: 30px 0; padding-left: 60px;
|
||||||
|
}}
|
||||||
|
.card-content li {{ margin-bottom: 20px; line-height: 1.6; }}
|
||||||
|
.card-content blockquote {{
|
||||||
|
border-left: 8px solid {blockquote_border};
|
||||||
|
padding-left: 40px;
|
||||||
|
background: {blockquote_bg};
|
||||||
|
padding-top: 25px; padding-bottom: 25px; padding-right: 30px;
|
||||||
|
margin: 35px 0;
|
||||||
|
color: {blockquote_color};
|
||||||
|
font-style: italic;
|
||||||
|
border-radius: 0 12px 12px 0;
|
||||||
|
}}
|
||||||
|
.card-content blockquote p {{ margin: 0; }}
|
||||||
|
.card-content code {{
|
||||||
|
background: {'#252540' if is_dark else '#f1f5f9'};
|
||||||
|
padding: 6px 16px; border-radius: 8px;
|
||||||
|
font-family: 'SF Mono', 'Monaco', 'Consolas', monospace;
|
||||||
|
font-size: 38px;
|
||||||
|
color: {style['accent_color']};
|
||||||
|
}}
|
||||||
|
.card-content pre {{
|
||||||
|
background: {pre_bg};
|
||||||
|
color: {'#e0e0e0' if is_dark else '#e2e8f0'};
|
||||||
|
padding: 40px; border-radius: 16px;
|
||||||
|
margin: 35px 0;
|
||||||
|
overflow-x: visible;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
word-break: break-all;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
font-size: 36px; line-height: 1.5;
|
||||||
|
}}
|
||||||
|
.card-content pre code {{
|
||||||
|
background: transparent; color: inherit; padding: 0; font-size: inherit;
|
||||||
|
}}
|
||||||
|
.card-content img {{
|
||||||
|
max-width: 100%; height: auto; border-radius: 16px;
|
||||||
|
margin: 35px auto; display: block;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
|
||||||
|
}}
|
||||||
|
.card-content hr {{
|
||||||
|
border: none; height: 2px;
|
||||||
|
background: {'#333355' if is_dark else '#e2e8f0'};
|
||||||
|
margin: 50px 0;
|
||||||
|
}}
|
||||||
|
.tags-container {{
|
||||||
|
margin-top: 50px; padding-top: 30px;
|
||||||
|
border-top: 2px solid {'#333355' if is_dark else '#e2e8f0'};
|
||||||
|
}}
|
||||||
|
.tag {{
|
||||||
|
display: inline-block;
|
||||||
|
background: {style['accent_color']};
|
||||||
|
color: white;
|
||||||
|
padding: 12px 28px; border-radius: 30px;
|
||||||
|
font-size: 34px;
|
||||||
|
margin: 10px 15px 10px 0;
|
||||||
|
font-weight: 500;
|
||||||
|
}}
|
||||||
|
.page-number {{
|
||||||
|
position: absolute;
|
||||||
|
bottom: 80px; right: 80px;
|
||||||
|
font-size: 36px;
|
||||||
|
color: rgba(255, 255, 255, 0.8);
|
||||||
|
font-weight: 500;
|
||||||
|
}}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="card-container">
|
||||||
|
<div class="card-inner">
|
||||||
|
<div class="card-content">
|
||||||
|
{html_content}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="page-number">{page_text}</div>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>'''
|
||||||
|
|
||||||
|
|
||||||
|
async def measure_content_height(page: Page, html_content: str) -> int:
|
||||||
|
"""使用 Playwright 测量实际内容高度"""
|
||||||
|
await page.set_content(html_content, wait_until='networkidle')
|
||||||
|
await page.wait_for_timeout(300) # 等待字体渲染
|
||||||
|
|
||||||
|
height = await page.evaluate('''() => {
|
||||||
|
const inner = document.querySelector('.card-inner');
|
||||||
|
if (inner) {
|
||||||
|
return inner.scrollHeight;
|
||||||
|
}
|
||||||
|
const container = document.querySelector('.card-container');
|
||||||
|
return container ? container.scrollHeight : document.body.scrollHeight;
|
||||||
|
}''')
|
||||||
|
|
||||||
|
return height
|
||||||
|
|
||||||
|
|
||||||
|
async def render_html_to_image(html_content: str, output_path: str,
|
||||||
|
width: int = CARD_WIDTH, height: int = CARD_HEIGHT):
|
||||||
|
"""使用 Playwright 将 HTML 渲染为图片"""
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
page = await browser.new_page(viewport={'width': width, 'height': height})
|
||||||
|
|
||||||
|
try:
|
||||||
|
await page.set_content(html_content, wait_until='networkidle')
|
||||||
|
await page.wait_for_timeout(300)
|
||||||
|
|
||||||
|
# 截图固定尺寸
|
||||||
|
await page.screenshot(
|
||||||
|
path=output_path,
|
||||||
|
clip={'x': 0, 'y': 0, 'width': width, 'height': height},
|
||||||
|
type='png'
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f" ✅ 已生成: {output_path}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
|
||||||
|
async def process_and_render_cards(card_contents: List[str], output_dir: str,
|
||||||
|
style_key: str) -> List[str]:
|
||||||
|
"""
|
||||||
|
处理卡片内容,检测高度并自动分页,然后渲染
|
||||||
|
返回最终生成的所有卡片文件路径
|
||||||
|
"""
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
page = await browser.new_page(viewport={'width': CARD_WIDTH, 'height': CARD_HEIGHT})
|
||||||
|
|
||||||
|
all_cards = []
|
||||||
|
|
||||||
|
try:
|
||||||
|
for content in card_contents:
|
||||||
|
# 预估内容高度
|
||||||
|
estimated_height = estimate_content_height(content)
|
||||||
|
|
||||||
|
# 如果预估高度超过安全高度,尝试拆分
|
||||||
|
if estimated_height > SAFE_HEIGHT:
|
||||||
|
split_contents = smart_split_content(content, SAFE_HEIGHT)
|
||||||
|
else:
|
||||||
|
split_contents = [content]
|
||||||
|
|
||||||
|
# 验证每个拆分后的内容
|
||||||
|
for split_content in split_contents:
|
||||||
|
# 生成临时 HTML 测量
|
||||||
|
temp_html = generate_card_html(split_content, 1, 1, style_key)
|
||||||
|
actual_height = await measure_content_height(page, temp_html)
|
||||||
|
|
||||||
|
# 如果仍然超出,进一步按行拆分
|
||||||
|
if actual_height > CARD_HEIGHT - 100:
|
||||||
|
lines = split_content.split('\n')
|
||||||
|
sub_contents = []
|
||||||
|
sub_lines = []
|
||||||
|
sub_height = 0
|
||||||
|
|
||||||
|
for line in lines:
|
||||||
|
test_lines = sub_lines + [line]
|
||||||
|
test_html = generate_card_html('\n'.join(test_lines), 1, 1, style_key)
|
||||||
|
test_height = await measure_content_height(page, test_html)
|
||||||
|
|
||||||
|
if test_height > CARD_HEIGHT - 100 and sub_lines:
|
||||||
|
sub_contents.append('\n'.join(sub_lines))
|
||||||
|
sub_lines = [line]
|
||||||
|
else:
|
||||||
|
sub_lines = test_lines
|
||||||
|
|
||||||
|
if sub_lines:
|
||||||
|
sub_contents.append('\n'.join(sub_lines))
|
||||||
|
|
||||||
|
all_cards.extend(sub_contents)
|
||||||
|
else:
|
||||||
|
all_cards.append(split_content)
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
return all_cards
|
||||||
|
|
||||||
|
|
||||||
|
async def render_markdown_to_cards(md_file: str, output_dir: str, style_key: str = "purple"):
|
||||||
|
"""主渲染函数:将 Markdown 文件渲染为多张卡片图片"""
|
||||||
|
print(f"\n🎨 开始渲染: {md_file}")
|
||||||
|
print(f"🎨 使用样式: {STYLES[style_key]['name']}")
|
||||||
|
|
||||||
|
# 确保输出目录存在
|
||||||
|
os.makedirs(output_dir, exist_ok=True)
|
||||||
|
|
||||||
|
# 解析 Markdown 文件
|
||||||
|
data = parse_markdown_file(md_file)
|
||||||
|
metadata = data['metadata']
|
||||||
|
body = data['body']
|
||||||
|
|
||||||
|
# 分割正文内容(基于用户手动分隔符)
|
||||||
|
card_contents = split_content_by_separator(body)
|
||||||
|
print(f" 📄 检测到 {len(card_contents)} 个内容块")
|
||||||
|
|
||||||
|
# 处理内容,智能分页
|
||||||
|
print(" 🔍 分析内容高度并智能分页...")
|
||||||
|
processed_cards = await process_and_render_cards(card_contents, output_dir, style_key)
|
||||||
|
total_cards = len(processed_cards)
|
||||||
|
print(f" 📄 将生成 {total_cards} 张卡片")
|
||||||
|
|
||||||
|
# 生成封面
|
||||||
|
if metadata.get('emoji') or metadata.get('title'):
|
||||||
|
print(" 📷 生成封面...")
|
||||||
|
cover_html = generate_cover_html(metadata, style_key)
|
||||||
|
cover_path = os.path.join(output_dir, 'cover.png')
|
||||||
|
await render_html_to_image(cover_html, cover_path)
|
||||||
|
|
||||||
|
# 生成正文卡片
|
||||||
|
async with async_playwright() as p:
|
||||||
|
browser = await p.chromium.launch()
|
||||||
|
page = await browser.new_page(viewport={'width': CARD_WIDTH, 'height': CARD_HEIGHT})
|
||||||
|
|
||||||
|
try:
|
||||||
|
for i, content in enumerate(processed_cards, 1):
|
||||||
|
print(f" 📷 生成卡片 {i}/{total_cards}...")
|
||||||
|
card_html = generate_card_html(content, i, total_cards, style_key)
|
||||||
|
card_path = os.path.join(output_dir, f'card_{i}.png')
|
||||||
|
|
||||||
|
await page.set_content(card_html, wait_until='networkidle')
|
||||||
|
await page.wait_for_timeout(300)
|
||||||
|
|
||||||
|
await page.screenshot(
|
||||||
|
path=card_path,
|
||||||
|
clip={'x': 0, 'y': 0, 'width': CARD_WIDTH, 'height': CARD_HEIGHT},
|
||||||
|
type='png'
|
||||||
|
)
|
||||||
|
print(f" ✅ 已生成: {card_path}")
|
||||||
|
|
||||||
|
finally:
|
||||||
|
await browser.close()
|
||||||
|
|
||||||
|
print(f"\n✨ 渲染完成!共生成 {total_cards} 张卡片,保存到: {output_dir}")
|
||||||
|
return total_cards
|
||||||
|
|
||||||
|
|
||||||
|
def list_styles():
|
||||||
|
"""列出所有可用样式"""
|
||||||
|
print("\n📋 可用样式列表:")
|
||||||
|
print("-" * 40)
|
||||||
|
for key, style in STYLES.items():
|
||||||
|
print(f" {key:12} - {style['name']}")
|
||||||
|
print("-" * 40)
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description='将 Markdown 文件渲染为小红书风格的图片卡片(智能分页版)',
|
||||||
|
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||||
|
epilog='''
|
||||||
|
示例:
|
||||||
|
python render_xhs_v2.py note.md
|
||||||
|
python render_xhs_v2.py note.md -o ./output --style xiaohongshu
|
||||||
|
python render_xhs_v2.py --list-styles
|
||||||
|
'''
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'markdown_file',
|
||||||
|
nargs='?',
|
||||||
|
help='Markdown 文件路径'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--output-dir', '-o',
|
||||||
|
default=os.getcwd(),
|
||||||
|
help='输出目录(默认为当前工作目录)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--style', '-s',
|
||||||
|
default='purple',
|
||||||
|
choices=list(STYLES.keys()),
|
||||||
|
help='样式主题(默认: purple)'
|
||||||
|
)
|
||||||
|
parser.add_argument(
|
||||||
|
'--list-styles',
|
||||||
|
action='store_true',
|
||||||
|
help='列出所有可用样式'
|
||||||
|
)
|
||||||
|
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
if args.list_styles:
|
||||||
|
list_styles()
|
||||||
|
return
|
||||||
|
|
||||||
|
if not args.markdown_file:
|
||||||
|
parser.print_help()
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
if not os.path.exists(args.markdown_file):
|
||||||
|
print(f"❌ 错误: 文件不存在 - {args.markdown_file}")
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
asyncio.run(render_markdown_to_cards(args.markdown_file, args.output_dir, args.style))
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user